diff --git a/.codex/skills/mvnf/scripts/mvnf.py b/.codex/skills/mvnf/scripts/mvnf.py index d7e600fcaff..076ae9ab27b 100755 --- a/.codex/skills/mvnf/scripts/mvnf.py +++ b/.codex/skills/mvnf/scripts/mvnf.py @@ -276,7 +276,7 @@ def main() -> int: print(f"Test selector: {test_selector} ({'failsafe' if args.it else 'surefire'})") clean_cmd = mvn_cmd + common_flags + ["-pl", module, "clean"] - install_cmd = mvn_cmd + (offline_flag + ["-T", "1C", "-Dmaven.repo.local=.m2_repo", "-Pquick", "clean", "install"]) + install_cmd = mvn_cmd + (offline_flag + ["-T", "1C", "-Dmaven.repo.local=.m2_repo", "-Pquick", "install"]) verify_cmd = mvn_cmd + common_flags + ["-pl", module] if test_selector is not None: @@ -298,7 +298,7 @@ def main() -> int: rc, _ = _run(install_cmd, repo_root, args.tail, log_paths[1], args.stream) if rc != 0: - print("\n[mvnf] Root clean install failed.") + print("\n[mvnf] Root install failed.") return rc rc, _ = _run(verify_cmd, repo_root, args.tail, log_paths[2], args.stream) diff --git a/.gitignore b/.gitignore index e986d3c508a..0ab8eda86b8 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,4 @@ e2e/test-results .m2_repo/ .serena/ .vscode +/.codex/environments/environment.toml diff --git a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java index 58a7803a557..e3d816052e3 100644 --- a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java +++ b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/CONFIG.java @@ -342,6 +342,12 @@ public static final class Shacl { public final static IRI rdfsSubClassReasoning = createIRI(NAMESPACE, "shacl.rdfsSubClassReasoning"); + /** + * tag:rdf4j.org,2023:config/shacl.includeInferredStatements + */ + public final static IRI includeInferredStatements = createIRI(NAMESPACE, + "shacl.includeInferredStatements"); + /** * tag:rdf4j.org,2023:config/shacl.performanceLogging */ diff --git a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RSX.java b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RSX.java index 113f3083eeb..dafe56b5f24 100644 --- a/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RSX.java +++ b/core/model-vocabulary/src/main/java/org/eclipse/rdf4j/model/vocabulary/RSX.java @@ -40,6 +40,9 @@ public class RSX { /** http://rdf4j.org/shacl-extensions#targetShape */ public final static IRI targetShape = create("targetShape"); + public final static IRI rdfsSubClassReasoning = create("rdfsSubClassReasoning"); + public final static IRI includeInferredStatements = create("includeInferredStatements"); + public final static IRI dataGraph = create("dataGraph"); public final static IRI shapesGraph = create("shapesGraph"); diff --git a/core/sail/inferencer/src/main/java/org/eclipse/rdf4j/sail/inferencer/fc/SchemaCachingRDFSInferencerConnection.java b/core/sail/inferencer/src/main/java/org/eclipse/rdf4j/sail/inferencer/fc/SchemaCachingRDFSInferencerConnection.java index 95b1df1e955..3a9bd33a2b7 100644 --- a/core/sail/inferencer/src/main/java/org/eclipse/rdf4j/sail/inferencer/fc/SchemaCachingRDFSInferencerConnection.java +++ b/core/sail/inferencer/src/main/java/org/eclipse/rdf4j/sail/inferencer/fc/SchemaCachingRDFSInferencerConnection.java @@ -114,7 +114,7 @@ void processForSchemaCache(Statement statement) { sail.addSubPropertyOfStatement( sail.getValueFactory().createStatement(subject, RDFS.SUBPROPERTYOF, RDFS.MEMBER)); schemaChange = true; - } else if (predicate.equals(RDF.TYPE)) { + } else if (predicate.equals(RDF.TYPE) && object instanceof Resource) { if (!sail.hasType(((Resource) object))) { sail.addType((Resource) object); schemaChange = true; @@ -276,11 +276,7 @@ private void addStatement(boolean actuallyAdd, Resource subject, IRI predicate, } - if (predicate.equals(RDF.TYPE)) { - if (!(object instanceof Resource)) { - throw new SailException("Expected object to a a Resource: " + object.toString()); - } - + if (predicate.equals(RDF.TYPE) && object instanceof Resource) { sail.resolveTypes((Resource) object) .stream() .peek(inferredType -> { diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailBaseConfiguration.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailBaseConfiguration.java index cc43e3e6015..dbab12bc961 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailBaseConfiguration.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailBaseConfiguration.java @@ -35,6 +35,7 @@ abstract class ShaclSailBaseConfiguration extends NotifyingSailWrapper { private boolean validationEnabled = ShaclSailConfig.VALIDATION_ENABLED_DEFAULT; private boolean cacheSelectNodes = ShaclSailConfig.CACHE_SELECT_NODES_DEFAULT; private boolean rdfsSubClassReasoning = ShaclSailConfig.RDFS_SUB_CLASS_REASONING_DEFAULT; + private boolean includeInferredStatements = true; private boolean serializableValidation = ShaclSailConfig.SERIALIZABLE_VALIDATION_DEFAULT; private boolean performanceLogging = ShaclSailConfig.PERFORMANCE_LOGGING_DEFAULT; private boolean eclipseRdf4jShaclExtensions = ShaclSailConfig.ECLIPSE_RDF4J_SHACL_EXTENSIONS_DEFAULT; @@ -146,6 +147,25 @@ public void setRdfsSubClassReasoning(boolean rdfsSubClassReasoning) { this.rdfsSubClassReasoning = rdfsSubClassReasoning; } + /** + * Check if inferred statements from the base sail should be used during SHACL validation when + * {@link #isRdfsSubClassReasoning()} is disabled. + * + * @return true if inferred statements should be considered, false otherwise. + */ + public boolean isIncludeInferredStatements() { + return includeInferredStatements; + } + + /** + * Allow SHACL validation to use inferred statements from the base sail when RDFS subclass reasoning is disabled. + * + * @param includeInferredStatements default true + */ + public void setIncludeInferredStatements(boolean includeInferredStatements) { + this.includeInferredStatements = includeInferredStatements; + } + /** * Disable the SHACL validation on commit() */ diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailConnection.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailConnection.java index c8c6db97dcb..01b7ac82dd9 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailConnection.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailConnection.java @@ -51,6 +51,7 @@ import org.eclipse.rdf4j.sail.memory.MemoryStore; import org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.ValidationApproach; import org.eclipse.rdf4j.sail.shacl.ast.ContextWithShape; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; import org.eclipse.rdf4j.sail.shacl.results.lazy.LazyValidationReport; import org.eclipse.rdf4j.sail.shacl.results.lazy.ValidationResultIterator; @@ -76,11 +77,25 @@ public class ShaclSailConnection extends NotifyingSailConnectionWrapper implemen Sail addedStatements; Sail removedStatements; + Sail addedStatementsInferred; + Sail removedStatementsInferred; + Sail addedStatementsRdfsInferred; + Sail removedStatementsRdfsInferred; + Sail addedStatementsWithInferred; + Sail removedStatementsWithInferred; + Sail addedStatementsWithRdfsInferred; + Sail removedStatementsWithRdfsInferred; + Sail addedStatementsWithInferredAndRdfs; + Sail removedStatementsWithInferredAndRdfs; private final HashSet addedStatementsSet = new HashSet<>(); private final HashSet removedStatementsSet = new HashSet<>(); + private final HashSet addedStatementsInferredSet = new HashSet<>(); + private final HashSet removedStatementsInferredSet = new HashSet<>(); private boolean shapeRefreshNeeded = false; + private boolean legacyStatementAddedWithoutInferredFlagObserved = false; + private boolean legacyStatementRemovedWithoutInferredFlagObserved = false; private boolean shapesModifiedInCurrentTransaction = false; public final ShaclSail sail; @@ -303,7 +318,7 @@ public void addStatement(UpdateContext modify, Resource subj, IRI pred, Value ob throws SailException { if (useDefaultShapesGraph && contexts.length == 1 && RDF4J.SHACL_SHAPE_GRAPH.equals(contexts[0])) { shapesRepoConnection.add(subj, pred, obj, contexts); - shapeRefreshNeeded = true; + markShapesRefreshNeeded(); } else { super.addStatement(modify, subj, pred, obj, contexts); } @@ -314,7 +329,7 @@ public void removeStatement(UpdateContext modify, Resource subj, IRI pred, Value throws SailException { if (useDefaultShapesGraph && contexts.length == 1 && RDF4J.SHACL_SHAPE_GRAPH.equals(contexts[0])) { shapesRepoConnection.remove(subj, pred, obj, contexts); - shapeRefreshNeeded = true; + markShapesRefreshNeeded(); } else { super.removeStatement(modify, subj, pred, obj, contexts); } @@ -324,7 +339,7 @@ public void removeStatement(UpdateContext modify, Resource subj, IRI pred, Value public void addStatement(Resource subj, IRI pred, Value obj, Resource... contexts) throws SailException { if (useDefaultShapesGraph && contexts.length == 1 && RDF4J.SHACL_SHAPE_GRAPH.equals(contexts[0])) { shapesRepoConnection.add(subj, pred, obj, contexts); - shapeRefreshNeeded = true; + markShapesRefreshNeeded(); } else { super.addStatement(subj, pred, obj, contexts); } @@ -334,7 +349,7 @@ public void addStatement(Resource subj, IRI pred, Value obj, Resource... context public void removeStatements(Resource subj, IRI pred, Value obj, Resource... contexts) throws SailException { if (useDefaultShapesGraph && contexts.length == 1 && RDF4J.SHACL_SHAPE_GRAPH.equals(contexts[0])) { shapesRepoConnection.remove(subj, pred, obj, contexts); - shapeRefreshNeeded = true; + markShapesRefreshNeeded(); } else { super.removeStatements(subj, pred, obj, contexts); } @@ -344,7 +359,7 @@ public void removeStatements(Resource subj, IRI pred, Value obj, Resource... con public void clear(Resource... contexts) throws SailException { if (Arrays.asList(contexts).contains(RDF4J.SHACL_SHAPE_GRAPH)) { shapesRepoConnection.clear(); - shapeRefreshNeeded = true; + markShapesRefreshNeeded(); } super.clear(contexts); } @@ -427,12 +442,56 @@ private void cleanup() { removedStatements.shutDown(); removedStatements = null; } + if (addedStatementsInferred != null) { + addedStatementsInferred.shutDown(); + addedStatementsInferred = null; + } + if (removedStatementsInferred != null) { + removedStatementsInferred.shutDown(); + removedStatementsInferred = null; + } + if (addedStatementsRdfsInferred != null) { + addedStatementsRdfsInferred.shutDown(); + addedStatementsRdfsInferred = null; + } + if (removedStatementsRdfsInferred != null) { + removedStatementsRdfsInferred.shutDown(); + removedStatementsRdfsInferred = null; + } + if (addedStatementsWithInferred != null) { + addedStatementsWithInferred.shutDown(); + addedStatementsWithInferred = null; + } + if (removedStatementsWithInferred != null) { + removedStatementsWithInferred.shutDown(); + removedStatementsWithInferred = null; + } + if (addedStatementsWithRdfsInferred != null) { + addedStatementsWithRdfsInferred.shutDown(); + addedStatementsWithRdfsInferred = null; + } + if (removedStatementsWithRdfsInferred != null) { + removedStatementsWithRdfsInferred.shutDown(); + removedStatementsWithRdfsInferred = null; + } + if (addedStatementsWithInferredAndRdfs != null) { + addedStatementsWithInferredAndRdfs.shutDown(); + addedStatementsWithInferredAndRdfs = null; + } + if (removedStatementsWithInferredAndRdfs != null) { + removedStatementsWithInferredAndRdfs.shutDown(); + removedStatementsWithInferredAndRdfs = null; + } addedStatementsSet.clear(); removedStatementsSet.clear(); + addedStatementsInferredSet.clear(); + removedStatementsInferredSet.clear(); stats = null; prepareHasBeenCalled = false; shapeRefreshNeeded = false; + legacyStatementAddedWithoutInferredFlagObserved = false; + legacyStatementRemovedWithoutInferredFlagObserved = false; shapesModifiedInCurrentTransaction = false; currentIsolationLevel = null; @@ -497,7 +556,8 @@ private ValidationReport validate(List shapes, boolean validat try { try (ConnectionsGroup connectionsGroup = getConnectionsGroup()) { - return performValidation(shapes, validateEntireBaseSail, connectionsGroup); + return performValidation(shapes, validateEntireBaseSail, connectionsGroup, this, + previousStateConnection); } } finally { rdfsSubClassOfReasoner = null; @@ -505,12 +565,15 @@ private ValidationReport validate(List shapes, boolean validat } - void prepareValidation(ValidationSettings validationSettings) throws InterruptedException { + void prepareValidation(ValidationSettings validationSettings, boolean requireRdfsSubClassReasoning) + throws InterruptedException { assert isValidationEnabled(); - if (sail.isRdfsSubClassReasoning()) { + if (requireRdfsSubClassReasoning) { rdfsSubClassOfReasoner = RdfsSubClassOfReasoner.createReasoner(this, validationSettings); + } else { + rdfsSubClassOfReasoner = null; } if (sail.isShutdown()) { @@ -528,15 +591,143 @@ void prepareValidation(ValidationSettings validationSettings) throws Interrupted } ConnectionsGroup getConnectionsGroup() { + return getConnectionsGroup(this, previousStateConnection, sail.isIncludeInferredStatements(), + sail.isRdfsSubClassReasoning()); + } + + ConnectionsGroup getConnectionsGroup(SailConnection baseConnection, SailConnection previousStateConnection, + boolean includeInferredStatements, boolean useRdfsSubClassReasoning) { + RdfsSubClassOfReasoner reasoner = useRdfsSubClassReasoning ? rdfsSubClassOfReasoner : null; + ConnectionsGroup.RdfsSubClassOfReasonerProvider provider = reasoner == null ? null : () -> reasoner; + Sail effectiveAddedStatements = getEffectiveAddedStatements(includeInferredStatements, + useRdfsSubClassReasoning); + Sail effectiveRemovedStatements = getEffectiveRemovedStatements(includeInferredStatements, + useRdfsSubClassReasoning); + + return new ConnectionsGroup( + new VerySimpleRdfsBackwardsChainingConnection(baseConnection, reasoner, includeInferredStatements), + previousStateConnection, effectiveAddedStatements, effectiveRemovedStatements, stats, + provider, includeInferredStatements, transactionSettings, sail.sparqlValidation); + } + + private Sail getEffectiveAddedStatements(boolean includeInferredStatements, boolean useRdfsSubClassReasoning) { + boolean includeBaseInferred = includeInferredStatements && addedStatementsInferred != null; + boolean includeRdfsInferred = useRdfsSubClassReasoning && addedStatementsRdfsInferred != null; + if (!includeBaseInferred && !includeRdfsInferred) { + return addedStatements; + } + if (includeBaseInferred && includeRdfsInferred) { + if (addedStatementsWithInferredAndRdfs == null) { + addedStatementsWithInferredAndRdfs = buildCombinedStatements(addedStatements, + addedStatementsInferred, addedStatementsRdfsInferred); + } + return addedStatementsWithInferredAndRdfs; + } + if (includeBaseInferred) { + if (addedStatementsWithInferred == null) { + addedStatementsWithInferred = buildCombinedStatements(addedStatements, addedStatementsInferred, null); + } + return addedStatementsWithInferred; + } + if (addedStatementsWithRdfsInferred == null) { + addedStatementsWithRdfsInferred = buildCombinedStatements(addedStatements, null, + addedStatementsRdfsInferred); + } + return addedStatementsWithRdfsInferred; + } + + private Sail getEffectiveRemovedStatements(boolean includeInferredStatements, boolean useRdfsSubClassReasoning) { + boolean includeBaseInferred = includeInferredStatements && removedStatementsInferred != null; + boolean includeRdfsInferred = useRdfsSubClassReasoning && removedStatementsRdfsInferred != null; + if (!includeBaseInferred && !includeRdfsInferred) { + return removedStatements; + } + if (includeBaseInferred && includeRdfsInferred) { + if (removedStatementsWithInferredAndRdfs == null) { + removedStatementsWithInferredAndRdfs = buildCombinedStatements(removedStatements, + removedStatementsInferred, removedStatementsRdfsInferred); + } + return removedStatementsWithInferredAndRdfs; + } + if (includeBaseInferred) { + if (removedStatementsWithInferred == null) { + removedStatementsWithInferred = buildCombinedStatements(removedStatements, removedStatementsInferred, + null); + } + return removedStatementsWithInferred; + } + if (removedStatementsWithRdfsInferred == null) { + removedStatementsWithRdfsInferred = buildCombinedStatements(removedStatements, null, + removedStatementsRdfsInferred); + } + return removedStatementsWithRdfsInferred; + } + + private Sail buildCombinedStatements(Sail explicitStatements, Sail inferredStatements, + Sail rdfsInferredStatements) { + if (explicitStatements == null && inferredStatements == null && rdfsInferredStatements == null) { + return null; + } + Sail combinedStatements = getNewMemorySail(); + try (SailConnection combinedConnection = combinedStatements.getConnection()) { + combinedConnection.begin(IsolationLevels.NONE); + copyStatements(explicitStatements, combinedConnection); + copyStatements(inferredStatements, combinedConnection); + copyStatements(rdfsInferredStatements, combinedConnection); + combinedConnection.commit(); + } + return combinedStatements; + } + + private void copyStatements(Sail source, SailConnection target) { + if (source == null) { + return; + } + try (SailConnection from = source.getConnection()) { + ConnectionHelper.transferStatements(from, target::addStatement); + } + } - return new ConnectionsGroup(new VerySimpleRdfsBackwardsChainingConnection(this, rdfsSubClassOfReasoner), - previousStateConnection, addedStatements, removedStatements, stats, - this::getRdfsSubClassOfReasoner, transactionSettings, sail.sparqlValidation); + private void resetCombinedStatementStores() { + if (addedStatementsWithInferred != null) { + addedStatementsWithInferred.shutDown(); + addedStatementsWithInferred = null; + } + if (removedStatementsWithInferred != null) { + removedStatementsWithInferred.shutDown(); + removedStatementsWithInferred = null; + } + if (addedStatementsWithRdfsInferred != null) { + addedStatementsWithRdfsInferred.shutDown(); + addedStatementsWithRdfsInferred = null; + } + if (removedStatementsWithRdfsInferred != null) { + removedStatementsWithRdfsInferred.shutDown(); + removedStatementsWithRdfsInferred = null; + } + if (addedStatementsWithInferredAndRdfs != null) { + addedStatementsWithInferredAndRdfs.shutDown(); + addedStatementsWithInferredAndRdfs = null; + } + if (removedStatementsWithInferredAndRdfs != null) { + removedStatementsWithInferredAndRdfs.shutDown(); + removedStatementsWithInferredAndRdfs = null; + } + } + + private boolean requiresRdfsSubClassReasoner(List shapes) { + return shapes.stream() + .map(ContextWithShape::getShape) + .map(Shape::getRdfsSubClassReasoningOverride) + .anyMatch(Boolean.TRUE::equals); } private ValidationReport performValidation(List shapes, boolean validateEntireBaseSail, - ConnectionsGroup connectionsGroup) throws InterruptedException { + ConnectionsGroup connectionsGroup, SailConnection baseConnection, SailConnection previousStateConnection) + throws InterruptedException { long beforeValidation = 0; + boolean defaultIncludeInferredStatements = sail.isIncludeInferredStatements(); + boolean defaultRdfsSubClassReasoning = sail.isRdfsSubClassReasoning(); if (sail.isPerformanceLogging()) { beforeValidation = System.currentTimeMillis(); @@ -547,18 +738,36 @@ private ValidationReport performValidation(List shapes, boolea Stream> callableStream = shapes .stream() - .map(contextWithShapes -> new ShapeValidationContainer( - contextWithShapes.getShape(), - () -> contextWithShapes.getShape() - .generatePlans(connectionsGroup, - new ValidationSettings(contextWithShapes.getDataGraph(), - sail.isLogValidationPlans(), validateEntireBaseSail, - sail.isPerformanceLogging())), - sail.isGlobalLogValidationExecution(), sail.isLogValidationViolations(), - sail.getEffectiveValidationResultsLimitPerConstraint(), sail.isPerformanceLogging(), - sail.isLogValidationPlans(), - logger, - connectionsGroup)) + .map(contextWithShapes -> { + Shape shape = contextWithShapes.getShape(); + boolean shapeRdfsSubClassReasoning = shape + .usesRdfsSubClassReasoning(defaultRdfsSubClassReasoning); + boolean shapeIncludeInferredStatements = shape + .usesIncludeInferredStatements(defaultIncludeInferredStatements); + + boolean closeConnectionsGroup = false; + ConnectionsGroup shapeConnectionsGroup = connectionsGroup; + if (shapeRdfsSubClassReasoning != defaultRdfsSubClassReasoning + || shapeIncludeInferredStatements != defaultIncludeInferredStatements) { + shapeConnectionsGroup = getConnectionsGroup(baseConnection, previousStateConnection, + shapeIncludeInferredStatements, shapeRdfsSubClassReasoning); + closeConnectionsGroup = true; + } + ConnectionsGroup planConnectionsGroup = shapeConnectionsGroup; + + return new ShapeValidationContainer( + shape, + () -> shape.generatePlans(planConnectionsGroup, + new ValidationSettings(contextWithShapes.getDataGraph(), + sail.isLogValidationPlans(), validateEntireBaseSail, + sail.isPerformanceLogging())), + sail.isGlobalLogValidationExecution(), sail.isLogValidationViolations(), + sail.getEffectiveValidationResultsLimitPerConstraint(), sail.isPerformanceLogging(), + sail.isLogValidationPlans(), + logger, + shapeConnectionsGroup, + closeConnectionsGroup); + }) .filter(ShapeValidationContainer::hasPlanNode) .peek(s -> { @@ -750,12 +959,15 @@ void fillAddedAndRemovedStatementRepositories() throws InterruptedException { boolean parallelValidation = isParallelValidation() && !addedStatementsSet.isEmpty() && !removedStatementsSet.isEmpty(); + resetCombinedStatementStores(); + try { Stream.of(addedStatementsSet, removedStatementsSet) .map(set -> (Callable) () -> { Set otherSet; - Sail repository; + Sail explicitRepository; + Sail inferredRepository = null; if (set == addedStatementsSet) { otherSet = removedStatementsSet; @@ -764,7 +976,17 @@ void fillAddedAndRemovedStatementRepositories() throws InterruptedException { } addedStatements = getNewMemorySail(); - repository = addedStatements; + explicitRepository = addedStatements; + if (rdfsSubClassOfReasoner != null) { + if (addedStatementsRdfsInferred != null) { + addedStatementsRdfsInferred.shutDown(); + } + addedStatementsRdfsInferred = getNewMemorySail(); + inferredRepository = addedStatementsRdfsInferred; + } else if (addedStatementsRdfsInferred != null) { + addedStatementsRdfsInferred.shutDown(); + addedStatementsRdfsInferred = null; + } set.forEach(stats::added); @@ -777,13 +999,29 @@ void fillAddedAndRemovedStatementRepositories() throws InterruptedException { } removedStatements = getNewMemorySail(); - repository = removedStatements; + explicitRepository = removedStatements; + if (rdfsSubClassOfReasoner != null) { + if (removedStatementsRdfsInferred != null) { + removedStatementsRdfsInferred.shutDown(); + } + removedStatementsRdfsInferred = getNewMemorySail(); + inferredRepository = removedStatementsRdfsInferred; + } else if (removedStatementsRdfsInferred != null) { + removedStatementsRdfsInferred.shutDown(); + removedStatementsRdfsInferred = null; + } set.forEach(stats::removed); } - try (SailConnection connection = repository.getConnection()) { - connection.begin(IsolationLevels.NONE); + try (SailConnection explicitConnection = explicitRepository.getConnection(); + SailConnection inferredConnection = inferredRepository != null + ? inferredRepository.getConnection() + : null) { + explicitConnection.begin(IsolationLevels.NONE); + if (inferredConnection != null) { + inferredConnection.begin(IsolationLevels.NONE); + } set.stream() .peek(s -> { if (Thread.currentThread().isInterrupted()) { @@ -792,13 +1030,19 @@ void fillAddedAndRemovedStatementRepositories() throws InterruptedException { } }) .filter(statement -> !otherSet.contains(statement)) - .flatMap(statement -> rdfsSubClassOfReasoner == null ? Stream.of(statement) - : rdfsSubClassOfReasoner.forwardChain(statement)) .forEach(statement -> { if (!Thread.currentThread().isInterrupted()) { - connection.addStatement(statement.getSubject(), + explicitConnection.addStatement(statement.getSubject(), statement.getPredicate(), statement.getObject(), statement.getContext()); + if (inferredConnection != null) { + rdfsSubClassOfReasoner.forwardChain(statement) + .forEach(inferredStatement -> inferredConnection + .addStatement(inferredStatement.getSubject(), + inferredStatement.getPredicate(), + inferredStatement.getObject(), + inferredStatement.getContext())); + } } }); @@ -806,7 +1050,10 @@ void fillAddedAndRemovedStatementRepositories() throws InterruptedException { throw new InterruptedException(); } - connection.commit(); + if (inferredConnection != null) { + inferredConnection.commit(); + } + explicitConnection.commit(); } return null; @@ -854,6 +1101,9 @@ void fillAddedAndRemovedStatementRepositories() throws InterruptedException { } } + fillInferredStatementRepository(addedStatementsInferredSet, removedStatementsInferredSet, true); + fillInferredStatementRepository(removedStatementsInferredSet, addedStatementsInferredSet, false); + } finally { if (futures != null) { for (Future future : futures) { @@ -868,6 +1118,63 @@ void fillAddedAndRemovedStatementRepositories() throws InterruptedException { } + private void fillInferredStatementRepository(Set sourceSet, Set otherSet, + boolean added) throws InterruptedException { + if (sourceSet.isEmpty()) { + if (added) { + if (addedStatementsInferred != null) { + addedStatementsInferred.shutDown(); + addedStatementsInferred = null; + } + } else { + if (removedStatementsInferred != null) { + removedStatementsInferred.shutDown(); + removedStatementsInferred = null; + } + } + return; + } + + Sail inferredRepository; + if (added) { + if (addedStatementsInferred != null) { + addedStatementsInferred.shutDown(); + } + addedStatementsInferred = getNewMemorySail(); + inferredRepository = addedStatementsInferred; + sourceSet.forEach(stats::added); + } else { + if (removedStatementsInferred != null) { + removedStatementsInferred.shutDown(); + } + removedStatementsInferred = getNewMemorySail(); + inferredRepository = removedStatementsInferred; + sourceSet.forEach(stats::removed); + } + + try (SailConnection inferredConnection = inferredRepository.getConnection()) { + inferredConnection.begin(IsolationLevels.NONE); + sourceSet.stream() + .peek(s -> { + if (Thread.currentThread().isInterrupted()) { + throw new SailException( + "ShaclSailConnection was interrupted while filling inferred statement repositories"); + } + }) + .filter(statement -> !otherSet.contains(statement)) + .forEach(statement -> { + if (!Thread.currentThread().isInterrupted()) { + inferredConnection.addStatement(statement.getSubject(), + statement.getPredicate(), statement.getObject(), statement.getContext()); + } + }); + if (Thread.interrupted()) { + throw new InterruptedException(); + } + inferredConnection.commit(); + } + } + private IsolationLevel getIsolationLevel() { return currentIsolationLevel; } @@ -1037,7 +1344,8 @@ public void prepare() throws SailException { : "isShapeRefreshNeeded should trigger shapesModifiedInCurrentTransaction once we have loaded the modified shapes, but shapesModifiedInCurrentTransaction should be null until then"; if (!shapeRefreshNeeded && !isBulkValidation() && addedStatementsSet.isEmpty() - && removedStatementsSet.isEmpty()) { + && removedStatementsSet.isEmpty() && addedStatementsInferredSet.isEmpty() + && removedStatementsInferredSet.isEmpty()) { logger.debug("Nothing has changed, nothing to validate."); return; } @@ -1071,24 +1379,29 @@ public void prepare() throws SailException { return; } + List shapesToValidate = shapesAfterRefresh != null ? shapesAfterRefresh : currentShapes; + validateLegacyCallbackInferredSupport(shapesToValidate); + boolean requiresRdfsSubClassReasoner = sail.isRdfsSubClassReasoning() + || requiresRdfsSubClassReasoner(shapesToValidate); + stats.setEmptyIncludingCurrentTransaction(ConnectionHelper.isEmpty(this)); prepareValidation( - new ValidationSettings(null, sail.isLogValidationPlans(), false, sail.isPerformanceLogging())); + new ValidationSettings(null, sail.isLogValidationPlans(), false, sail.isPerformanceLogging()), + requiresRdfsSubClassReasoner); ValidationReport invalidTuples = null; if (useSerializableValidation) { synchronized (sail.singleConnectionMonitor) { if (!sail.usesSingleConnection()) { - invalidTuples = serializableValidation( - shapesAfterRefresh != null ? shapesAfterRefresh : currentShapes); + invalidTuples = serializableValidation(shapesToValidate); } } } if (invalidTuples == null) { invalidTuples = validate( - shapesAfterRefresh != null ? shapesAfterRefresh : currentShapes, + shapesToValidate, shapesModifiedInCurrentTransaction || isBulkValidation()); } @@ -1152,10 +1465,8 @@ private boolean isBulkValidation() { private ValidationReport serializableValidation(List shapesAfterRefresh) throws InterruptedException { try { - try (ConnectionsGroup connectionsGroup = new ConnectionsGroup( - new VerySimpleRdfsBackwardsChainingConnection(serializableConnection, rdfsSubClassOfReasoner), null, - addedStatements, removedStatements, stats, this::getRdfsSubClassOfReasoner, transactionSettings, - sail.sparqlValidation)) { + try (ConnectionsGroup connectionsGroup = getConnectionsGroup(serializableConnection, null, + sail.isIncludeInferredStatements(), sail.isRdfsSubClassReasoning())) { connectionsGroup.getBaseConnection().begin(IsolationLevels.SNAPSHOT); // actually force a transaction to start @@ -1176,7 +1487,7 @@ private ValidationReport serializableValidation(List shapesAft serializableConnection.flush(); return performValidation(shapesAfterRefresh, shapesModifiedInCurrentTransaction || isBulkValidation(), - connectionsGroup); + connectionsGroup, serializableConnection, null); } finally { serializableConnection.rollback(); @@ -1190,13 +1501,26 @@ private ValidationReport serializableValidation(List shapesAft @Override public void statementAdded(Statement statement) { + legacyStatementAddedWithoutInferredFlagObserved = true; + statementAdded(statement, false); + } + + @Override + public void statementAdded(Statement statement, boolean inferred) { if (prepareHasBeenCalled) { throw new IllegalStateException("Detected changes after prepare() has been called."); } checkIfShapesRefreshIsNeeded(statement); - boolean add = addedStatementsSet.add(statement); - if (!add) { - removedStatementsSet.remove(statement); + if (inferred) { + boolean add = addedStatementsInferredSet.add(statement); + if (!add) { + removedStatementsInferredSet.remove(statement); + } + } else { + boolean add = addedStatementsSet.add(statement); + if (!add) { + removedStatementsSet.remove(statement); + } } checkTransactionalValidationLimit(); @@ -1205,14 +1529,27 @@ public void statementAdded(Statement statement) { @Override public void statementRemoved(Statement statement) { + legacyStatementRemovedWithoutInferredFlagObserved = true; + statementRemoved(statement, false); + } + + @Override + public void statementRemoved(Statement statement, boolean inferred) { if (prepareHasBeenCalled) { throw new IllegalStateException("Detected changes after prepare() has been called."); } checkIfShapesRefreshIsNeeded(statement); - boolean add = removedStatementsSet.add(statement); - if (!add) { - addedStatementsSet.remove(statement); + if (inferred) { + boolean add = removedStatementsInferredSet.add(statement); + if (!add) { + addedStatementsInferredSet.remove(statement); + } + } else { + boolean add = removedStatementsSet.add(statement); + if (!add) { + addedStatementsSet.remove(statement); + } } checkTransactionalValidationLimit(); @@ -1223,15 +1560,96 @@ private void checkIfShapesRefreshIsNeeded(Statement statement) { if (!shapeRefreshNeeded) { for (IRI shapesGraph : shapesGraphs) { if (Objects.equals(statement.getContext(), shapesGraph)) { - shapeRefreshNeeded = true; + markShapesRefreshNeeded(); break; } } } } + private void markShapesRefreshNeeded() { + shapeRefreshNeeded = true; + } + + private Boolean inferInferredFromStatementMetadata(Statement statement) { + try { + Boolean inferred = invokeBooleanStatementMethod(statement, "isInferred"); + if (inferred != null) { + return inferred; + } + Boolean explicit = invokeBooleanStatementMethod(statement, "isExplicit"); + if (explicit != null) { + return !explicit; + } + return null; + } catch (ReflectiveOperationException e) { + if (logger.isDebugEnabled()) { + logger.debug("Unable to infer inferred-flag from legacy callback statement metadata.", e); + } + return null; + } + } + + private Boolean invokeBooleanStatementMethod(Statement statement, String methodName) + throws ReflectiveOperationException { + var method = statement.getClass().getMethod(methodName); + Class returnType = method.getReturnType(); + if (returnType != boolean.class && returnType != Boolean.class) { + return null; + } + Object value = method.invoke(statement); + return value == null ? null : (Boolean) value; + } + + /** + * Reject legacy no-flag callbacks whenever effective includeInferredStatements=false is active for at least one + * validated shape (globally through ShaclSail#setIncludeInferredStatements(false) and/or via + * rsx:includeInferredStatements=false). In that case, inferred-vs-explicit classification is required for correct + * validation and stores must use statementAdded/Removed callbacks with the inferred boolean argument. + */ + private void validateLegacyCallbackInferredSupport(List shapesToValidate) { + if (!legacyStatementAddedWithoutInferredFlagObserved && !legacyStatementRemovedWithoutInferredFlagObserved) { + return; + } + + boolean includeInferredStatementsEnabledByDefault = sail.isIncludeInferredStatements(); + boolean requiresInferredClassification = shapesToValidate.stream() + .filter(ContextWithShape::hasShape) + .map(ContextWithShape::getShape) + .anyMatch(shape -> !shape.usesIncludeInferredStatements(includeInferredStatementsEnabledByDefault)); + + if (!requiresInferredClassification) { + return; + } + + String callbackDetails = getObservedLegacyCallbacksWithoutInferredFlag(); + String message = "Underlying Sail does not support shapes that explicitly set " + + "rsx:includeInferredStatements=false or globally configure " + + "ShaclSail#setIncludeInferredStatements(false), because it emits deprecated " + + "SailConnectionListener callbacks without inferred flags (" + callbackDetails + "). " + + "Implement statementAdded(Statement, boolean inferred) and " + + "statementRemoved(Statement, boolean inferred)."; + logger.error(message); + throw new ShaclSailValidationException(message); + } + + private String getObservedLegacyCallbacksWithoutInferredFlag() { + if (legacyStatementAddedWithoutInferredFlagObserved && legacyStatementRemovedWithoutInferredFlagObserved) { + return "statementAdded(Statement), statementRemoved(Statement)"; + } + if (legacyStatementAddedWithoutInferredFlagObserved) { + return "statementAdded(Statement)"; + } + if (legacyStatementRemovedWithoutInferredFlagObserved) { + return "statementRemoved(Statement)"; + } + return ""; + } + private void checkTransactionalValidationLimit() { - if ((addedStatementsSet.size() + removedStatementsSet.size()) > sail.getTransactionalValidationLimit()) { + int changeCount = addedStatementsSet.size() + removedStatementsSet.size() + + addedStatementsInferredSet.size() + removedStatementsInferredSet.size(); + if (changeCount > sail.getTransactionalValidationLimit()) { if (shouldUseSerializableValidation()) { logger.debug( "Transaction size limit exceeded, could not switch to bulk validation because serializable validation is enabled."); @@ -1243,6 +1661,8 @@ private void checkTransactionalValidationLimit() { getTransactionSettings().applyTransactionSettings(bulkValidation); removedStatementsSet.clear(); addedStatementsSet.clear(); + removedStatementsInferredSet.clear(); + addedStatementsInferredSet.clear(); } } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationException.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationException.java index abee864a097..e6b4e29bae9 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationException.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclSailValidationException.java @@ -32,6 +32,11 @@ public class ShaclSailValidationException extends SailException implements Valid this.validationReport = validationReport; } + ShaclSailValidationException(String message) { + super(message); + this.validationReport = new ValidationReport(false); + } + /** * @return A Model containing the validation report as specified by the SHACL Recommendation */ diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java index ac23b2b8871..2e741ba7e79 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShaclValidator.java @@ -156,6 +156,7 @@ public static Builder settingsFrom(ShaclSail shaclSail) { builder.setGlobalLogValidationExecution(shaclSail.isGlobalLogValidationExecution()); builder.setCacheSelectNodes(shaclSail.isCacheSelectNodes()); builder.setRdfsSubClassReasoning(shaclSail.isRdfsSubClassReasoning()); + builder.setIncludeInferredStatements(shaclSail.isIncludeInferredStatements()); builder.setSerializableValidation(shaclSail.isSerializableValidation()); builder.setPerformanceLogging(shaclSail.isPerformanceLogging()); builder.setEclipseRdf4jShaclExtensions(shaclSail.isEclipseRdf4jShaclExtensions()); @@ -384,6 +385,7 @@ static class InternalBuilder> implements Cloneable private boolean cacheSelectNodes = true; private boolean globalLogValidationExecution = false; private boolean rdfsSubClassReasoning = true; + private boolean includeInferredStatements = true; private boolean performanceLogging = false; private boolean serializableValidation = false; private boolean eclipseRdf4jShaclExtensions = true; @@ -404,6 +406,7 @@ void setAll(InternalBuilder other) { this.cacheSelectNodes = other.cacheSelectNodes; this.globalLogValidationExecution = other.globalLogValidationExecution; this.rdfsSubClassReasoning = other.rdfsSubClassReasoning; + this.includeInferredStatements = other.includeInferredStatements; this.performanceLogging = other.performanceLogging; this.serializableValidation = other.serializableValidation; this.eclipseRdf4jShaclExtensions = other.eclipseRdf4jShaclExtensions; @@ -481,6 +484,17 @@ public T setRdfsSubClassReasoning(boolean rdfsSubClassReasoning) { return (T) this; } + /** + * Enable or disable inclusion of inferred statements during validation. + * + * @param includeInferredStatements whether to include inferred statements + * @return this builder instance + */ + public T setIncludeInferredStatements(boolean includeInferredStatements) { + this.includeInferredStatements = includeInferredStatements; + return (T) this; + } + /** * Disable SHACL validation entirely. * @@ -1543,11 +1557,11 @@ private static ValidationReport validateInternalWithoutTimeout(Sail dataRepo, Sa try (SailConnection dataRepoConnection = dataRepo.getConnection()) { dataRepoConnection.begin(IsolationLevels.NONE); try { - RdfsSubClassOfReasoner reasoner; - SailConnection baseConnection = dataRepoConnection; + boolean requiresReasoner = settings.rdfsSubClassReasoning || requiresRdfsSubClassReasoner(shapes); + RdfsSubClassOfReasoner reasoner = null; ConnectionsGroup.RdfsSubClassOfReasonerProvider rdfsSubClassOfReasonerProvider = null; - if (settings.rdfsSubClassReasoning) { + if (requiresReasoner) { try (SailConnection shapesConnection = shapesRepo.getConnection()) { shapesConnection.begin(IsolationLevels.NONE); try { @@ -1561,9 +1575,11 @@ private static ValidationReport validateInternalWithoutTimeout(Sail dataRepo, Sa RdfsSubClassOfReasoner finalReasoner = reasoner; rdfsSubClassOfReasonerProvider = () -> finalReasoner; - baseConnection = new VerySimpleRdfsBackwardsChainingConnection(dataRepoConnection, finalReasoner); } + SailConnection baseConnection = new VerySimpleRdfsBackwardsChainingConnection(dataRepoConnection, + reasoner, settings.includeInferredStatements); + ShaclSailConnection.Settings transactionSettings = new ShaclSailConnection.Settings( settings.cacheSelectNodes, settings.validationEnabled, @@ -1577,9 +1593,10 @@ private static ValidationReport validateInternalWithoutTimeout(Sail dataRepo, Sa null, new Stats(), rdfsSubClassOfReasonerProvider, + settings.includeInferredStatements, transactionSettings, settings.sparqlValidation)) { - return performValidation(shapes, connectionsGroup, settings); + return performValidation(shapes, connectionsGroup, settings, dataRepoConnection, reasoner); } } finally { dataRepoConnection.commit(); @@ -1634,29 +1651,71 @@ private static List readShapes(Sail shapesRepo, InternalBuilde } } + private static boolean requiresRdfsSubClassReasoner(List shapes) { + return shapes.stream() + .map(ContextWithShape::getShape) + .map(Shape::getRdfsSubClassReasoningOverride) + .anyMatch(Boolean.TRUE::equals); + } + + private static ConnectionsGroup createConnectionsGroup(SailConnection baseConnection, + RdfsSubClassOfReasoner reasoner, + boolean includeInferredStatements, boolean useRdfsSubClassReasoning, ConnectionsGroup defaults) { + RdfsSubClassOfReasoner effectiveReasoner = useRdfsSubClassReasoning ? reasoner : null; + ConnectionsGroup.RdfsSubClassOfReasonerProvider provider = effectiveReasoner == null + ? null + : () -> effectiveReasoner; + + SailConnection wrappedConnection = new VerySimpleRdfsBackwardsChainingConnection(baseConnection, + effectiveReasoner, + includeInferredStatements); + + return new ConnectionsGroup(wrappedConnection, null, null, null, defaults.getStats(), provider, + includeInferredStatements, defaults.getTransactionSettings(), + defaults.isSparqlValidation()); + } + private static ValidationReport performValidation(List shapes, ConnectionsGroup connectionsGroup, - InternalBuilder settings) { + InternalBuilder settings, SailConnection baseConnection, RdfsSubClassOfReasoner reasoner) { long effectiveValidationResultsLimitPerConstraint = settings.getEffectiveValidationResultsLimitPerConstraint(); long validationResultsLimitTotal = settings.validationResultsLimitTotal; + boolean defaultIncludeInferredStatements = settings.includeInferredStatements; + boolean defaultRdfsSubClassReasoning = settings.rdfsSubClassReasoning; List validationResultIterators = shapes .stream() - .map(contextWithShape -> new ShapeValidationContainer( - contextWithShape.getShape(), - () -> contextWithShape.getShape() - .generatePlans(connectionsGroup, - new ValidationSettings(contextWithShape.getDataGraph(), - settings.logValidationPlans, - true, settings.performanceLogging)), - settings.globalLogValidationExecution, - settings.logValidationViolations, - effectiveValidationResultsLimitPerConstraint, - settings.performanceLogging, - settings.logValidationPlans, - logger, - connectionsGroup) - ) + .map(contextWithShape -> { + Shape shape = contextWithShape.getShape(); + boolean shapeRdfsSubClassReasoning = shape.usesRdfsSubClassReasoning(defaultRdfsSubClassReasoning); + boolean shapeIncludeInferredStatements = shape + .usesIncludeInferredStatements(defaultIncludeInferredStatements); + + boolean closeConnectionsGroup = false; + ConnectionsGroup shapeConnectionsGroup = connectionsGroup; + if (shapeRdfsSubClassReasoning != defaultRdfsSubClassReasoning + || shapeIncludeInferredStatements != defaultIncludeInferredStatements) { + shapeConnectionsGroup = createConnectionsGroup(baseConnection, reasoner, + shapeIncludeInferredStatements, shapeRdfsSubClassReasoning, connectionsGroup); + closeConnectionsGroup = true; + } + ConnectionsGroup planConnectionsGroup = shapeConnectionsGroup; + + return new ShapeValidationContainer( + shape, + () -> shape.generatePlans(planConnectionsGroup, + new ValidationSettings(contextWithShape.getDataGraph(), + settings.logValidationPlans, + true, settings.performanceLogging)), + settings.globalLogValidationExecution, + settings.logValidationViolations, + effectiveValidationResultsLimitPerConstraint, + settings.performanceLogging, + settings.logValidationPlans, + logger, + shapeConnectionsGroup, + closeConnectionsGroup); + }) .filter(ShapeValidationContainer::hasPlanNode) .map(ShapeValidationContainer::performValidation) .collect(Collectors.toList()); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShapeValidationContainer.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShapeValidationContainer.java index 91e3b856a0e..7110b7ddea7 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShapeValidationContainer.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ShapeValidationContainer.java @@ -36,16 +36,22 @@ class ShapeValidationContainer { private final long effectiveValidationResultsLimitPerConstraint; private final boolean performanceLogging; private final Logger logger; + private final ConnectionsGroup connectionsGroup; + private final boolean closeConnectionsGroup; private volatile CloseableIteration iterator; + private volatile boolean connectionsGroupClosed; public ShapeValidationContainer(Shape shape, Supplier planNodeSupplier, boolean logValidationExecution, boolean logValidationViolations, long effectiveValidationResultsLimitPerConstraint, - boolean performanceLogging, boolean logValidationPlans, Logger logger, ConnectionsGroup connectionsGroup) { + boolean performanceLogging, boolean logValidationPlans, Logger logger, ConnectionsGroup connectionsGroup, + boolean closeConnectionsGroup) { this.shape = shape; this.logValidationViolations = logValidationViolations; this.effectiveValidationResultsLimitPerConstraint = effectiveValidationResultsLimitPerConstraint; this.performanceLogging = performanceLogging; this.logger = logger; + this.connectionsGroup = connectionsGroup; + this.closeConnectionsGroup = closeConnectionsGroup; try { PlanNode planNode = planNodeSupplier.get(); @@ -108,6 +114,7 @@ public ShapeValidationContainer(Shape shape, Supplier planNodeSupplier this.planNode = planNode; } } catch (Throwable e) { + closeConnectionsGroup("after plan construction failure"); logger.warn("Error processing SHACL Shape {}", shape.getId(), e); logger.warn("Error processing SHACL Shape\n{}", shape, e); if (e instanceof Error) { @@ -123,7 +130,11 @@ public Shape getShape() { } public boolean hasPlanNode() { - return !(planNode.isGuaranteedEmpty()); + boolean hasPlanNode = !(planNode.isGuaranteedEmpty()); + if (!hasPlanNode) { + closeConnectionsGroup("for guaranteed-empty plan"); + } + return hasPlanNode; } public ValidationResultIterator performValidation() throws SailException { @@ -154,10 +165,28 @@ public ValidationResultIterator performValidation() throws SailException { } } finally { this.iterator = null; + closeConnectionsGroup("after validation"); } } + private void closeConnectionsGroup(String context) { + if (!closeConnectionsGroup || connectionsGroupClosed) { + return; + } + synchronized (this) { + if (connectionsGroupClosed) { + return; + } + connectionsGroupClosed = true; + } + try { + connectionsGroup.close(); + } catch (Exception closeException) { + logger.debug("Error closing connections group {}", context, closeException); + } + } + private long getTimeStamp() { if (performanceLogging) { return System.currentTimeMillis(); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclProperties.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclProperties.java index fbdb55106ee..a2810f33e43 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclProperties.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ShaclProperties.java @@ -89,6 +89,8 @@ public class ShaclProperties { private Boolean deactivated = null; private Boolean uniqueLang = null; + private Boolean rdfsSubClassReasoning = null; + private Boolean includeInferredStatements = null; private Boolean closed = null; private Resource ignoredProperties; @@ -411,6 +413,30 @@ public ShaclProperties(Resource id, ShapeSource connection) { throw getExceptionForLiteralFormatIssue(id, predicate, object, Boolean.class); } break; + case "http://rdf4j.org/shacl-extensions#rdfsSubClassReasoning": + if (rdfsSubClassReasoning != null) { + throw getExceptionForAlreadyPopulated(id, predicate, rdfsSubClassReasoning, object); + } + try { + rdfsSubClassReasoning = ((Literal) object).booleanValue(); + } catch (ClassCastException e) { + throw getExceptionForCastIssue(id, predicate, Literal.class, object); + } catch (IllegalArgumentException e) { + throw getExceptionForLiteralFormatIssue(id, predicate, object, Boolean.class); + } + break; + case "http://rdf4j.org/shacl-extensions#includeInferredStatements": + if (includeInferredStatements != null) { + throw getExceptionForAlreadyPopulated(id, predicate, includeInferredStatements, object); + } + try { + includeInferredStatements = ((Literal) object).booleanValue(); + } catch (ClassCastException e) { + throw getExceptionForCastIssue(id, predicate, Literal.class, object); + } catch (IllegalArgumentException e) { + throw getExceptionForLiteralFormatIssue(id, predicate, object, Boolean.class); + } + break; case "http://www.w3.org/ns/shacl#closed": if (closed != null) { throw getExceptionForAlreadyPopulated(id, predicate, closed, object); @@ -751,6 +777,14 @@ public boolean isUniqueLang() { return uniqueLang != null && uniqueLang; } + public Boolean getRdfsSubClassReasoning() { + return rdfsSubClassReasoning; + } + + public Boolean getIncludeInferredStatements() { + return includeInferredStatements; + } + public Resource getId() { return id; } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java index 45702900b82..e9a32d79228 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/Shape.java @@ -107,6 +107,8 @@ abstract public class Shape implements ConstraintComponent, Identifiable { List target = new ArrayList<>(); boolean deactivated; + private Boolean rdfsSubClassReasoningOverride; + private Boolean includeInferredStatementsOverride; List message; Severity severity; @@ -119,6 +121,8 @@ public Shape() { public Shape(Shape shape) { this.deactivated = shape.deactivated; + this.rdfsSubClassReasoningOverride = shape.rdfsSubClassReasoningOverride; + this.includeInferredStatementsOverride = shape.includeInferredStatementsOverride; this.message = shape.message; this.severity = shape.severity; this.id = shape.id; @@ -135,6 +139,11 @@ public void populate(ShaclProperties properties, ShapeSource shapeSource, ParseS this.contexts = shapeSource.getActiveContexts(); this.severity = Severity.fromIri(properties.getSeverity()); + if (parseSettings.parseEclipseRdf4jShaclExtensions()) { + this.rdfsSubClassReasoningOverride = properties.getRdfsSubClassReasoning(); + this.includeInferredStatementsOverride = properties.getIncludeInferredStatements(); + } + if (!properties.getTargetClass().isEmpty()) { target.add(new TargetClass(properties.getTargetClass())); } @@ -218,6 +227,12 @@ public void toModel(Resource subject, IRI predicate, Model model, Set if (severity != null) { modelBuilder.add(SHACL.SEVERITY_PROP, severity.getIri()); } + if (rdfsSubClassReasoningOverride != null) { + modelBuilder.add(RSX.rdfsSubClassReasoning, rdfsSubClassReasoningOverride); + } + if (includeInferredStatementsOverride != null) { + modelBuilder.add(RSX.includeInferredStatements, includeInferredStatementsOverride); + } model.addAll(modelBuilder.build()); } @@ -424,7 +439,7 @@ public PlanNode generatePlans(ConnectionsGroup connectionsGroup, ValidationSetti .generateSparqlValidationQuery(connectionsGroup, validationSettings, false, false, Scope.none) .getValidationPlan(connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), - getContexts()), + getContexts(), connectionsGroup.isIncludeInferredStatements()), this, connectionsGroup); } else { logger.debug("Use fall back validation approach for bulk validation instead of SPARQL for shape {}", @@ -479,6 +494,22 @@ public boolean isDeactivated() { return deactivated; } + public Boolean getRdfsSubClassReasoningOverride() { + return rdfsSubClassReasoningOverride; + } + + public Boolean getIncludeInferredStatementsOverride() { + return includeInferredStatementsOverride; + } + + public boolean usesRdfsSubClassReasoning(boolean defaultValue) { + return rdfsSubClassReasoningOverride == null ? defaultValue : rdfsSubClassReasoningOverride; + } + + public boolean usesIncludeInferredStatements(boolean defaultValue) { + return includeInferredStatementsOverride == null ? defaultValue : includeInferredStatementsOverride; + } + @Override public boolean requiresEvaluation(ConnectionsGroup connectionsGroup, Scope scope, Resource[] dataGraph, StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider) { @@ -574,6 +605,12 @@ public boolean equals(ConstraintComponent o, IdentityHashMap compa if (severity != shape.severity) { return false; } + if (!Objects.equals(rdfsSubClassReasoningOverride, shape.rdfsSubClassReasoningOverride)) { + return false; + } + if (!Objects.equals(includeInferredStatementsOverride, shape.includeInferredStatementsOverride)) { + return false; + } if (constraintComponents.size() != shape.constraintComponents.size()) { return false; @@ -637,6 +674,9 @@ public int hashCode(IdentityHashMap cycleDetection) { result = 31 * result + (deactivated ? 1 : 0); result = 31 * result + (message != null ? message.hashCode() : 0); result = 31 * result + (severity != null ? severity.hashCode() : 0); + result = 31 * result + (rdfsSubClassReasoningOverride != null ? rdfsSubClassReasoningOverride.hashCode() : 0); + result = 31 * result + + (includeInferredStatementsOverride != null ? includeInferredStatementsOverride.hashCode() : 0); long temp = 0; diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ValidationQuery.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ValidationQuery.java index 959b9b04c4b..79244478f17 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ValidationQuery.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/ValidationQuery.java @@ -166,7 +166,7 @@ public String getQuery() { } public PlanNode getValidationPlan(SailConnection baseConnection, Resource[] dataGraph, - Resource[] shapesGraphs) { + Resource[] shapesGraphs, boolean includeInferredStatements) { assert query != null; assert shape != null; @@ -198,7 +198,7 @@ public PlanNode getValidationPlan(SailConnection baseConnection, Resource[] data return validationTuple.addValidationResult(validationResultFunction); - }, dataGraph); + }, dataGraph, includeInferredStatements); return select; @@ -339,7 +339,7 @@ public static Deactivated getInstance() { @Override public PlanNode getValidationPlan(SailConnection baseConnection, Resource[] dataGraph, - Resource[] shapesGraphs) { + Resource[] shapesGraphs, boolean includeInferredStatements) { return EmptyNode.getInstance(); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java index b6de74b1f30..ce688b8c3c1 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractConstraintComponent.java @@ -125,6 +125,9 @@ static CharSequence[] trim(String... s) { } public String stringRepresentationOfValue(Value value) { + if (value == null) { + return "null"; + } if (value.isIRI()) { return "<" + value + ">"; } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java index eb43ec44531..c7915981e77 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/AbstractPairwiseConstraintComponent.java @@ -144,12 +144,12 @@ private PlanNode getAllTargetsBasedOnPredicate(ConnectionsGroup connectionsGroup PlanNode addedByPredicate = new UnorderedSelect(connectionsGroup.getAddedStatements(), null, predicate, null, validationSettings.getDataGraph(), (s, d) -> { return new ValidationTuple(s.getSubject(), Scope.propertyShape, false, d); - }, null); + }, null, connectionsGroup.isIncludeInferredStatements()); PlanNode removedByPredicate = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, predicate, null, validationSettings.getDataGraph(), (s, d) -> { return new ValidationTuple(s.getSubject(), Scope.propertyShape, false, d); - }, null); + }, null, connectionsGroup.isIncludeInferredStatements()); PlanNode targetFilter1 = effectiveTarget.getTargetFilter(connectionsGroup, validationSettings.getDataGraph(), addedByPredicate); @@ -176,7 +176,8 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] if (connectionsGroup.getStats().hasRemoved()) { PlanNode deletedPredicates = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, predicate, null, dataGraph, - UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.propertyShape), null); + UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.propertyShape), null, + connectionsGroup.isIncludeInferredStatements()); deletedPredicates = getTargetChain() .getEffectiveTarget(Scope.propertyShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -195,7 +196,7 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] if (connectionsGroup.getStats().hasAdded()) { PlanNode addedPredicates = new UnorderedSelect(connectionsGroup.getAddedStatements(), null, predicate, null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.propertyShape), - null); + null, connectionsGroup.isIncludeInferredStatements()); addedPredicates = getTargetChain() .getEffectiveTarget(Scope.propertyShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -220,7 +221,8 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] if (connectionsGroup.getStats().hasRemoved()) { PlanNode deletedPredicates = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, predicate, null, - dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null); + dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null, + connectionsGroup.isIncludeInferredStatements()); deletedPredicates = getTargetChain() .getEffectiveTarget(Scope.nodeShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -239,7 +241,8 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] if (connectionsGroup.getStats().hasAdded()) { PlanNode addedPredicates = new UnorderedSelect(connectionsGroup.getAddedStatements(), null, predicate, null, - dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null); + dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null, + connectionsGroup.isIncludeInferredStatements()); addedPredicates = getTargetChain() .getEffectiveTarget(Scope.nodeShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -264,8 +267,12 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] public boolean requiresEvaluation(ConnectionsGroup connectionsGroup, Scope scope, Resource[] dataGraph, StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider) { return super.requiresEvaluation(connectionsGroup, scope, dataGraph, stableRandomVariableProvider) - || connectionsGroup.getRemovedStatements().hasStatement(null, predicate, null, true, dataGraph) - || connectionsGroup.getAddedStatements().hasStatement(null, predicate, null, true, dataGraph); + || connectionsGroup.getRemovedStatements() + .hasStatement(null, predicate, null, + connectionsGroup.isIncludeInferredStatements(), dataGraph) + || connectionsGroup.getAddedStatements() + .hasStatement(null, predicate, null, + connectionsGroup.isIncludeInferredStatements(), dataGraph); } @Override diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java index a2aee5506cb..428effaeba0 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClassConstraintComponent.java @@ -103,7 +103,8 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections allTargets = new FilterByPredicateObject( connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), RDF.TYPE, clazzSet, - allTargets, false, FilterByPredicateObject.FilterOn.value, true, connectionsGroup); + allTargets, false, FilterByPredicateObject.FilterOn.value, + connectionsGroup.isIncludeInferredStatements(), connectionsGroup); return allTargets; @@ -136,7 +137,8 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections if (connectionsGroup.getStats().hasRemoved()) { PlanNode deletedTypes = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, RDF.TYPE, clazz, validationSettings.getDataGraph(), - UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null); + UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null, + connectionsGroup.isIncludeInferredStatements()); deletedTypes = getTargetChain() .getEffectiveTarget(Scope.nodeShape, @@ -181,14 +183,16 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections falseNode = new FilterByPredicateObject( connectionsGroup.getAddedStatements(), validationSettings.getDataGraph(), RDF.TYPE, clazzSet, - falseNode, false, FilterByPredicateObject.FilterOn.value, false, connectionsGroup); + falseNode, false, FilterByPredicateObject.FilterOn.value, + connectionsGroup.isIncludeInferredStatements(), connectionsGroup); } // filter by type against the base sail falseNode = new FilterByPredicateObject( connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), RDF.TYPE, clazzSet, - falseNode, false, FilterByPredicateObject.FilterOn.value, true, connectionsGroup); + falseNode, false, FilterByPredicateObject.FilterOn.value, + connectionsGroup.isIncludeInferredStatements(), connectionsGroup); return falseNode; @@ -209,7 +213,8 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections if (connectionsGroup.getStats().hasRemoved()) { PlanNode deletedTypes = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, RDF.TYPE, clazz, validationSettings.getDataGraph(), - UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), null); + UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), null, + connectionsGroup.isIncludeInferredStatements()); deletedTypes = getTargetChain() .getEffectiveTarget(scope, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -227,7 +232,8 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections PlanNode falseNode = new FilterByPredicateObject( connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), RDF.TYPE, clazzSet, - addedTargets, false, FilterByPredicateObject.FilterOn.value, true, connectionsGroup); + addedTargets, false, FilterByPredicateObject.FilterOn.value, + connectionsGroup.isIncludeInferredStatements(), connectionsGroup); return falseNode; @@ -251,7 +257,7 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] if (connectionsGroup.getStats().hasRemoved()) { PlanNode deletedTypes = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, RDF.TYPE, clazz, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), - null); + null, connectionsGroup.isIncludeInferredStatements()); deletedTypes = getTargetChain() .getEffectiveTarget(Scope.nodeShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -269,7 +275,7 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] if (connectionsGroup.getStats().hasAdded()) { PlanNode addedTypes = new UnorderedSelect(connectionsGroup.getAddedStatements(), null, RDF.TYPE, clazz, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), - null); + null, connectionsGroup.isIncludeInferredStatements()); addedTypes = getTargetChain() .getEffectiveTarget(Scope.nodeShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -292,7 +298,8 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] // removed type statements that match clazz could affect sh:or if (connectionsGroup.getStats().hasRemoved()) { PlanNode deletedTypes = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, RDF.TYPE, clazz, - dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null); + dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null, + connectionsGroup.isIncludeInferredStatements()); deletedTypes = getTargetChain() .getEffectiveTarget(Scope.nodeShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -309,7 +316,8 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] // added type statements that match clazz could affect sh:not if (connectionsGroup.getStats().hasAdded()) { PlanNode addedTypes = new UnorderedSelect(connectionsGroup.getAddedStatements(), null, RDF.TYPE, clazz, - dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null); + dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(Scope.nodeShape), null, + connectionsGroup.isIncludeInferredStatements()); addedTypes = getTargetChain() .getEffectiveTarget(Scope.nodeShape, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider) @@ -330,8 +338,12 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] public boolean requiresEvaluation(ConnectionsGroup connectionsGroup, Scope scope, Resource[] dataGraph, StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider) { return super.requiresEvaluation(connectionsGroup, scope, dataGraph, stableRandomVariableProvider) - || connectionsGroup.getRemovedStatements().hasStatement(null, RDF.TYPE, clazz, true, dataGraph) - || connectionsGroup.getAddedStatements().hasStatement(null, RDF.TYPE, clazz, true, dataGraph); + || connectionsGroup.getRemovedStatements() + .hasStatement(null, RDF.TYPE, clazz, + connectionsGroup.isIncludeInferredStatements(), dataGraph) + || connectionsGroup.getAddedStatements() + .hasStatement(null, RDF.TYPE, clazz, + connectionsGroup.isIncludeInferredStatements(), dataGraph); } @Override diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java index 025e4fe021f..5b993f92bd5 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/ClosedConstraintComponent.java @@ -163,7 +163,7 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections null, validationSettings.getDataGraph(), UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), (statement -> { return !allAllowedPredicates.contains(statement.getPredicate()); - })); + }), connectionsGroup.isIncludeInferredStatements()); addedByValue = getTargetChain() .getEffectiveTarget(Scope.nodeShape, @@ -259,7 +259,7 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections null, validationSettings.getDataGraph(), UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), (statement -> { return !allAllowedPredicates.contains(statement.getPredicate()); - })); + }), connectionsGroup.isIncludeInferredStatements()); // then remove any that are in the addedTargets node PlanNode notValuesIn = new ReduceTargets(unorderedSelect, addedTargets, connectionsGroup); @@ -350,7 +350,8 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] null, null, null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), - (statement -> !allAllowedPredicates.contains(statement.getPredicate()))); + (statement -> !allAllowedPredicates.contains(statement.getPredicate())), + connectionsGroup.isIncludeInferredStatements()); // then remove any that are in the targets node statementsNotMatchingPredicateList = new ReduceTargets(statementsNotMatchingPredicateList, @@ -367,7 +368,8 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] PlanNode removed = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, null, null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), - (statement -> !allAllowedPredicates.contains(statement.getPredicate()))); + (statement -> !allAllowedPredicates.contains(statement.getPredicate())), + connectionsGroup.isIncludeInferredStatements()); removed = new ReduceTargets(removed, targets.getPlanNode(), connectionsGroup); @@ -413,13 +415,13 @@ public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), (statement -> { return !allAllowedPredicates.contains(statement.getPredicate()); - })); + }), connectionsGroup.isIncludeInferredStatements()); PlanNode removedByValue = new UnorderedSelect(connectionsGroup.getRemovedStatements(), null, null, null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), (statement -> { return !allAllowedPredicates.contains(statement.getPredicate()); - })); + }), connectionsGroup.isIncludeInferredStatements()); addedByValue = UnionNode.getInstance(connectionsGroup, addedByValue, removedByValue); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java index cf21ee80ed5..19ca6d2b0fb 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/DisjointConstraintComponent.java @@ -49,7 +49,7 @@ PlanNode getPairwiseCheck(ConnectionsGroup connectionsGroup, ValidationSettings SparqlFragment targetQueryFragment) { return new CheckDisjointValuesBasedOnPathAndPredicate(connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), allTargets, predicate, subject, object, targetQueryFragment, shape, - this, producesValidationReport); + this, producesValidationReport, connectionsGroup.isIncludeInferredStatements()); } @Override diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java index 43cb2b0e5f6..0eb57cfd10e 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/EqualsConstraintComponent.java @@ -49,7 +49,7 @@ CheckEqualsValuesBasedOnPathAndPredicate getPairwiseCheck(ConnectionsGroup conne StatementMatcher.Variable object, SparqlFragment targetQueryFragment) { return new CheckEqualsValuesBasedOnPathAndPredicate(connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), allTargets, predicate, subject, object, targetQueryFragment, shape, - this, producesValidationReport); + this, producesValidationReport, connectionsGroup.isIncludeInferredStatements()); } @Override diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java index 81bf4afb87f..70852b09ad0 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanConstraintComponent.java @@ -49,7 +49,7 @@ PlanNode getPairwiseCheck(ConnectionsGroup connectionsGroup, ValidationSettings SparqlFragment targetQueryFragment) { return new CheckLessThanValuesBasedOnPathAndPredicate(connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), allTargets, predicate, subject, object, targetQueryFragment, shape, - this, producesValidationReport); + this, producesValidationReport, connectionsGroup.isIncludeInferredStatements()); } @Override diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java index 7281218907e..67cbe95e557 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/LessThanOrEqualsConstraintComponent.java @@ -49,7 +49,7 @@ PlanNode getPairwiseCheck(ConnectionsGroup connectionsGroup, ValidationSettings SparqlFragment targetQueryFragment) { return new CheckLessThanOrEqualValuesBasedOnPathAndPredicate(connectionsGroup.getBaseConnection(), validationSettings.getDataGraph(), allTargets, predicate, subject, object, targetQueryFragment, shape, - this, producesValidationReport); + this, producesValidationReport, connectionsGroup.isIncludeInferredStatements()); } @Override diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java index 37bf477514e..669a133e385 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/constraintcomponents/SparqlConstraintComponent.java @@ -199,7 +199,8 @@ public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connections } return new SparqlConstraintSelect(connectionsGroup.getBaseConnection(), allTargets, select, scope, - validationSettings.getDataGraph(), produceValidationReports, this, shape); + validationSettings.getDataGraph(), produceValidationReports, this, shape, + connectionsGroup.isIncludeInferredStatements()); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/OneOrMorePath.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/OneOrMorePath.java index bab4e2c8e96..0fa6d1a7e0f 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/OneOrMorePath.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/OneOrMorePath.java @@ -173,7 +173,8 @@ public SparqlFragment getTargetQueryFragment(StatementMatcher.Variable subject, .getStatements()) { try (CloseableIteration evaluate = baseConnection.evaluate( tupleExpr, PlanNodeHelper.asDefaultGraphDataset(dataGraph), - new SingletonBindingSet(subjectName, statement.getSubject()), true)) { + new SingletonBindingSet(subjectName, statement.getSubject()), + connectionsGroup.isIncludeInferredStatements())) { while (evaluate.hasNext()) { BindingSet next = evaluate.next(); statements.add(new EffectiveTarget.SubjectObjectAndMatcher.SubjectObject( diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SequencePath.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SequencePath.java index ce6baae67fa..1dfb1c6b65c 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SequencePath.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SequencePath.java @@ -112,7 +112,7 @@ public PlanNode getAnyAdded(ConnectionsGroup connectionsGroup, Resource[] dataGr PlanNode unorderedSelect = new Select(connectionsGroup.getAddedStatements(), targetQueryFragment, null, new AllTargetsPlanNode.AllTargetsBindingSetMapper(List.of("subject", "value"), ConstraintComponent.Scope.propertyShape, true, dataGraph), - dataGraph); + dataGraph, connectionsGroup.isIncludeInferredStatements()); if (planNodeWrapper != null) { unorderedSelect = planNodeWrapper.apply(unorderedSelect); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SimplePath.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SimplePath.java index 271c6f50d95..67cff9844a6 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SimplePath.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/SimplePath.java @@ -49,7 +49,8 @@ public Resource getId() { public PlanNode getAllAdded(ConnectionsGroup connectionsGroup, Resource[] dataGraph, PlanNodeWrapper planNodeWrapper) { PlanNode unorderedSelect = new UnorderedSelect(connectionsGroup.getAddedStatements(), null, predicate, null, - dataGraph, UnorderedSelect.Mapper.SubjectObjectPropertyShapeMapper.getFunction(), null); + dataGraph, UnorderedSelect.Mapper.SubjectObjectPropertyShapeMapper.getFunction(), null, + connectionsGroup.isIncludeInferredStatements()); if (planNodeWrapper != null) { unorderedSelect = planNodeWrapper.apply(unorderedSelect); @@ -97,8 +98,8 @@ public SparqlFragment getTargetQueryFragment(StatementMatcher.Variable subject, .map(currentStatement -> { try (CloseableIteration statements = connectionsGroup .getBaseConnection() - .getStatements(null, predicate, currentStatement.getSubject(), true, - dataGraph)) { + .getStatements(null, predicate, currentStatement.getSubject(), + connectionsGroup.isIncludeInferredStatements(), dataGraph)) { return QueryResults.asList(statements); } }) @@ -112,8 +113,8 @@ public SparqlFragment getTargetQueryFragment(StatementMatcher.Variable subject, .map(currentStatement -> { try (CloseableIteration statements = connectionsGroup .getBaseConnection() - .getStatements(null, predicate, currentStatement.getObject(), true, - dataGraph)) { + .getStatements(null, predicate, currentStatement.getObject(), + connectionsGroup.isIncludeInferredStatements(), dataGraph)) { return QueryResults.asList(statements); } }) diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/ZeroOrMorePath.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/ZeroOrMorePath.java index f1973fca581..090fb853d4f 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/ZeroOrMorePath.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/paths/ZeroOrMorePath.java @@ -184,7 +184,8 @@ public SparqlFragment getTargetQueryFragment(StatementMatcher.Variable subject, .getStatements()) { try (CloseableIteration evaluate = baseConnection.evaluate( tupleExpr, PlanNodeHelper.asDefaultGraphDataset(dataGraph), - new SingletonBindingSet(subjectName, statement.getSubject()), true)) { + new SingletonBindingSet(subjectName, statement.getSubject()), + connectionsGroup.isIncludeInferredStatements())) { while (evaluate.hasNext()) { BindingSet next = evaluate.next(); statements.add(new EffectiveTarget.SubjectObjectAndMatcher.SubjectObject( diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractBulkJoinPlanNode.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractBulkJoinPlanNode.java index 6eefda26973..0f8c19e1aff 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractBulkJoinPlanNode.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractBulkJoinPlanNode.java @@ -41,12 +41,14 @@ public abstract class AbstractBulkJoinPlanNode implements PlanNode { protected static final int BULK_SIZE = 1000; private final List vars; private final String varsQueryString; + private final boolean includeInferredStatements; StackTraceElement[] stackTrace; protected Function mapper; ValidationExecutionLogger validationExecutionLogger; - public AbstractBulkJoinPlanNode(List vars) { + public AbstractBulkJoinPlanNode(List vars, boolean includeInferredStatements) { this.vars = vars; + this.includeInferredStatements = includeInferredStatements; this.varsQueryString = vars.stream() .map(StatementMatcher.Variable::asSparqlVariable) .reduce((a, b) -> a + " " + b) @@ -85,7 +87,7 @@ private void executeQuery(ArrayDeque right, SailConnection conn // System.out.println(stackTrace[3].getClassName()); try (Stream stream = connection - .evaluate(parsedQuery, dataset, EmptyBindingSet.getInstance(), true) + .evaluate(parsedQuery, dataset, EmptyBindingSet.getInstance(), includeInferredStatements) .stream()) { stream .map(mapper) @@ -133,13 +135,13 @@ private List buildBindingSets(Collection left, Sail if (!(tuple.getActiveTarget().isResource())) { hasStatement = previousStateConnection.hasStatement(null, null, tuple.getActiveTarget(), - true, dataGraph); + includeInferredStatements, dataGraph); } else { hasStatement = previousStateConnection.hasStatement(((Resource) tuple.getActiveTarget()), - null, null, true, dataGraph) || - previousStateConnection.hasStatement(null, null, tuple.getActiveTarget(), true, - dataGraph); + null, null, includeInferredStatements, dataGraph) || + previousStateConnection.hasStatement(null, null, tuple.getActiveTarget(), + includeInferredStatements, dataGraph); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractPairwisePlanNode.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractPairwisePlanNode.java index cfac96a61c2..5f50a5eb11a 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractPairwisePlanNode.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AbstractPairwisePlanNode.java @@ -56,13 +56,14 @@ abstract class AbstractPairwisePlanNode implements PlanNode { private final PlanNode parent; private final Shape shape; private final ConstraintComponent constraintComponent; + private final boolean includeInferredStatements; private ValidationExecutionLogger validationExecutionLogger; private boolean produceValidationReports; public AbstractPairwisePlanNode(SailConnection connection, Resource[] dataGraph, PlanNode parent, IRI predicate, StatementMatcher.Variable subject, StatementMatcher.Variable object, SparqlFragment targetQueryFragment, Shape shape, ConstraintComponent constraintComponent, - boolean produceValidationReports) { + boolean produceValidationReports, boolean includeInferredStatements) { this.parent = parent; this.connection = connection; assert this.connection != null; @@ -79,6 +80,7 @@ public AbstractPairwisePlanNode(SailConnection connection, Resource[] dataGraph, this.shape = shape; this.constraintComponent = constraintComponent; this.produceValidationReports = produceValidationReports; + this.includeInferredStatements = includeInferredStatements; } @@ -95,14 +97,16 @@ private Set getMismatchedValues(ValidationTuple t) { TupleExpr tupleExpr = SparqlQueryParserCache.get(query); try (Stream stream = connection - .evaluate(tupleExpr, dataset, new SingletonBindingSet(subject.getName(), target), true) + .evaluate(tupleExpr, dataset, new SingletonBindingSet(subject.getName(), target), + includeInferredStatements) .stream()) { valuesByPath = stream.map(bindingSet -> bindingSet.getValue(object.getName())) .collect(Collectors.toSet()); } } - try (Stream stream = connection.getStatements(target, predicate, null, false, dataGraph) + try (Stream stream = connection.getStatements(target, predicate, null, + includeInferredStatements, dataGraph) .stream()) { valuesByPredicate = stream.map(Statement::getObject).collect(Collectors.toSet()); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AllTargetsPlanNode.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AllTargetsPlanNode.java index a74d4dd6c2f..b2f77f55319 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AllTargetsPlanNode.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/AllTargetsPlanNode.java @@ -42,7 +42,8 @@ public class AllTargetsPlanNode implements PlanNode { public AllTargetsPlanNode(SailConnection sailConnection, Resource[] dataGraph, ArrayDeque chain, List> vars, - ConstraintComponent.Scope scope) { + ConstraintComponent.Scope scope, + boolean includeInferredStatements) { List sparqlFragments = chain.stream() .map(EffectiveTarget.EffectiveTargetFragment::getQueryFragment) @@ -53,7 +54,8 @@ public AllTargetsPlanNode(SailConnection sailConnection, List varNames = vars.stream().map(StatementMatcher.Variable::getName).collect(Collectors.toList()); this.select = new Select(sailConnection, sparqlFragment, null, - new AllTargetsBindingSetMapper(varNames, scope, false, dataGraph), dataGraph); + new AllTargetsBindingSetMapper(varNames, scope, false, dataGraph), dataGraph, + includeInferredStatements); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BindSelect.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BindSelect.java index 6f3a1ae607d..0fef3ee4228 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BindSelect.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BindSelect.java @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: BSD-3-Clause *******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl.ast.planNodes; @@ -58,6 +59,7 @@ public class BindSelect implements PlanNode { private final SailConnection connection; private final Dataset dataset; private final Function mapper; + private final boolean includeInferredStatements; private final String query; private final List> vars; @@ -96,6 +98,7 @@ public BindSelect(SailConnection connection, Resource[] dataGraph, SparqlFragmen this.includePropertyShapeValues = includePropertyShapeValues; dataset = PlanNodeHelper.asDefaultGraphDataset(dataGraph); + this.includeInferredStatements = connectionsGroup.isIncludeInferredStatements(); // this.stackTrace = Thread.currentThread().getStackTrace(); @@ -240,7 +243,7 @@ public void calculateNext() { updateQuery(parsedQuery, bindingSets, targetChainSize); bindingSet = connection.evaluate(parsedQuery, dataset, - EmptyBindingSet.getInstance(), true); + EmptyBindingSet.getInstance(), includeInferredStatements); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalInnerJoin.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalInnerJoin.java index 73f4f49ba16..5879f956385 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalInnerJoin.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalInnerJoin.java @@ -63,7 +63,7 @@ public BulkedExternalInnerJoin(PlanNode leftNode, SailConnection connection, Res boolean skipBasedOnPreviousConnection, SailConnection previousStateConnection, Function mapper, ConnectionsGroup connectionsGroup, List vars) { - super(vars); + super(vars, connectionsGroup.isIncludeInferredStatements()); assert !skipBasedOnPreviousConnection || previousStateConnection != null; this.leftNode = PlanNodeHelper.handleSorting(this, leftNode, connectionsGroup); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalLeftOuterJoin.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalLeftOuterJoin.java index e0890381d7f..d1b7715dbb9 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalLeftOuterJoin.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/BulkedExternalLeftOuterJoin.java @@ -48,7 +48,7 @@ public BulkedExternalLeftOuterJoin(PlanNode leftNode, SailConnection connection, SparqlFragment query, Function mapper, ConnectionsGroup connectionsGroup, List vars) { - super(vars); + super(vars, connectionsGroup.isIncludeInferredStatements()); leftNode = PlanNodeHelper.handleSorting(this, leftNode, connectionsGroup); this.leftNode = leftNode; this.query = query.getNamespacesForSparql() diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckDisjointValuesBasedOnPathAndPredicate.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckDisjointValuesBasedOnPathAndPredicate.java index 62e27698b31..3c969e53a6d 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckDisjointValuesBasedOnPathAndPredicate.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckDisjointValuesBasedOnPathAndPredicate.java @@ -29,9 +29,9 @@ public class CheckDisjointValuesBasedOnPathAndPredicate extends AbstractPairwise public CheckDisjointValuesBasedOnPathAndPredicate(SailConnection connection, Resource[] dataGraph, PlanNode parent, IRI predicate, StatementMatcher.Variable subject, StatementMatcher.Variable object, SparqlFragment targetQueryFragment, Shape shape, ConstraintComponent constraintComponent, - boolean produceValidationReports) { + boolean produceValidationReports, boolean includeInferredStatements) { super(connection, dataGraph, parent, predicate, subject, object, targetQueryFragment, shape, - constraintComponent, produceValidationReports); + constraintComponent, produceValidationReports, includeInferredStatements); } Set getInvalidValues(Set valuesByPath, Set valuesByPredicate) { diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckEqualsValuesBasedOnPathAndPredicate.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckEqualsValuesBasedOnPathAndPredicate.java index a950b42fcdf..dd1c80ccbfe 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckEqualsValuesBasedOnPathAndPredicate.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckEqualsValuesBasedOnPathAndPredicate.java @@ -36,9 +36,9 @@ public class CheckEqualsValuesBasedOnPathAndPredicate extends AbstractPairwisePl public CheckEqualsValuesBasedOnPathAndPredicate(SailConnection connection, Resource[] dataGraph, PlanNode parent, IRI predicate, StatementMatcher.Variable subject, StatementMatcher.Variable object, SparqlFragment targetQueryFragment, Shape shape, ConstraintComponent constraintComponent, - boolean produceValidationReports) { + boolean produceValidationReports, boolean includeInferredStatements) { super(connection, dataGraph, parent, predicate, subject, object, targetQueryFragment, shape, - constraintComponent, produceValidationReports); + constraintComponent, produceValidationReports, includeInferredStatements); } Set getInvalidValues(Set valuesByPath, Set valuesByPredicate) { diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanOrEqualValuesBasedOnPathAndPredicate.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanOrEqualValuesBasedOnPathAndPredicate.java index 78fff34aaac..d2efa5267d9 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanOrEqualValuesBasedOnPathAndPredicate.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanOrEqualValuesBasedOnPathAndPredicate.java @@ -30,9 +30,10 @@ public class CheckLessThanOrEqualValuesBasedOnPathAndPredicate extends AbstractP public CheckLessThanOrEqualValuesBasedOnPathAndPredicate(SailConnection connection, Resource[] dataGraph, PlanNode parent, IRI predicate, StatementMatcher.Variable subject, StatementMatcher.Variable object, SparqlFragment targetQueryFragment, Shape shape, - ConstraintComponent constraintComponent, boolean produceValidationReports) { + ConstraintComponent constraintComponent, boolean produceValidationReports, + boolean includeInferredStatements) { super(connection, dataGraph, parent, predicate, subject, object, targetQueryFragment, shape, - constraintComponent, produceValidationReports); + constraintComponent, produceValidationReports, includeInferredStatements); } Set getInvalidValues(Set valuesByPath, Set valuesByPredicate) { diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanValuesBasedOnPathAndPredicate.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanValuesBasedOnPathAndPredicate.java index c02ed9f60de..6a82fa4ef43 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanValuesBasedOnPathAndPredicate.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/CheckLessThanValuesBasedOnPathAndPredicate.java @@ -33,9 +33,9 @@ public class CheckLessThanValuesBasedOnPathAndPredicate extends AbstractPairwise public CheckLessThanValuesBasedOnPathAndPredicate(SailConnection connection, Resource[] dataGraph, PlanNode parent, IRI predicate, StatementMatcher.Variable subject, StatementMatcher.Variable object, SparqlFragment targetQueryFragment, Shape shape, ConstraintComponent constraintComponent, - boolean produceValidationReports) { + boolean produceValidationReports, boolean includeInferredStatements) { super(connection, dataGraph, parent, predicate, subject, object, targetQueryFragment, shape, - constraintComponent, produceValidationReports); + constraintComponent, produceValidationReports, includeInferredStatements); } Set getInvalidValues(Set valuesByPath, Set valuesByPredicate) { diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/ExternalFilterByQuery.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/ExternalFilterByQuery.java index 81f558766a5..4af89d0a82e 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/ExternalFilterByQuery.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/ExternalFilterByQuery.java @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: BSD-3-Clause *******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl.ast.planNodes; @@ -45,6 +46,7 @@ public class ExternalFilterByQuery extends FilterPlanNode { private final Function filterOn; private final String queryString; private final BiFunction map; + private final boolean includeInferredStatements; public ExternalFilterByQuery(SailConnection connection, Resource[] dataGraph, PlanNode parent, SparqlFragment queryFragment, @@ -78,6 +80,7 @@ public ExternalFilterByQuery(SailConnection connection, Resource[] dataGraph, Pl dataset = PlanNodeHelper.asDefaultGraphDataset(dataGraph); this.map = map; + this.includeInferredStatements = connectionsGroup.isIncludeInferredStatements(); } @@ -87,7 +90,7 @@ boolean checkTuple(Reference t) { Value value = filterOn.apply(t.get()); SingletonBindingSet bindings = new SingletonBindingSet(queryVariable.getName(), value); - try (var bindingSet = connection.evaluate(query, dataset, bindings, false)) { + try (var bindingSet = connection.evaluate(query, dataset, bindings, includeInferredStatements)) { if (bindingSet.hasNext()) { if (map != null) { do { diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicate.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicate.java index b433434491b..032b378fbf7 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicate.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicate.java @@ -36,6 +36,7 @@ public class FilterByPredicate implements PlanNode { final PlanNode parent; private final On on; private final ConnectionsGroup connectionsGroup; + private final boolean includeInferredStatements; private boolean printed = false; private ValidationExecutionLogger validationExecutionLogger; private final Resource[] dataGraph; @@ -54,6 +55,7 @@ public FilterByPredicate(SailConnection connection, Set filterOnPredicates, this.filterOnPredicates = filterOnPredicates; this.on = on; this.connectionsGroup = connectionsGroup; + this.includeInferredStatements = connectionsGroup.isIncludeInferredStatements(); } @Override @@ -121,15 +123,16 @@ private IRI matchesFilter(Value node) { if (node.isResource() && on == On.Subject) { return filterOnPredicates.stream() - .filter(predicate -> connection.hasStatement((Resource) node, predicate, null, true, - dataGraph)) + .filter(predicate -> connection.hasStatement((Resource) node, predicate, null, + includeInferredStatements, dataGraph)) .findFirst() .orElse(null); } else if (on == On.Object) { return filterOnPredicates.stream() - .filter(predicate -> connection.hasStatement(null, predicate, node, true, dataGraph)) + .filter(predicate -> connection.hasStatement(null, predicate, node, + includeInferredStatements, dataGraph)) .findFirst() .orElse(null); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicateObject.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicateObject.java index 4d882998ad7..a74595fede0 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicateObject.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterByPredicateObject.java @@ -8,6 +8,7 @@ * * SPDX-License-Identifier: BSD-3-Clause *******************************************************************************/ +// Some portions generated by Codex package org.eclipse.rdf4j.sail.shacl.ast.planNodes; @@ -270,7 +271,7 @@ private Boolean matchesCached(Resource subject, IRI filterOnPredicate, Resource[ private boolean matchesUnCached(Resource subject, IRI filterOnPredicate, Resource[] filterOnObject) { for (Resource object : filterOnObject) { if (connection.hasStatement(subject, filterOnPredicate, object, - includeInferred && !typeFilterWithInference, dataGraph)) { + includeInferred, dataGraph)) { return true; } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsObject.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsObject.java index bdb80fb8779..8876ce54ec1 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsObject.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsObject.java @@ -27,6 +27,7 @@ public class FilterTargetIsObject extends FilterPlanNode { private final SailConnection connection; private final Resource[] dataGraph; + private final boolean includeInferredStatements; public FilterTargetIsObject(SailConnection connection, Resource[] dataGraph, PlanNode parent, ConnectionsGroup connectionsGroup) { @@ -34,12 +35,13 @@ public FilterTargetIsObject(SailConnection connection, Resource[] dataGraph, Pla this.connection = connection; assert this.connection != null; this.dataGraph = dataGraph; + this.includeInferredStatements = connectionsGroup.isIncludeInferredStatements(); } @Override boolean checkTuple(Reference t) { Value target = t.get().getActiveTarget(); - return connection.hasStatement(null, null, target, true, dataGraph); + return connection.hasStatement(null, null, target, includeInferredStatements, dataGraph); } @Override @@ -61,7 +63,8 @@ public boolean equals(Object o) { .equals(((MemoryStoreConnection) that.connection).getSail()) && Arrays.equals(dataGraph, that.dataGraph); } - return Objects.equals(connection, that.connection) && Arrays.equals(dataGraph, that.dataGraph); + return Objects.equals(connection, that.connection) && Arrays.equals(dataGraph, that.dataGraph) + && includeInferredStatements == that.includeInferredStatements; } @Override @@ -70,6 +73,6 @@ public int hashCode() { return Objects.hash(super.hashCode(), ((MemoryStoreConnection) connection).getSail(), Arrays.hashCode(dataGraph)); } - return Objects.hash(super.hashCode(), connection, Arrays.hashCode(dataGraph)); + return Objects.hash(super.hashCode(), connection, Arrays.hashCode(dataGraph), includeInferredStatements); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsSubject.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsSubject.java index 3124e6f076e..9bae00d0686 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsSubject.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/FilterTargetIsSubject.java @@ -27,6 +27,7 @@ public class FilterTargetIsSubject extends FilterPlanNode { private final SailConnection connection; private final Resource[] dataGraph; + private final boolean includeInferredStatements; public FilterTargetIsSubject(SailConnection connection, Resource[] dataGraph, PlanNode parent, ConnectionsGroup connectionsGroup) { @@ -34,6 +35,7 @@ public FilterTargetIsSubject(SailConnection connection, Resource[] dataGraph, Pl this.connection = connection; assert this.connection != null; this.dataGraph = dataGraph; + this.includeInferredStatements = connectionsGroup.isIncludeInferredStatements(); } @Override @@ -42,7 +44,7 @@ boolean checkTuple(Reference t) { Value target = t.get().getActiveTarget(); if (target.isResource()) { - return connection.hasStatement((Resource) target, null, null, true, dataGraph); + return connection.hasStatement((Resource) target, null, null, includeInferredStatements, dataGraph); } else { return false; } @@ -68,7 +70,8 @@ public boolean equals(Object o) { .equals(((MemoryStoreConnection) that.connection).getSail()) && Arrays.equals(dataGraph, that.dataGraph); } - return Objects.equals(connection, that.connection) && Arrays.equals(dataGraph, that.dataGraph); + return Objects.equals(connection, that.connection) && Arrays.equals(dataGraph, that.dataGraph) + && includeInferredStatements == that.includeInferredStatements; } @Override @@ -77,6 +80,6 @@ public int hashCode() { return Objects.hash(super.hashCode(), ((MemoryStoreConnection) connection).getSail(), Arrays.hashCode(dataGraph)); } - return Objects.hash(super.hashCode(), connection, Arrays.hashCode(dataGraph)); + return Objects.hash(super.hashCode(), connection, Arrays.hashCode(dataGraph), includeInferredStatements); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/Select.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/Select.java index 45dfe20daa9..d4f1aa62255 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/Select.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/planNodes/Select.java @@ -46,12 +46,13 @@ public class Select implements PlanNode { private final String query; private final boolean sorted; private final Dataset dataset; + private final boolean includeInferredStatements; private StackTraceElement[] stackTrace; private boolean printed = false; private ValidationExecutionLogger validationExecutionLogger; public Select(SailConnection connection, SparqlFragment queryFragment, String orderBy, - Function mapper, Resource[] dataGraph) { + Function mapper, Resource[] dataGraph, boolean includeInferredStatements) { this.connection = connection; assert this.connection != null; this.mapper = mapper; @@ -75,13 +76,14 @@ public Select(SailConnection connection, SparqlFragment queryFragment, String or } dataset = PlanNodeHelper.asDefaultGraphDataset(dataGraph); + this.includeInferredStatements = includeInferredStatements; if (logger.isDebugEnabled()) { this.stackTrace = Thread.currentThread().getStackTrace(); } } public Select(SailConnection connection, String query, Function mapper, - Resource[] dataGraph) { + Resource[] dataGraph, boolean includeInferredStatements) { assert !query.toLowerCase().contains("order by") : "Queries with order by are not supported."; assert query.trim().toLowerCase().contains("select ") : "Expected query to contain select."; @@ -90,6 +92,7 @@ public Select(SailConnection connection, String query, Function mapper, - Function filter) { + Function filter, boolean includeInferredStatements) { this.connection = connection; assert this.connection != null; + this.includeInferredStatements = includeInferredStatements; this.subject = subject; this.predicate = predicate; this.object = object; @@ -72,7 +74,8 @@ protected void init() { assert statements == null; if (filter != null) { statements = new FilterIteration( - connection.getStatements(subject, predicate, object, true, dataGraph)) { + connection.getStatements(subject, predicate, object, includeInferredStatements, + dataGraph)) { @Override protected boolean accept(Statement st) { return filter.apply(st); @@ -84,7 +87,8 @@ protected void handleClose() { } }; } else { - statements = connection.getStatements(subject, predicate, object, true, dataGraph); + statements = connection.getStatements(subject, predicate, object, includeInferredStatements, + dataGraph); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllObjects.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllObjects.java index 8decbb793ec..c8363bd7272 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllObjects.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllObjects.java @@ -64,8 +64,8 @@ private PlanNode getAddedRemovedInner(SailConnection connection, Resource[] data ConstraintComponent.Scope scope, ConnectionsGroup connectionsGroup) { return Unique.getInstance(new UnorderedSelect(connection, null, - null, null, dataGraph, UnorderedSelect.Mapper.ObjectScopedMapper.getFunction(scope), null), false, - connectionsGroup); + null, null, dataGraph, UnorderedSelect.Mapper.ObjectScopedMapper.getFunction(scope), null, + connectionsGroup.isIncludeInferredStatements()), false, connectionsGroup); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllSubjects.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllSubjects.java index 292e2176188..8c3909c8197 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllSubjects.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/DashAllSubjects.java @@ -62,8 +62,8 @@ private PlanNode getAddedRemovedInner(SailConnection connection, Resource[] data ConstraintComponent.Scope scope, ConnectionsGroup connectionsGroup) { return Unique.getInstance(new UnorderedSelect(connection, null, - null, null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), null), false, - connectionsGroup); + null, null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), null, + connectionsGroup.isIncludeInferredStatements()), false, connectionsGroup); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/EffectiveTarget.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/EffectiveTarget.java index 2a9ef7040e1..470b32fc685 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/EffectiveTarget.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/EffectiveTarget.java @@ -9,6 +9,8 @@ * SPDX-License-Identifier: BSD-3-Clause *******************************************************************************/ +// Some portions generated by Codex + package org.eclipse.rdf4j.sail.shacl.ast.targets; import java.util.ArrayDeque; @@ -195,6 +197,7 @@ public boolean couldMatch(ConnectionsGroup connectionsGroup, Resource[] dataGrap SailConnection addedStatements = connectionsGroup.getAddedStatements(); SailConnection removedStatements = connectionsGroup.getRemovedStatements(); + boolean includeInferredStatements = connectionsGroup.isIncludeInferredStatements(); if (optional != null) { List statementMatchers = optional.getQueryFragment().getStatementMatchers(); @@ -202,11 +205,11 @@ public boolean couldMatch(ConnectionsGroup connectionsGroup, Resource[] dataGrap boolean match = addedStatements .hasStatement(currentStatementPattern.getSubjectValue(), currentStatementPattern.getPredicateValue(), - currentStatementPattern.getObjectValue(), false, dataGraph) + currentStatementPattern.getObjectValue(), includeInferredStatements, dataGraph) || removedStatements .hasStatement(currentStatementPattern.getSubjectValue(), currentStatementPattern.getPredicateValue(), - currentStatementPattern.getObjectValue(), false, dataGraph); + currentStatementPattern.getObjectValue(), includeInferredStatements, dataGraph); if (match) { return true; @@ -221,11 +224,11 @@ public boolean couldMatch(ConnectionsGroup connectionsGroup, Resource[] dataGrap boolean match = addedStatements .hasStatement(currentStatementPattern.getSubjectValue(), currentStatementPattern.getPredicateValue(), - currentStatementPattern.getObjectValue(), false, dataGraph) + currentStatementPattern.getObjectValue(), includeInferredStatements, dataGraph) || removedStatements .hasStatement(currentStatementPattern.getSubjectValue(), currentStatementPattern.getPredicateValue(), - currentStatementPattern.getObjectValue(), false, dataGraph); + currentStatementPattern.getObjectValue(), includeInferredStatements, dataGraph); if (match) { return true; @@ -239,7 +242,8 @@ public boolean couldMatch(ConnectionsGroup connectionsGroup, Resource[] dataGrap public PlanNode getAllTargets(ConnectionsGroup connectionsGroup, Resource[] dataGraph, ConstraintComponent.Scope scope) { - return new AllTargetsPlanNode(connectionsGroup.getBaseConnection(), dataGraph, chain, getVars(false), scope); + return new AllTargetsPlanNode(connectionsGroup.getBaseConnection(), dataGraph, chain, getVars(false), scope, + connectionsGroup.isIncludeInferredStatements()); } public PlanNode getPlanNode(ConnectionsGroup connectionsGroup, Resource[] dataGraph, @@ -323,7 +327,7 @@ public PlanNode getPlanNode(ConnectionsGroup connectionsGroup, Resource[] dataGr } else { targetsPlanNode = new AllTargetsPlanNode(connectionsGroup.getBaseConnection(), dataGraph, chain, - getVars(false), scope); + getVars(false), scope, connectionsGroup.isIncludeInferredStatements()); } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/SparqlTarget.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/SparqlTarget.java index d066d112cdc..3d77e023bfd 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/SparqlTarget.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/SparqlTarget.java @@ -103,7 +103,8 @@ public PlanNode getAdded(ConnectionsGroup connectionsGroup, Resource[] dataGraph List varNames = List.of("this"); return new Select(connectionsGroup.getBaseConnection(), sparqlFragment, null, - new AllTargetsPlanNode.AllTargetsBindingSetMapper(varNames, scope, false, dataGraph), dataGraph); + new AllTargetsPlanNode.AllTargetsBindingSetMapper(varNames, scope, false, dataGraph), dataGraph, + connectionsGroup.isIncludeInferredStatements()); } @Override diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetChainRetriever.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetChainRetriever.java index 3269c690b4a..f3b3e8748a2 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetChainRetriever.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetChainRetriever.java @@ -9,6 +9,8 @@ * SPDX-License-Identifier: BSD-3-Clause *******************************************************************************/ +// Some portions generated by Codex + package org.eclipse.rdf4j.sail.shacl.ast.targets; import java.util.ArrayList; @@ -242,7 +244,8 @@ public void calculateNextStatementMatcher() { statements = connection.getStatements( currentStatementMatcher.getSubjectValue(), currentStatementMatcher.getPredicateValue(), - currentStatementMatcher.getObjectValue(), false, dataGraph); + currentStatementMatcher.getObjectValue(), connectionsGroup.isIncludeInferredStatements(), + dataGraph); } while (!statements.hasNext()); @@ -289,7 +292,7 @@ private void calculateNextResult() { results = connectionsGroup.getBaseConnection() .evaluate(parsedQuery.getTupleExpr(), dataset, - EmptyBindingSet.getInstance(), true); + EmptyBindingSet.getInstance(), connectionsGroup.isIncludeInferredStatements()); } catch (MalformedQueryException e) { logger.error("Malformed query:\n{}", queryFragment); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetClass.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetClass.java index 0036591c91b..55508494276 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetClass.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetClass.java @@ -65,13 +65,15 @@ private PlanNode getAddedRemovedInner(SailConnection connection, Resource[] data if (targetClass.size() == 1) { Resource clazz = targetClass.stream().findAny().get(); planNode = new UnorderedSelect(connection, null, RDF.TYPE, clazz, - dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), null); + dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), null, + connectionsGroup.isIncludeInferredStatements()); // return planNode; } else { planNode = new Select(connection, SparqlFragment.bgp(Set.of(), getQueryFragment("?a", "?c", null, new StatementMatcher.StableRandomVariableProvider())), - "?a", b -> new ValidationTuple(b.getValue("a"), scope, false, dataGraph), dataGraph); + "?a", b -> new ValidationTuple(b.getValue("a"), scope, false, dataGraph), dataGraph, + connectionsGroup.isIncludeInferredStatements()); } return Unique.getInstance(planNode, false, connectionsGroup); @@ -116,23 +118,25 @@ public PlanNode getTargetFilter(ConnectionsGroup connectionsGroup, Resource[] da FilterByPredicateObject typeFoundInAdded = new FilterByPredicateObject( connectionsGroup.getAddedStatements(), dataGraph, RDF.TYPE, targetClass, - bufferedSplitter.getPlanNode(), true, FilterByPredicateObject.FilterOn.activeTarget, false, - connectionsGroup); + bufferedSplitter.getPlanNode(), true, FilterByPredicateObject.FilterOn.activeTarget, + connectionsGroup.isIncludeInferredStatements(), connectionsGroup); FilterByPredicateObject typeNotFoundInAdded = new FilterByPredicateObject( connectionsGroup.getAddedStatements(), dataGraph, RDF.TYPE, targetClass, - bufferedSplitter.getPlanNode(), false, FilterByPredicateObject.FilterOn.activeTarget, false, - connectionsGroup); + bufferedSplitter.getPlanNode(), false, FilterByPredicateObject.FilterOn.activeTarget, + connectionsGroup.isIncludeInferredStatements(), connectionsGroup); FilterByPredicateObject filterAgainstBaseConnection = new FilterByPredicateObject( connectionsGroup.getBaseConnection(), dataGraph, RDF.TYPE, targetClass, typeNotFoundInAdded, true, - FilterByPredicateObject.FilterOn.activeTarget, true, connectionsGroup); + FilterByPredicateObject.FilterOn.activeTarget, connectionsGroup.isIncludeInferredStatements(), + connectionsGroup); return new Sort(UnionNode.getInstance(connectionsGroup, typeFoundInAdded, filterAgainstBaseConnection), connectionsGroup); } else { return new FilterByPredicateObject(connectionsGroup.getBaseConnection(), dataGraph, RDF.TYPE, - targetClass, parent, true, FilterByPredicateObject.FilterOn.activeTarget, true, connectionsGroup); + targetClass, parent, true, FilterByPredicateObject.FilterOn.activeTarget, + connectionsGroup.isIncludeInferredStatements(), connectionsGroup); } } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetObjectsOf.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetObjectsOf.java index 2633682efce..b9369efd7a4 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetObjectsOf.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetObjectsOf.java @@ -67,7 +67,8 @@ private PlanNode getAddedRemovedInner(SailConnection connection, Resource[] data PlanNode planNode = targetObjectsOf.stream() .map(predicate -> (PlanNode) new UnorderedSelect(connection, null, - predicate, null, dataGraph, UnorderedSelect.Mapper.ObjectScopedMapper.getFunction(scope), null)) + predicate, null, dataGraph, UnorderedSelect.Mapper.ObjectScopedMapper.getFunction(scope), null, + connectionsGroup.isIncludeInferredStatements())) .reduce((nodes, nodes2) -> UnionNode.getInstance(connectionsGroup, nodes, nodes2)) .orElse(EmptyNode.getInstance()); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetSubjectsOf.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetSubjectsOf.java index 2d5e866f81f..eee0dd77874 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetSubjectsOf.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/ast/targets/TargetSubjectsOf.java @@ -68,7 +68,7 @@ private PlanNode getAddedRemovedInner(SailConnection connection, Resource[] data PlanNode planNode = targetSubjectsOf.stream() .map(predicate -> (PlanNode) new UnorderedSelect(connection, null, predicate, null, dataGraph, UnorderedSelect.Mapper.SubjectScopedMapper.getFunction(scope), - null)) + null, connectionsGroup.isIncludeInferredStatements())) .reduce((nodes, nodes2) -> UnionNode.getInstance(connectionsGroup, nodes, nodes2)) .orElse(EmptyNode.getInstance()); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfig.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfig.java index 31642f60aa5..8bab02787af 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfig.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfig.java @@ -13,6 +13,7 @@ import static org.eclipse.rdf4j.model.util.Values.literal; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.CACHE_SELECT_NODES; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.GLOBAL_LOG_VALIDATION_EXECUTION; +import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.INCLUDE_INFERRED_STATEMENTS; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.LOG_VALIDATION_PLANS; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.LOG_VALIDATION_VIOLATIONS; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.PARALLEL_VALIDATION; @@ -52,6 +53,7 @@ public class ShaclSailConfig extends AbstractDelegatingSailImplConfig { public static final boolean CACHE_SELECT_NODES_DEFAULT = true; public static final boolean GLOBAL_LOG_VALIDATION_EXECUTION_DEFAULT = false; public static final boolean RDFS_SUB_CLASS_REASONING_DEFAULT = true; + public static final boolean INCLUDE_INFERRED_STATEMENTS_DEFAULT = true; public static final boolean PERFORMANCE_LOGGING_DEFAULT = false; public static final boolean SERIALIZABLE_VALIDATION_DEFAULT = true; public static final boolean ECLIPSE_RDF4J_SHACL_EXTENSIONS_DEFAULT = false; @@ -68,6 +70,7 @@ public class ShaclSailConfig extends AbstractDelegatingSailImplConfig { private boolean cacheSelectNodes = CACHE_SELECT_NODES_DEFAULT; private boolean globalLogValidationExecution = GLOBAL_LOG_VALIDATION_EXECUTION_DEFAULT; private boolean rdfsSubClassReasoning = RDFS_SUB_CLASS_REASONING_DEFAULT; + private boolean includeInferredStatements = INCLUDE_INFERRED_STATEMENTS_DEFAULT; private boolean performanceLogging = PERFORMANCE_LOGGING_DEFAULT; private boolean serializableValidation = SERIALIZABLE_VALIDATION_DEFAULT; private boolean eclipseRdf4jShaclExtensions = ECLIPSE_RDF4J_SHACL_EXTENSIONS_DEFAULT; @@ -141,6 +144,14 @@ public void setRdfsSubClassReasoning(boolean rdfsSubClassReasoning) { this.rdfsSubClassReasoning = rdfsSubClassReasoning; } + public boolean isIncludeInferredStatements() { + return includeInferredStatements; + } + + public void setIncludeInferredStatements(boolean includeInferredStatements) { + this.includeInferredStatements = includeInferredStatements; + } + public boolean isPerformanceLogging() { return performanceLogging; } @@ -222,6 +233,7 @@ public Resource export(Model m) { m.add(implNode, CONFIG.Shacl.globalLogValidationExecution, BooleanLiteral.valueOf(isGlobalLogValidationExecution())); m.add(implNode, CONFIG.Shacl.rdfsSubClassReasoning, BooleanLiteral.valueOf(isRdfsSubClassReasoning())); + m.add(implNode, CONFIG.Shacl.includeInferredStatements, BooleanLiteral.valueOf(isIncludeInferredStatements())); m.add(implNode, CONFIG.Shacl.performanceLogging, BooleanLiteral.valueOf(isPerformanceLogging())); m.add(implNode, CONFIG.Shacl.serializableValidation, BooleanLiteral.valueOf(isSerializableValidation())); m.add(implNode, CONFIG.Shacl.eclipseRdf4jShaclExtensions, @@ -267,6 +279,11 @@ public void parse(Model m, Resource implNode) throws SailConfigException { Configurations.getLiteralValue(m, implNode, CONFIG.Shacl.rdfsSubClassReasoning, RDFS_SUB_CLASS_REASONING) .ifPresent(l -> setRdfsSubClassReasoning(l.booleanValue())); + Configurations + .getLiteralValue(m, implNode, CONFIG.Shacl.includeInferredStatements, + INCLUDE_INFERRED_STATEMENTS) + .ifPresent(l -> setIncludeInferredStatements(l.booleanValue())); + Configurations.getLiteralValue(m, implNode, CONFIG.Shacl.performanceLogging, PERFORMANCE_LOGGING) .ifPresent(l -> setPerformanceLogging(l.booleanValue())); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactory.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactory.java index 3819dca5b39..7b6e06a84ba 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactory.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactory.java @@ -65,6 +65,7 @@ public Sail getSail(SailImplConfig config) throws SailConfigException { sail.setPerformanceLogging(shaclSailConfig.isPerformanceLogging()); sail.setSerializableValidation(shaclSailConfig.isSerializableValidation()); sail.setRdfsSubClassReasoning(shaclSailConfig.isRdfsSubClassReasoning()); + sail.setIncludeInferredStatements(shaclSailConfig.isIncludeInferredStatements()); sail.setEclipseRdf4jShaclExtensions(shaclSailConfig.isEclipseRdf4jShaclExtensions()); sail.setDashDataShapes(shaclSailConfig.isDashDataShapes()); sail.setValidationResultsLimitTotal(shaclSailConfig.getValidationResultsLimitTotal()); diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailSchema.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailSchema.java index 9a8585bba34..8d6b0603e29 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailSchema.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailSchema.java @@ -81,6 +81,13 @@ public class ShaclSailSchema { */ public final static IRI RDFS_SUB_CLASS_REASONING = create("rdfsSubClassReasoning"); + /** + * http://rdf4j.org/config/sail/shacl#includeInferredStatements + * + * @deprecated use {@link CONFIG.Shacl#includeInferredStatements} instead. + */ + public final static IRI INCLUDE_INFERRED_STATEMENTS = create("includeInferredStatements"); + /** * http://rdf4j.org/config/sail/shacl#performanceLogging * diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/ConnectionsGroup.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/ConnectionsGroup.java index b67fff20fd2..3b53a1941d8 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/ConnectionsGroup.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/ConnectionsGroup.java @@ -58,6 +58,7 @@ public class ConnectionsGroup implements AutoCloseable { private final RdfsSubClassOfReasonerProvider rdfsSubClassOfReasonerProvider; private final boolean sparqlValidation; + private final boolean includeInferredStatements; // used to cache Select plan nodes so that we don't query a store for the same data during the same validation step. private final Map nodeCache = new ConcurrentHashMap<>(); @@ -70,12 +71,14 @@ public class ConnectionsGroup implements AutoCloseable { public ConnectionsGroup(SailConnection baseConnection, SailConnection previousStateConnection, Sail addedStatements, Sail removedStatements, Stats stats, RdfsSubClassOfReasonerProvider rdfsSubClassOfReasonerProvider, - ShaclSailConnection.Settings transactionSettings, boolean sparqlValidation) { + boolean includeInferredStatements, ShaclSailConnection.Settings transactionSettings, + boolean sparqlValidation) { this.baseConnection = baseConnection; this.previousStateConnection = previousStateConnection; this.stats = stats; this.rdfsSubClassOfReasonerProvider = rdfsSubClassOfReasonerProvider; + this.includeInferredStatements = includeInferredStatements; this.transactionSettings = transactionSettings; this.sparqlValidation = sparqlValidation; @@ -242,6 +245,10 @@ public Stats getStats() { return stats; } + public boolean isIncludeInferredStatements() { + return includeInferredStatements; + } + public ShaclSailConnection.Settings getTransactionSettings() { return transactionSettings; } diff --git a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/VerySimpleRdfsBackwardsChainingConnection.java b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/VerySimpleRdfsBackwardsChainingConnection.java index 2371154f092..9b4ec986ec8 100644 --- a/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/VerySimpleRdfsBackwardsChainingConnection.java +++ b/core/sail/shacl/src/main/java/org/eclipse/rdf4j/sail/shacl/wrapper/data/VerySimpleRdfsBackwardsChainingConnection.java @@ -36,26 +36,37 @@ public class VerySimpleRdfsBackwardsChainingConnection extends SailConnectionWrapper { private final RdfsSubClassOfReasoner rdfsSubClassOfReasoner; + private final boolean includeInferredStatements; private static final Logger logger = LoggerFactory.getLogger(VerySimpleRdfsBackwardsChainingConnection.class); public VerySimpleRdfsBackwardsChainingConnection(SailConnection wrappedCon, RdfsSubClassOfReasoner rdfsSubClassOfReasoner) { + this(wrappedCon, rdfsSubClassOfReasoner, false); + } + + public VerySimpleRdfsBackwardsChainingConnection(SailConnection wrappedCon, + RdfsSubClassOfReasoner rdfsSubClassOfReasoner, boolean includeInferredStatements) { super(wrappedCon); this.rdfsSubClassOfReasoner = rdfsSubClassOfReasoner; + this.includeInferredStatements = includeInferredStatements; } @Override public boolean hasStatement(Resource subj, IRI pred, Value obj, boolean includeInferred, Resource... contexts) throws SailException { - boolean hasStatement = super.hasStatement(subj, pred, obj, false, contexts); + boolean includeBaseInferred = includeInferredStatements && includeInferred; + if (rdfsSubClassOfReasoner == null) { + return super.hasStatement(subj, pred, obj, includeBaseInferred, contexts); + } + + boolean hasStatement = super.hasStatement(subj, pred, obj, includeBaseInferred, contexts); if (hasStatement) { return true; } - if (rdfsSubClassOfReasoner != null && includeInferred && obj != null && obj.isResource() - && RDF.TYPE.equals(pred)) { + if (obj != null && obj.isResource() && RDF.TYPE.equals(pred)) { Set types = rdfsSubClassOfReasoner.backwardsChain((Resource) obj); if (types.size() == 1) { @@ -63,7 +74,7 @@ public boolean hasStatement(Resource subj, IRI pred, Value obj, boolean includeI } if (types.size() > 10) { try (CloseableIteration statements = super.getStatements(subj, - RDF.TYPE, null, false, contexts)) { + RDF.TYPE, null, includeBaseInferred, contexts)) { return statements.stream() .map(Statement::getObject) .filter(Value::isResource) @@ -73,7 +84,7 @@ public boolean hasStatement(Resource subj, IRI pred, Value obj, boolean includeI } else { return types .stream() - .anyMatch(type -> super.hasStatement(subj, pred, type, false, contexts)); + .anyMatch(type -> super.hasStatement(subj, pred, type, includeBaseInferred, contexts)); } } @@ -85,13 +96,14 @@ public boolean hasStatement(Resource subj, IRI pred, Value obj, boolean includeI public CloseableIteration getStatements(Resource subj, IRI pred, Value obj, boolean includeInferred, Resource... contexts) throws SailException { - if (rdfsSubClassOfReasoner != null && includeInferred && obj != null && obj.isResource() + boolean includeBaseInferred = includeInferredStatements && includeInferred; + if (rdfsSubClassOfReasoner != null && obj != null && obj.isResource() && RDF.TYPE.equals(pred)) { Set inferredTypes = rdfsSubClassOfReasoner.backwardsChain((Resource) obj); if (inferredTypes.size() > 1) { CloseableIteration[] statementsMatchingInferredTypes = inferredTypes.stream() - .map(r -> super.getStatements(subj, pred, r, false, contexts)) + .map(r -> super.getStatements(subj, pred, r, includeBaseInferred, contexts)) .toArray(CloseableIteration[]::new); return new LookAheadIteration<>() { @@ -143,6 +155,6 @@ protected void handleClose() throws SailException { } } - return super.getStatements(subj, pred, obj, includeInferred, contexts); + return super.getStatements(subj, pred, obj, includeBaseInferred, contexts); } } diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/AbstractShaclReasoningCombinationTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/AbstractShaclReasoningCombinationTest.java new file mode 100644 index 00000000000..139d87151d5 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/AbstractShaclReasoningCombinationTest.java @@ -0,0 +1,243 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import java.io.StringReader; +import java.util.Set; + +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.util.Values; +import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.repository.RepositoryConnection; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.ValidationApproach; +import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; +import org.junit.jupiter.api.Assertions; + +abstract class AbstractShaclReasoningCombinationTest { + + protected static final IRI DATA_GRAPH = Values.iri("urn:data"); + protected static final IRI ONTOLOGY_GRAPH = Values.iri("urn:ontology"); + + protected static final class ReasoningCase { + final String name; + final String shapesTurtle; + final String dataTurtle; + final String updatePart1; + final String updatePart2; + final boolean conformsWhenEnabled; + final boolean conformsWhenDisabled; + + ReasoningCase(String name, String shapesTurtle, String dataTurtle, String updatePart1, String updatePart2, + boolean conformsWhenEnabled, boolean conformsWhenDisabled) { + this.name = name; + this.shapesTurtle = shapesTurtle; + this.dataTurtle = dataTurtle; + this.updatePart1 = updatePart1; + this.updatePart2 = updatePart2; + this.conformsWhenEnabled = conformsWhenEnabled; + this.conformsWhenDisabled = conformsWhenDisabled; + } + + boolean expectedConforms(boolean enabled) { + return enabled ? conformsWhenEnabled : conformsWhenDisabled; + } + + @Override + public String toString() { + return name; + } + } + + protected void runAllModes(ReasoningCase testCase, boolean expectedEnabled, boolean rdfsSubClassReasoning, + boolean includeInferredStatements) { + assertSingleTransaction(testCase, expectedEnabled, rdfsSubClassReasoning, includeInferredStatements); + assertBulkValidation(testCase, expectedEnabled, rdfsSubClassReasoning, includeInferredStatements); + assertMultiUpdateTransaction(testCase, expectedEnabled, rdfsSubClassReasoning, includeInferredStatements); + assertShaclValidator(testCase, expectedEnabled, rdfsSubClassReasoning, includeInferredStatements); + } + + protected abstract NotifyingSail createDataSail(); + + protected abstract String ontologyTurtle(); + + private void assertSingleTransaction(ReasoningCase testCase, boolean expectedEnabled, boolean rdfsSubClassReasoning, + boolean includeInferredStatements) { + SailRepository repository = createRepository(testCase, rdfsSubClassReasoning, includeInferredStatements); + try { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE); + addTurtle(connection, testCase.dataTurtle, DATA_GRAPH); + commitExpecting(testCase, expectedEnabled, connection, "single transaction"); + } + } finally { + repository.shutDown(); + } + } + + private void assertBulkValidation(ReasoningCase testCase, boolean expectedEnabled, boolean rdfsSubClassReasoning, + boolean includeInferredStatements) { + SailRepository repository = createRepository(testCase, rdfsSubClassReasoning, includeInferredStatements); + try { + loadDataWithoutValidation(repository, testCase.dataTurtle); + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(ValidationApproach.Bulk); + try { + connection.commit(); + Assertions.assertTrue(testCase.expectedConforms(expectedEnabled), + "bulk validation should have failed"); + } catch (RepositoryException e) { + connection.rollback(); + Throwable cause = e.getCause(); + if (!(cause instanceof ShaclSailValidationException)) { + throw e; + } + Assertions.assertFalse(testCase.expectedConforms(expectedEnabled), + "bulk validation unexpectedly failed"); + } + } + } finally { + repository.shutDown(); + } + } + + private void assertMultiUpdateTransaction(ReasoningCase testCase, boolean expectedEnabled, + boolean rdfsSubClassReasoning, + boolean includeInferredStatements) { + SailRepository repository = createRepository(testCase, rdfsSubClassReasoning, includeInferredStatements); + try { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE); + connection.prepareUpdate(QueryLanguage.SPARQL, testCase.updatePart1).execute(); + connection.prepareUpdate(QueryLanguage.SPARQL, testCase.updatePart2).execute(); + commitExpecting(testCase, expectedEnabled, connection, "multi-update transaction"); + } + } finally { + repository.shutDown(); + } + } + + private void assertShaclValidator(ReasoningCase testCase, boolean expectedEnabled, boolean rdfsSubClassReasoning, + boolean includeInferredStatements) { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + SailRepository dataRepo = new SailRepository(createDataSail()); + try { + shapesRepo.init(); + dataRepo.init(); + loadShapes(shapesRepo, testCase.shapesTurtle); + loadOntology(dataRepo); + loadDataWithoutValidation(dataRepo, testCase.dataTurtle); + + ValidationReport report = ShaclValidator.builder() + .setEclipseRdf4jShaclExtensions(true) + .setCacheSelectNodes(true) + .setParallelValidation(false) + .setRdfsSubClassReasoning(rdfsSubClassReasoning) + .setIncludeInferredStatements(includeInferredStatements) + .withShapes(shapesRepo.getSail()) + .build() + .validate(dataRepo.getSail()); + + Assertions.assertEquals(testCase.expectedConforms(expectedEnabled), report.conforms(), + "ShaclValidator result mismatch"); + } finally { + try { + shapesRepo.shutDown(); + } finally { + dataRepo.shutDown(); + } + } + } + + private void commitExpecting(ReasoningCase testCase, boolean expectedEnabled, RepositoryConnection connection, + String label) { + boolean expectedConforms = testCase.expectedConforms(expectedEnabled); + try { + connection.commit(); + Assertions.assertTrue(expectedConforms, label + " should have failed"); + } catch (RepositoryException e) { + connection.rollback(); + Throwable cause = e.getCause(); + if (!(cause instanceof ShaclSailValidationException)) { + throw e; + } + Assertions.assertFalse(expectedConforms, label + " should have conformed"); + } + } + + private SailRepository createRepository(ReasoningCase testCase, boolean rdfsSubClassReasoning, + boolean includeInferredStatements) { + SailRepository repository = new SailRepository( + createShaclSail(rdfsSubClassReasoning, includeInferredStatements)); + repository.init(); + loadShapes(repository, testCase.shapesTurtle); + loadOntology(repository); + return repository; + } + + private ShaclSail createShaclSail(boolean rdfsSubClassReasoning, boolean includeInferredStatements) { + NotifyingSail baseSail = createDataSail(); + ShaclSail shaclSail = new ShaclSail(baseSail); + shaclSail.setLogValidationPlans(false); + shaclSail.setCacheSelectNodes(true); + shaclSail.setParallelValidation(false); + shaclSail.setLogValidationViolations(false); + shaclSail.setGlobalLogValidationExecution(false); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setDashDataShapes(false); + shaclSail.setPerformanceLogging(false); + shaclSail.setSerializableValidation(false); + shaclSail.setShapesGraphs(Set.of(RDF4J.SHACL_SHAPE_GRAPH)); + shaclSail.setRdfsSubClassReasoning(rdfsSubClassReasoning); + shaclSail.setIncludeInferredStatements(includeInferredStatements); + return shaclSail; + } + + private void loadShapes(SailRepository repository, String turtle) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, turtle, RDF4J.SHACL_SHAPE_GRAPH); + connection.commit(); + } + } + + private void loadOntology(SailRepository repository) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, ontologyTurtle(), ONTOLOGY_GRAPH); + connection.commit(); + } + } + + private void loadDataWithoutValidation(SailRepository repository, String turtle) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, turtle, DATA_GRAPH); + connection.commit(); + } + } + + private static void addTurtle(RepositoryConnection connection, String turtle, IRI context) { + try { + connection.add(new StringReader(turtle), "", RDFFormat.TURTLE, context); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/InferredPlanNodeAndReasonerCoverageTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/InferredPlanNodeAndReasonerCoverageTest.java new file mode 100644 index 00000000000..9ee42467a42 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/InferredPlanNodeAndReasonerCoverageTest.java @@ -0,0 +1,290 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.NoSuchElementException; +import java.util.Set; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.eclipse.rdf4j.query.BindingSet; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.SailConnection; +import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterByPredicate; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.Select; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationExecutionLogger; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationTuple; +import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup; +import org.eclipse.rdf4j.sail.shacl.wrapper.data.VerySimpleRdfsBackwardsChainingConnection; +import org.junit.jupiter.api.Test; + +class InferredPlanNodeAndReasonerCoverageTest { + + private static final SimpleValueFactory VF = SimpleValueFactory.getInstance(); + + private static final Resource[] ALL_CONTEXTS = { null }; + + private static final IRI TARGET = VF.createIRI("urn:target"); + private static final IRI P = VF.createIRI("urn:p"); + private static final IRI P_SUB = VF.createIRI("urn:pSub"); + private static final IRI O = VF.createIRI("urn:o"); + private static final IRI CLASS_SUB = VF.createIRI("urn:ClassSub"); + private static final IRI CLASS_SUPER = VF.createIRI("urn:ClassSuper"); + + @Test + void verySimpleRdfsBackwardsChainingConnectionShouldGateBaseInferredStatementsByConstructorFlag() { + try (TestSailContext context = TestSailContext.withInferredStatement()) { + VerySimpleRdfsBackwardsChainingConnection wrappedWithoutBaseInferred = new VerySimpleRdfsBackwardsChainingConnection( + context.connection, null, false); + + assertFalse(wrappedWithoutBaseInferred.hasStatement(TARGET, P, O, true, ALL_CONTEXTS), + "Expected constructor includeInferredStatements=false to hide base inferred statements"); + assertFalse(wrappedWithoutBaseInferred.hasStatement(TARGET, P, O, false, ALL_CONTEXTS), + "Expected includeInferred=false to hide base inferred statements"); + + try (CloseableIteration statements = wrappedWithoutBaseInferred.getStatements(TARGET, + P, O, + true, ALL_CONTEXTS)) { + assertFalse(statements.hasNext(), + "Expected getStatements() to hide base inferred statements when constructor flag is false"); + } + + VerySimpleRdfsBackwardsChainingConnection wrappedWithBaseInferred = new VerySimpleRdfsBackwardsChainingConnection( + context.connection, null, true); + + assertTrue(wrappedWithBaseInferred.hasStatement(TARGET, P, O, true, ALL_CONTEXTS), + "Expected constructor includeInferredStatements=true to expose base inferred statements"); + assertFalse(wrappedWithBaseInferred.hasStatement(TARGET, P, O, false, ALL_CONTEXTS), + "Expected includeInferred=false to hide base inferred statements even when constructor flag is true"); + + try (CloseableIteration statements = wrappedWithBaseInferred.getStatements(TARGET, P, + O, + true, ALL_CONTEXTS)) { + assertTrue(statements.hasNext(), + "Expected getStatements() to expose base inferred statements when constructor flag is true"); + } + } + } + + @Test + void filterByPredicateShouldRespectConnectionsGroupIncludeInferredSetting() { + try (TestSailContext context = TestSailContext.withInferredStatement()) { + PlanNode parent = new SingletonPlanNode( + new ValidationTuple(TARGET, ConstraintComponent.Scope.nodeShape, false, ALL_CONTEXTS)); + + try (ConnectionsGroup withoutInferred = context.connectionsGroup(false)) { + FilterByPredicate filterByPredicate = new FilterByPredicate( + withoutInferred.getBaseConnection(), + Set.of(P), + parent, + FilterByPredicate.On.Subject, + ALL_CONTEXTS, + withoutInferred); + assertEquals(0, countTuples(filterByPredicate), + "Expected inferred statements to be excluded when includeInferredStatements=false"); + } + + try (ConnectionsGroup withInferred = context.connectionsGroup(true)) { + FilterByPredicate filterByPredicate = new FilterByPredicate( + withInferred.getBaseConnection(), + Set.of(P), + parent, + FilterByPredicate.On.Subject, + ALL_CONTEXTS, + withInferred); + assertEquals(1, countTuples(filterByPredicate), + "Expected inferred statements to be included when includeInferredStatements=true"); + } + } + } + + @Test + void selectShouldRespectIncludeInferredArgument() { + try (TestSailContext context = TestSailContext.withInferredStatement()) { + String query = "select * where { ?this . }"; + + Select withoutInferred = new Select( + context.connection, + query, + InferredPlanNodeAndReasonerCoverageTest::mapBindingSet, + ALL_CONTEXTS, + false); + assertEquals(0, countTuples(withoutInferred), + "Expected Select to hide inferred statements when includeInferred=false"); + + Select withInferred = new Select( + context.connection, + query, + InferredPlanNodeAndReasonerCoverageTest::mapBindingSet, + ALL_CONTEXTS, + true); + assertEquals(1, countTuples(withInferred), + "Expected Select to include inferred statements when includeInferred=true"); + } + } + + private static ValidationTuple mapBindingSet(BindingSet bindingSet) { + return new ValidationTuple((Resource) bindingSet.getValue("this"), ConstraintComponent.Scope.nodeShape, false, + ALL_CONTEXTS); + } + + private static int countTuples(PlanNode planNode) { + planNode.receiveLogger(ValidationExecutionLogger.getInstance(false)); + try (CloseableIteration iterator = planNode.iterator()) { + int count = 0; + while (iterator.hasNext()) { + iterator.next(); + count++; + } + return count; + } + } + + private static final class SingletonPlanNode implements PlanNode { + + private final ValidationTuple tuple; + + private SingletonPlanNode(ValidationTuple tuple) { + this.tuple = tuple; + } + + @Override + public CloseableIteration iterator() { + return new CloseableIteration<>() { + private boolean available = true; + + @Override + public void close() { + available = false; + } + + @Override + public boolean hasNext() { + return available; + } + + @Override + public ValidationTuple next() { + if (!available) { + throw new NoSuchElementException(); + } + available = false; + return tuple; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int depth() { + return 0; + } + + @Override + public void getPlanAsGraphvizDot(StringBuilder stringBuilder) { + // no-op + } + + @Override + public String getId() { + return Integer.toString(System.identityHashCode(this)); + } + + @Override + public void receiveLogger(ValidationExecutionLogger validationExecutionLogger) { + // no-op + } + + @Override + public boolean producesSorted() { + return false; + } + + @Override + public boolean requiresSorted() { + return false; + } + } + + private static final class TestSailContext implements AutoCloseable { + private final NotifyingSail sail; + private final SailConnection connection; + + private TestSailContext(NotifyingSail sail, SailConnection connection) { + this.sail = sail; + this.connection = connection; + } + + static TestSailContext withInferredStatement() { + MemoryStore memoryStore = new MemoryStore(); + NotifyingSail sail = new SchemaCachingRDFSInferencer(memoryStore, false); + sail.init(); + + SailConnection connection = sail.getConnection(); + connection.begin(IsolationLevels.NONE); + connection.addStatement(P_SUB, RDFS.SUBPROPERTYOF, P); + connection.addStatement(TARGET, P_SUB, O); + connection.commit(); + + connection.begin(IsolationLevels.NONE); + assertTrue(connection.hasStatement(TARGET, P, O, true, ALL_CONTEXTS), + "Sanity check: expected inferred statement to exist"); + assertFalse(connection.hasStatement(TARGET, P, O, false, ALL_CONTEXTS), + "Sanity check: expected inferred statement to be hidden when includeInferred=false"); + + return new TestSailContext(sail, connection); + } + + ConnectionsGroup connectionsGroup(boolean includeInferredStatements) { + ShaclSailConnection.Settings transactionSettings = new ShaclSailConnection.Settings(false, true, false, + IsolationLevels.NONE); + return new ConnectionsGroup(connection, null, null, null, new Stats(), null, includeInferredStatements, + transactionSettings, true); + } + + @Override + public void close() { + try { + try { + try { + connection.rollback(); + } catch (Exception ignored) { + // ignore + } + connection.close(); + } finally { + sail.shutDown(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/InferredStatementHandlingConsistencyTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/InferredStatementHandlingConsistencyTest.java new file mode 100644 index 00000000000..2cce7aa8127 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/InferredStatementHandlingConsistencyTest.java @@ -0,0 +1,460 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import java.util.ArrayDeque; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Set; + +import org.eclipse.rdf4j.common.iteration.CloseableIteration; +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Namespace; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.util.Values; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.SailConnection; +import org.eclipse.rdf4j.sail.inferencer.InferencerConnection; +import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.ast.SparqlFragment; +import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher; +import org.eclipse.rdf4j.sail.shacl.ast.Targetable; +import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ConstraintComponent; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.BindSelect; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ExternalFilterByQuery; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterByPredicateObject; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.UnBufferedPlanNode; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationExecutionLogger; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationTuple; +import org.eclipse.rdf4j.sail.shacl.ast.targets.EffectiveTarget; +import org.eclipse.rdf4j.sail.shacl.ast.targets.TargetChainRetriever; +import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup; +import org.eclipse.rdf4j.sail.shacl.wrapper.data.RdfsSubClassOfReasoner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class InferredStatementHandlingConsistencyTest { + + private static final Resource[] ALL_CONTEXTS = {}; + + private static final IRI TARGET = Values.iri("urn:target"); + private static final IRI P = Values.iri("urn:p"); + private static final IRI P_SUB = Values.iri("urn:pSub"); + private static final IRI DOMAIN_P = Values.iri("urn:domainP"); + private static final IRI O = Values.iri("urn:o"); + private static final IRI C = Values.iri("urn:c"); + + @Test + void externalFilterByQueryShouldUseConnectionsGroupIncludeInferredStatements() { + try (TestSailContext context = TestSailContext.withInferredStatement()) { + try (ConnectionsGroup connectionsGroup = context.connectionsGroup(true)) { + PlanNode parent = new SingletonPlanNode( + new ValidationTuple(TARGET, ConstraintComponent.Scope.nodeShape, false, ALL_CONTEXTS)); + + SparqlFragment query = SparqlFragment.bgp(List.of(), + StatementMatcher.Variable.THIS.asSparqlVariable() + " .", + false); + + PlanNode accepted = new ExternalFilterByQuery( + connectionsGroup.getBaseConnection(), + ALL_CONTEXTS, + parent, + query, + StatementMatcher.Variable.THIS, + ValidationTuple::getActiveTarget, + null, + connectionsGroup + ).getTrueNode(UnBufferedPlanNode.class); + + Assertions.assertEquals(1, countTuples(accepted), + "Expected inferred statements to be visible when includeInferredStatements is enabled"); + } + } + } + + @Test + void bindSelectShouldUseConnectionsGroupIncludeInferredStatements() { + try (TestSailContext context = TestSailContext.withInferredStatement()) { + try (ConnectionsGroup connectionsGroup = context.connectionsGroup(false)) { + PlanNode source = new SingletonPlanNode( + new ValidationTuple(TARGET, ConstraintComponent.Scope.nodeShape, false, ALL_CONTEXTS)); + + SparqlFragment query = SparqlFragment.bgp(List.of(), + "?x ?o .", + false); + + BindSelect bindSelect = new BindSelect( + connectionsGroup.getBaseConnection(), + ALL_CONTEXTS, + query, + List.of(new StatementMatcher.Variable<>("x")), + source, + List.of("x", "o"), + ConstraintComponent.Scope.nodeShape, + 10, + EffectiveTarget.Extend.right, + false, + connectionsGroup + ); + + Assertions.assertEquals(0, countTuples(bindSelect), + "Expected inferred statements to be hidden when includeInferredStatements is disabled"); + } + } + } + + @Test + void filterByPredicateObjectShouldIncludeInferredTypeStatementsWhenReasonerEnabled() { + try (TestSailContext context = TestSailContext.withInferredTypeFromDomain()) { + try (ConnectionsGroup connectionsGroup = context.connectionsGroup(true, new RdfsSubClassOfReasoner())) { + PlanNode parent = new SingletonPlanNode( + new ValidationTuple(TARGET, ConstraintComponent.Scope.nodeShape, false, ALL_CONTEXTS)); + + PlanNode accepted = new FilterByPredicateObject( + connectionsGroup.getBaseConnection(), + ALL_CONTEXTS, + RDF.TYPE, + Set.of(C), + parent, + true, + FilterByPredicateObject.FilterOn.activeTarget, + connectionsGroup.isIncludeInferredStatements(), + connectionsGroup + ); + + Assertions.assertEquals(1, countTuples(accepted), + "Expected inferred rdf:type statements to be visible when includeInferredStatements is enabled"); + } + } + } + + @Test + void targetChainRetrieverShouldSeeInferredStatementsInAddedStatementsDelta() { + MemoryStore baseStore = new MemoryStore(); + baseStore.init(); + + NotifyingSail addedStatements = new SchemaCachingRDFSInferencer(new MemoryStore(), false); + addedStatements.init(); + + try (SailConnection baseConnection = baseStore.getConnection()) { + baseConnection.begin(IsolationLevels.NONE); + baseConnection.addStatement(TARGET, P, O); + baseConnection.commit(); + baseConnection.begin(IsolationLevels.NONE); + + try (InferencerConnection addedConnection = (InferencerConnection) addedStatements.getConnection()) { + addedConnection.begin(IsolationLevels.NONE); + addedConnection.addInferredStatement(TARGET, P, O, (Resource) null); + addedConnection.commit(); + } + + try (SailConnection checkConnection = addedStatements.getConnection()) { + checkConnection.begin(IsolationLevels.NONE); + Assertions.assertTrue(checkConnection.hasStatement(TARGET, P, O, true, (Resource) null), + "Sanity check: expected inferred statement in added-statements delta to exist"); + Assertions.assertFalse(checkConnection.hasStatement(TARGET, P, O, false, (Resource) null), + "Sanity check: expected inferred statement to be hidden when includeInferred=false"); + checkConnection.commit(); + } + + ShaclSailConnection.Settings transactionSettings = new ShaclSailConnection.Settings(false, true, false, + IsolationLevels.NONE); + + try (ConnectionsGroup connectionsGroup = new ConnectionsGroup(baseConnection, null, addedStatements, null, + new Stats(), null, true, transactionSettings, true)) { + StatementMatcher statementMatcher = new StatementMatcher( + new StatementMatcher.Variable<>("this"), + new StatementMatcher.Variable<>(P), + new StatementMatcher.Variable<>(O), + null, + Set.of()); + + SparqlFragment query = SparqlFragment.bgp(List.of(), + StatementMatcher.Variable.THIS.asSparqlVariable() + " .", + false); + + PlanNode retriever = new TargetChainRetriever( + connectionsGroup, + new Resource[] { null }, + List.of(statementMatcher), + null, + null, + query, + List.of(StatementMatcher.Variable.THIS), + ConstraintComponent.Scope.nodeShape, + false); + + Assertions.assertEquals(1, countTuples(retriever), + "Expected inferred statements in the delta store to be visible to target-chain retrieval"); + } + } finally { + try { + addedStatements.shutDown(); + } finally { + baseStore.shutDown(); + } + } + } + + @Test + void effectiveTargetCouldMatchShouldSeeInferredStatementsInAddedStatementsDelta() { + MemoryStore baseStore = new MemoryStore(); + baseStore.init(); + + NotifyingSail addedStatements = new SchemaCachingRDFSInferencer(new MemoryStore(), false); + addedStatements.init(); + + MemoryStore removedStatements = new MemoryStore(); + removedStatements.init(); + + try (SailConnection baseConnection = baseStore.getConnection()) { + baseConnection.begin(IsolationLevels.NONE); + + try (InferencerConnection addedConnection = (InferencerConnection) addedStatements.getConnection()) { + addedConnection.begin(IsolationLevels.NONE); + addedConnection.addInferredStatement(TARGET, P, O, (Resource) null); + addedConnection.commit(); + } + + try (SailConnection checkConnection = addedStatements.getConnection()) { + checkConnection.begin(IsolationLevels.NONE); + Assertions.assertTrue(checkConnection.hasStatement(TARGET, P, O, true, (Resource) null), + "Sanity check: expected inferred statement in added-statements delta to exist"); + Assertions.assertFalse(checkConnection.hasStatement(TARGET, P, O, false, (Resource) null), + "Sanity check: expected inferred statement to be hidden when includeInferred=false"); + checkConnection.commit(); + } + + ShaclSailConnection.Settings transactionSettings = new ShaclSailConnection.Settings(false, true, false, + IsolationLevels.NONE); + + try (ConnectionsGroup connectionsGroup = new ConnectionsGroup(baseConnection, null, addedStatements, + removedStatements, + new Stats(), null, true, transactionSettings, true)) { + + Targetable targetable = new Targetable() { + @Override + public SparqlFragment getTargetQueryFragment(StatementMatcher.Variable subject, + StatementMatcher.Variable object, RdfsSubClassOfReasoner rdfsSubClassOfReasoner, + StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider, + Set inheritedVarNames) { + StatementMatcher.Variable targetVariable = subject != null ? subject : object; + StatementMatcher statementMatcher = new StatementMatcher( + targetVariable, + new StatementMatcher.Variable<>(P), + new StatementMatcher.Variable<>(O), + null, + inheritedVarNames); + + return SparqlFragment.bgp(List.of(), + targetVariable.asSparqlVariable() + " .", + statementMatcher); + } + + @Override + public Set getNamespaces() { + return Set.of(); + } + }; + + ArrayDeque chain = new ArrayDeque<>(); + chain.add(targetable); + + EffectiveTarget effectiveTarget = new EffectiveTarget(chain, null, null, + new StatementMatcher.StableRandomVariableProvider()); + + Assertions.assertTrue(effectiveTarget.couldMatch(connectionsGroup, new Resource[] { null }), + "Expected couldMatch() to see inferred statements in the delta store when includeInferredStatements is enabled"); + } + } finally { + try { + removedStatements.shutDown(); + } finally { + try { + addedStatements.shutDown(); + } finally { + baseStore.shutDown(); + } + } + } + } + + private static int countTuples(PlanNode planNode) { + planNode.receiveLogger(ValidationExecutionLogger.getInstance(false)); + try (CloseableIteration iterator = planNode.iterator()) { + int count = 0; + while (iterator.hasNext()) { + iterator.next(); + count++; + } + return count; + } + } + + private static final class SingletonPlanNode implements PlanNode { + + private final ValidationTuple tuple; + + private SingletonPlanNode(ValidationTuple tuple) { + this.tuple = tuple; + } + + @Override + public CloseableIteration iterator() { + return new CloseableIteration<>() { + private boolean available = true; + + @Override + public void close() { + available = false; + } + + @Override + public boolean hasNext() { + return available; + } + + @Override + public ValidationTuple next() { + if (!available) { + throw new NoSuchElementException(); + } + available = false; + return tuple; + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int depth() { + return 0; + } + + @Override + public void getPlanAsGraphvizDot(StringBuilder stringBuilder) { + // no-op + } + + @Override + public String getId() { + return Integer.toString(System.identityHashCode(this)); + } + + @Override + public void receiveLogger(ValidationExecutionLogger validationExecutionLogger) { + // no-op + } + + @Override + public boolean producesSorted() { + return false; + } + + @Override + public boolean requiresSorted() { + return false; + } + } + + private static final class TestSailContext implements AutoCloseable { + private final NotifyingSail sail; + private final SailConnection connection; + + private TestSailContext(NotifyingSail sail, SailConnection connection) { + this.sail = sail; + this.connection = connection; + } + + static TestSailContext withInferredStatement() { + MemoryStore memoryStore = new MemoryStore(); + NotifyingSail sail = new SchemaCachingRDFSInferencer(memoryStore, false); + sail.init(); + + SailConnection connection = sail.getConnection(); + connection.begin(IsolationLevels.NONE); + connection.addStatement(P_SUB, RDFS.SUBPROPERTYOF, P); + connection.addStatement(TARGET, P_SUB, O); + connection.commit(); + + connection.begin(IsolationLevels.NONE); + Assertions.assertTrue(connection.hasStatement(TARGET, P, O, true), + "Sanity check: expected inferred statement to exist"); + Assertions.assertFalse(connection.hasStatement(TARGET, P, O, false), + "Sanity check: expected inferred statement to be hidden when includeInferred=false"); + + return new TestSailContext(sail, connection); + } + + static TestSailContext withInferredTypeFromDomain() { + MemoryStore memoryStore = new MemoryStore(); + NotifyingSail sail = new SchemaCachingRDFSInferencer(memoryStore, false); + sail.init(); + + SailConnection connection = sail.getConnection(); + connection.begin(IsolationLevels.NONE); + connection.addStatement(DOMAIN_P, RDFS.DOMAIN, C); + connection.addStatement(TARGET, DOMAIN_P, O); + connection.commit(); + + connection.begin(IsolationLevels.NONE); + Assertions.assertTrue(connection.hasStatement(TARGET, RDF.TYPE, C, true), + "Sanity check: expected inferred rdf:type statement to exist"); + Assertions.assertFalse(connection.hasStatement(TARGET, RDF.TYPE, C, false), + "Sanity check: expected inferred rdf:type statement to be hidden when includeInferred=false"); + + return new TestSailContext(sail, connection); + } + + ConnectionsGroup connectionsGroup(boolean includeInferredStatements) { + ShaclSailConnection.Settings transactionSettings = new ShaclSailConnection.Settings(false, true, false, + IsolationLevels.NONE); + return new ConnectionsGroup(connection, null, null, null, new Stats(), null, includeInferredStatements, + transactionSettings, true); + } + + ConnectionsGroup connectionsGroup(boolean includeInferredStatements, RdfsSubClassOfReasoner reasoner) { + ShaclSailConnection.Settings transactionSettings = new ShaclSailConnection.Settings(false, true, false, + IsolationLevels.NONE); + return new ConnectionsGroup(connection, null, null, null, new Stats(), () -> reasoner, + includeInferredStatements, transactionSettings, true); + } + + @Override + public void close() { + try { + try { + try { + connection.rollback(); + } catch (Exception ignored) { + // ignore + } + connection.close(); + } finally { + sail.shutDown(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoChangeTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoChangeTest.java index a03b301368d..55348e15aed 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoChangeTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoChangeTest.java @@ -54,7 +54,7 @@ public void testSkippingValidationWhenThereAreNoChanges() throws IOException, In ShaclSailConnection connectionSpy = Mockito.spy((ShaclSailConnection) connection); connectionSpy.begin(); connectionSpy.commit(); - verify(connectionSpy, never()).prepareValidation(new ValidationSettings()); + verify(connectionSpy, never()).prepareValidation(Mockito.any(), Mockito.anyBoolean()); } shaclSail.shutDown(); diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoShapesTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoShapesTest.java index 291b87bf65c..90b9856a19e 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoShapesTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/NoShapesTest.java @@ -34,7 +34,7 @@ public void testSkippingValidationWhenThereAreNoShapes() throws InterruptedExcep connectionSpy.begin(); connectionSpy.addStatement(RDF.TYPE, RDF.TYPE, RDFS.RESOURCE); connectionSpy.commit(); - verify(connectionSpy, never()).prepareValidation(new ValidationSettings()); + verify(connectionSpy, never()).prepareValidation(Mockito.any(), Mockito.anyBoolean()); } try (SailConnection connection = shaclSail.getConnection()) { @@ -43,7 +43,7 @@ public void testSkippingValidationWhenThereAreNoShapes() throws InterruptedExcep connectionSpy.begin(); connectionSpy.addStatement(RDF.TYPE, RDF.TYPE, RDF.PROPERTY); connectionSpy.commit(); - verify(connectionSpy, never()).prepareValidation(new ValidationSettings()); + verify(connectionSpy, never()).prepareValidation(Mockito.any(), Mockito.anyBoolean()); } try (SailConnection connection = shaclSail.getConnection()) { @@ -53,7 +53,7 @@ public void testSkippingValidationWhenThereAreNoShapes() throws InterruptedExcep connectionSpy.addStatement(RDF.TYPE, RDF.TYPE, RDF.PREDICATE); connectionSpy.addStatement(RDF.TYPE, RDF.TYPE, RDFS.RESOURCE, RDF4J.SHACL_SHAPE_GRAPH); connectionSpy.commit(); - verify(connectionSpy, never()).prepareValidation(new ValidationSettings()); + verify(connectionSpy, never()).prepareValidation(Mockito.any(), Mockito.anyBoolean()); } shaclSail.shutDown(); diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/RdfsReasoningShaclSailTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/RdfsReasoningShaclSailTest.java new file mode 100644 index 00000000000..f0f978c3fda --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/RdfsReasoningShaclSailTest.java @@ -0,0 +1,628 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.repository.RepositoryConnection; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.NotifyingSailConnection; +import org.eclipse.rdf4j.sail.SailConnectionListener; +import org.eclipse.rdf4j.sail.SailException; +import org.eclipse.rdf4j.sail.helpers.NotifyingSailWrapper; +import org.eclipse.rdf4j.sail.inferencer.InferencerConnection; +import org.eclipse.rdf4j.sail.inferencer.InferencerConnectionWrapper; +import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; +import org.junit.jupiter.api.Test; + +public class RdfsReasoningShaclSailTest { + + @Test + public void inversePathClassConstraintFailsWithoutIncludeInferredStatements() throws Exception { + SailRepository repo = createRepository(false, false); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, trainShapeTurtle(null, null), RDF4J.SHACL_SHAPE_GRAPH); + loadTurtle(repo, dataTurtle(), dataGraph); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.prepareUpdate(QueryLanguage.SPARQL, insertUpdate()).execute(); + assertThrows(ShaclSailValidationException.class, () -> commitAndRethrow(conn)); + } finally { + repo.shutDown(); + } + } + + @Test + public void inversePathClassConstraintPassesWithIncludeInferredStatements() throws Exception { + SailRepository repo = createRepository(true, false); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, trainShapeTurtle(null, null), RDF4J.SHACL_SHAPE_GRAPH); + loadTurtle(repo, dataTurtle(), dataGraph); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.prepareUpdate(QueryLanguage.SPARQL, insertUpdate()).execute(); + assertDoesNotThrow(() -> commitAndRethrow(conn)); + } finally { + repo.shutDown(); + } + } + + @Test + public void inversePathClassConstraintPassesWithPerShapeIncludeInferredStatements() throws Exception { + SailRepository repo = createRepository(false, false); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, trainShapeTurtle(null, true), RDF4J.SHACL_SHAPE_GRAPH); + loadTurtle(repo, dataTurtle(), dataGraph); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.prepareUpdate(QueryLanguage.SPARQL, insertUpdate()).execute(); + assertDoesNotThrow(() -> commitAndRethrow(conn)); + } finally { + repo.shutDown(); + } + } + + @Test + public void inferredTargetClassChangeIsDetectedWhenInferredStatementsReported() throws Exception { + SailRepository repo = createRepository(true, false); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, railcarBrakeShapeTurtle(null, null), RDF4J.SHACL_SHAPE_GRAPH); + + IRI wagon1 = repo.getValueFactory().createIRI("urn:wagon1"); + IRI freightWagon = repo.getValueFactory().createIRI("https://example.com/trains/FreightWagon"); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.add(wagon1, RDF.TYPE, freightWagon, dataGraph); + assertThrows(ShaclSailValidationException.class, () -> commitAndRethrow(conn)); + } finally { + repo.shutDown(); + } + } + + @Test + public void inferredOnlyNotificationsStillTriggerValidation() throws Exception { + SailRepository repo = createRepositoryWithExplicitStatementFiltering(true); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, railcarBrakeShapeTurtle(null, null), RDF4J.SHACL_SHAPE_GRAPH); + + IRI wagon1 = repo.getValueFactory().createIRI("urn:wagon1"); + IRI freightWagon = repo.getValueFactory().createIRI("https://example.com/trains/FreightWagon"); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.add(wagon1, RDF.TYPE, freightWagon, dataGraph); + assertThrows(ShaclSailValidationException.class, () -> commitAndRethrow(conn)); + } finally { + repo.shutDown(); + } + } + + @Test + public void targetClassUsesPerShapeRdfsSubClassReasoning() throws Exception { + SailRepository repo = createRepository(false, false, false); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, railcarBrakeShapeTurtle(true, null), RDF4J.SHACL_SHAPE_GRAPH); + + IRI wagon1 = repo.getValueFactory().createIRI("urn:wagon1"); + IRI freightWagon = repo.getValueFactory().createIRI("https://example.com/trains/FreightWagon"); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.add(wagon1, RDF.TYPE, freightWagon, dataGraph); + assertThrows(ShaclSailValidationException.class, () -> commitAndRethrow(conn)); + } finally { + repo.shutDown(); + } + } + + @Test + public void inferredTargetClassChangeIsMissedWhenInferredStatementsNotReported() throws Exception { + SailRepository repo = createRepository(true, true); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, railcarBrakeShapeTurtle(null, null), RDF4J.SHACL_SHAPE_GRAPH); + + IRI wagon1 = repo.getValueFactory().createIRI("urn:wagon1"); + IRI freightWagon = repo.getValueFactory().createIRI("https://example.com/trains/FreightWagon"); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.add(wagon1, RDF.TYPE, freightWagon, dataGraph); + assertDoesNotThrow(() -> commitAndRethrow(conn)); + } + + try (SailRepositoryConnection conn = repo.getConnection()) { + conn.begin(); + ValidationReport report = ((ShaclSailConnection) conn.getSailConnection()).revalidate(); + assertFalse(report.conforms()); + conn.rollback(); + } finally { + repo.shutDown(); + } + } + + @Test + public void legacyCallbacksWithoutInferenceMetadataFailWhenAShapeDisablesInferredStatements() + throws Exception { + SailRepository repo = createRepositoryWithLegacyCallbackForwarding(true); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, railcarBrakeShapeTurtle(null, false), RDF4J.SHACL_SHAPE_GRAPH); + + IRI wagon1 = repo.getValueFactory().createIRI("urn:wagon1"); + IRI freightWagon = repo.getValueFactory().createIRI("https://example.com/trains/FreightWagon"); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.add(wagon1, RDF.TYPE, freightWagon, dataGraph); + ShaclSailValidationException exception = assertThrows(ShaclSailValidationException.class, + () -> commitAndRethrow(conn)); + assertTrue(exception.getMessage().contains("does not support shapes that explicitly set")); + assertTrue(exception.getMessage().contains("statementAdded(Statement, boolean inferred)")); + assertTrue(exception.getMessage().contains("statementRemoved(Statement, boolean inferred)")); + } finally { + repo.shutDown(); + } + } + + @Test + public void legacyCallbacksWithoutInferenceMetadataFailWhenGlobalIncludeInferredStatementsIsDisabled() + throws Exception { + SailRepository repo = createRepositoryWithLegacyCallbackForwarding(false); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, railcarBrakeShapeTurtle(null, null), RDF4J.SHACL_SHAPE_GRAPH); + + IRI wagon1 = repo.getValueFactory().createIRI("urn:wagon1"); + IRI freightWagon = repo.getValueFactory().createIRI("https://example.com/trains/FreightWagon"); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + conn.add(wagon1, RDF.TYPE, freightWagon, dataGraph); + ShaclSailValidationException exception = assertThrows(ShaclSailValidationException.class, + () -> commitAndRethrow(conn)); + assertTrue(exception.getMessage() + .contains("deprecated SailConnectionListener callbacks without inferred flags")); + assertTrue(exception.getMessage().contains("statementAdded(Statement, boolean inferred)")); + assertTrue(exception.getMessage().contains("statementRemoved(Statement, boolean inferred)")); + } finally { + repo.shutDown(); + } + } + + @Test + public void legacyCallbacksWithoutInferenceMetadataAreAcceptedWhenAllShapesIncludeInferredStatements() + throws Exception { + SailRepository repo = createRepositoryWithLegacyCallbackForwarding(true); + + IRI ontologyGraph = repo.getValueFactory().createIRI("urn:ontology"); + IRI dataGraph = repo.getValueFactory().createIRI("urn:data"); + + loadTurtle(repo, ontologyTurtle(), ontologyGraph); + loadTurtle(repo, railcarBrakeShapeTurtle(null, null), RDF4J.SHACL_SHAPE_GRAPH); + + IRI wagon1 = repo.getValueFactory().createIRI("urn:wagon1"); + IRI freightWagon = repo.getValueFactory().createIRI("https://example.com/trains/FreightWagon"); + + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(); + assertDoesNotThrow(() -> conn.add(wagon1, RDF.TYPE, freightWagon, dataGraph)); + conn.rollback(); + } finally { + repo.shutDown(); + } + } + + private static SailRepository createRepository(boolean includeInferredStatements, + boolean filterInferredNotifications) { + return createRepository(includeInferredStatements, filterInferredNotifications, true); + } + + private static SailRepository createRepositoryWithLegacyCallbackForwarding(boolean includeInferredStatements) { + NotifyingSail baseSail = new LegacyCallbackForwardingSail(new MemoryStore()); + NotifyingSail shaclBase = new SchemaCachingRDFSInferencer(baseSail); + ShaclSail shaclSail = new ShaclSail(shaclBase); + shaclSail.setRdfsSubClassReasoning(false); + shaclSail.setIncludeInferredStatements(includeInferredStatements); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setSerializableValidation(false); + + SailRepository repo = new SailRepository(shaclSail); + repo.init(); + return repo; + } + + private static SailRepository createRepositoryWithExplicitStatementFiltering(boolean includeInferredStatements) { + NotifyingSail baseSail = new ExplicitStatementFilteringSail(new MemoryStore()); + NotifyingSail shaclBase = new SchemaCachingRDFSInferencer(baseSail); + ShaclSail shaclSail = new ShaclSail(shaclBase); + shaclSail.setRdfsSubClassReasoning(false); + shaclSail.setIncludeInferredStatements(includeInferredStatements); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setSerializableValidation(false); + + SailRepository repo = new SailRepository(shaclSail); + repo.init(); + return repo; + } + + private static SailRepository createRepository(boolean includeInferredStatements, + boolean filterInferredNotifications, + boolean useSchemaCachingInferencer) { + NotifyingSail baseSail = new MemoryStore(); + if (filterInferredNotifications) { + baseSail = new InferredStatementFilteringSail(baseSail); + } + NotifyingSail shaclBase = baseSail; + if (useSchemaCachingInferencer) { + shaclBase = new SchemaCachingRDFSInferencer(baseSail); + } + ShaclSail shaclSail = new ShaclSail(shaclBase); + shaclSail.setRdfsSubClassReasoning(false); + shaclSail.setIncludeInferredStatements(includeInferredStatements); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setSerializableValidation(false); + + SailRepository repo = new SailRepository(shaclSail); + repo.init(); + return repo; + } + + private static void loadTurtle(SailRepository repo, String resource, IRI context) throws IOException { + try (RepositoryConnection conn = repo.getConnection()) { + conn.begin(IsolationLevels.NONE, ShaclSail.TransactionSettings.ValidationApproach.Disabled); + conn.add(new StringReader(resource), "", RDFFormat.TURTLE, context); + conn.commit(); + } + } + + private static void commitAndRethrow(RepositoryConnection conn) throws Throwable { + try { + conn.commit(); + } catch (RepositoryException e) { + conn.rollback(); + throw e.getCause() == null ? e : e.getCause(); + } + } + + private static String ontologyTurtle() { + return String.join("\n", + "@prefix tr: .", + "@prefix owl: .", + "@prefix rdfs: .", + "", + "tr:Train a owl:Class .", + "tr:Railcar a owl:Class .", + "tr:FreightWagon a owl:Class ;", + " rdfs:subClassOf tr:Railcar .", + "tr:hasRailcar a owl:ObjectProperty .", + "tr:hasBrake a owl:ObjectProperty .", + "tr:isRailcarOf owl:inverseOf tr:hasRailcar .", + "" + ); + } + + private static String trainShapeTurtle(Boolean rdfsSubClassReasoning, Boolean includeInferredStatements) { + List lines = new ArrayList<>(); + lines.add("@prefix tr: ."); + lines.add("@prefix owl: ."); + lines.add("@prefix sh: ."); + if (rdfsSubClassReasoning != null || includeInferredStatements != null) { + lines.add("@prefix rsx: ."); + } + lines.add(""); + lines.add("tr:TrainShape a owl:NamedIndividual, sh:NodeShape ;"); + lines.add(" sh:property ["); + lines.add(" sh:class tr:Railcar ;"); + lines.add(" sh:path [ sh:inversePath tr:isRailcarOf ]"); + lines.add(" ] ;"); + if (rdfsSubClassReasoning != null) { + lines.add(" rsx:rdfsSubClassReasoning " + rdfsSubClassReasoning + " ;"); + } + if (includeInferredStatements != null) { + lines.add(" rsx:includeInferredStatements " + includeInferredStatements + " ;"); + } + lines.add(" sh:targetClass tr:Train ."); + lines.add(""); + return String.join("\n", lines); + } + + private static String railcarBrakeShapeTurtle(Boolean rdfsSubClassReasoning, Boolean includeInferredStatements) { + List lines = new ArrayList<>(); + lines.add("@prefix tr: ."); + lines.add("@prefix owl: ."); + lines.add("@prefix sh: ."); + if (rdfsSubClassReasoning != null || includeInferredStatements != null) { + lines.add("@prefix rsx: ."); + } + lines.add(""); + lines.add("tr:RailcarBrakeShape a owl:NamedIndividual, sh:NodeShape ;"); + lines.add(" sh:targetClass tr:Railcar ;"); + if (rdfsSubClassReasoning != null) { + lines.add(" rsx:rdfsSubClassReasoning " + rdfsSubClassReasoning + " ;"); + } + if (includeInferredStatements != null) { + lines.add(" rsx:includeInferredStatements " + includeInferredStatements + " ;"); + } + lines.add(" sh:property ["); + lines.add(" sh:path tr:hasBrake ;"); + lines.add(" sh:minCount 1"); + lines.add(" ] ."); + lines.add(""); + return String.join("\n", lines); + } + + private static String dataTurtle() { + return String.join("\n", + "@prefix tr: .", + "", + " a tr:Train .", + " a tr:FreightWagon .", + "" + ); + } + + private static String insertUpdate() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " tr:isRailcarOf .", + " }", + "}", + "" + ); + } + + // Test helper: hide inferred change notifications from listeners. + private static final class InferredStatementFilteringSail extends NotifyingSailWrapper { + InferredStatementFilteringSail(NotifyingSail baseSail) { + super(baseSail); + } + + @Override + public NotifyingSailConnection getConnection() throws SailException { + InferencerConnection connection = (InferencerConnection) super.getConnection(); + return new InferredStatementFilteringConnection(connection); + } + } + + private static final class InferredStatementFilteringConnection extends InferencerConnectionWrapper { + private final Map listenerMap = new IdentityHashMap<>(); + + InferredStatementFilteringConnection(InferencerConnection wrappedCon) { + super(wrappedCon); + } + + @Override + public void addConnectionListener(SailConnectionListener listener) { + SailConnectionListener filteringListener = new SailConnectionListener() { + @Override + public void statementAdded(Statement st) { + statementAdded(st, false); + } + + @Override + public void statementRemoved(Statement st) { + statementRemoved(st, false); + } + + @Override + public void statementAdded(Statement st, boolean inferred) { + if (!inferred) { + listener.statementAdded(st, false); + } + } + + @Override + public void statementRemoved(Statement st, boolean inferred) { + if (!inferred) { + listener.statementRemoved(st, false); + } + } + }; + listenerMap.put(listener, filteringListener); + super.addConnectionListener(filteringListener); + } + + @Override + public void removeConnectionListener(SailConnectionListener listener) { + SailConnectionListener filteringListener = listenerMap.remove(listener); + if (filteringListener != null) { + super.removeConnectionListener(filteringListener); + } else { + super.removeConnectionListener(listener); + } + } + } + + // Test helper: hide explicit change notifications from listeners. + private static final class ExplicitStatementFilteringSail extends NotifyingSailWrapper { + ExplicitStatementFilteringSail(NotifyingSail baseSail) { + super(baseSail); + } + + @Override + public NotifyingSailConnection getConnection() throws SailException { + InferencerConnection connection = (InferencerConnection) super.getConnection(); + return new ExplicitStatementFilteringConnection(connection); + } + } + + private static final class ExplicitStatementFilteringConnection extends InferencerConnectionWrapper { + private final Map listenerMap = new IdentityHashMap<>(); + + ExplicitStatementFilteringConnection(InferencerConnection wrappedCon) { + super(wrappedCon); + } + + @Override + public void addConnectionListener(SailConnectionListener listener) { + SailConnectionListener filteringListener = new SailConnectionListener() { + @Override + public void statementAdded(Statement st) { + statementAdded(st, false); + } + + @Override + public void statementRemoved(Statement st) { + statementRemoved(st, false); + } + + @Override + public void statementAdded(Statement st, boolean inferred) { + if (inferred) { + listener.statementAdded(st, true); + } + } + + @Override + public void statementRemoved(Statement st, boolean inferred) { + if (inferred) { + listener.statementRemoved(st, true); + } + } + }; + listenerMap.put(listener, filteringListener); + super.addConnectionListener(filteringListener); + } + + @Override + public void removeConnectionListener(SailConnectionListener listener) { + SailConnectionListener filteringListener = listenerMap.remove(listener); + if (filteringListener != null) { + super.removeConnectionListener(filteringListener); + } else { + super.removeConnectionListener(listener); + } + } + } + + // Test helper: force all notifications through deprecated no-flag listener methods. + private static final class LegacyCallbackForwardingSail extends NotifyingSailWrapper { + LegacyCallbackForwardingSail(NotifyingSail baseSail) { + super(baseSail); + } + + @Override + public NotifyingSailConnection getConnection() throws SailException { + InferencerConnection connection = (InferencerConnection) super.getConnection(); + return new LegacyCallbackForwardingConnection(connection); + } + } + + private static final class LegacyCallbackForwardingConnection extends InferencerConnectionWrapper { + private final Map listenerMap = new IdentityHashMap<>(); + + LegacyCallbackForwardingConnection(InferencerConnection wrappedCon) { + super(wrappedCon); + } + + @Override + public void addConnectionListener(SailConnectionListener listener) { + SailConnectionListener legacyForwardingListener = new SailConnectionListener() { + @Override + public void statementAdded(Statement st) { + listener.statementAdded(st); + } + + @Override + public void statementRemoved(Statement st) { + listener.statementRemoved(st); + } + + @Override + public void statementAdded(Statement st, boolean inferred) { + listener.statementAdded(st); + } + + @Override + public void statementRemoved(Statement st, boolean inferred) { + listener.statementRemoved(st); + } + }; + listenerMap.put(listener, legacyForwardingListener); + super.addConnectionListener(legacyForwardingListener); + } + + @Override + public void removeConnectionListener(SailConnectionListener listener) { + SailConnectionListener filteringListener = listenerMap.remove(listener); + if (filteringListener != null) { + super.removeConnectionListener(filteringListener); + } else { + super.removeConnectionListener(listener); + } + } + } + +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/RdfsShaclConnectionTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/RdfsShaclConnectionTest.java index 39dc1780a59..352f06efad7 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/RdfsShaclConnectionTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/RdfsShaclConnectionTest.java @@ -60,6 +60,27 @@ public void testHasStatement() { } + @Test + public void testHasStatementWithoutInferredKeepsRdfsReasoning() { + + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + shaclSail.init(); + + fill(shaclSail); + + try (NotifyingSailConnection connection = shaclSail.getConnection()) { + ((ShaclSailConnection) connection).rdfsSubClassOfReasoner = RdfsSubClassOfReasoner + .createReasoner((ShaclSailConnection) connection, new ValidationSettings()); + VerySimpleRdfsBackwardsChainingConnection connection2 = new VerySimpleRdfsBackwardsChainingConnection( + connection, + ((ShaclSailConnection) connection).getRdfsSubClassOfReasoner()); + + Assertions.assertTrue(connection2.hasStatement(aSubSub, RDF.TYPE, sup, false)); + } + shaclSail.shutDown(); + + } + @Test public void testGetStatement() { @@ -102,6 +123,34 @@ public void testGetStatement() { } + @Test + public void testGetStatementWithoutInferredKeepsRdfsReasoning() { + + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + shaclSail.init(); + + fill(shaclSail); + + try (NotifyingSailConnection connection = shaclSail.getConnection()) { + ((ShaclSailConnection) connection).rdfsSubClassOfReasoner = RdfsSubClassOfReasoner + .createReasoner((ShaclSailConnection) connection, new ValidationSettings()); + + VerySimpleRdfsBackwardsChainingConnection connection2 = new VerySimpleRdfsBackwardsChainingConnection( + connection, + ((ShaclSailConnection) connection).getRdfsSubClassOfReasoner()); + + try (Stream stream = connection2.getStatements(aSubSub, RDF.TYPE, sup, false) + .stream()) { + Set collect = stream.collect(Collectors.toSet()); + Set expected = Set.of(vf.createStatement(aSubSub, RDF.TYPE, sup)); + Assertions.assertEquals(expected, collect); + } + } + + shaclSail.shutDown(); + + } + @Test public void testGetStatementNoDuplicates() { diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ReduceNumberOfPlansTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ReduceNumberOfPlansTest.java index b05a1f668a8..91505db02db 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ReduceNumberOfPlansTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ReduceNumberOfPlansTest.java @@ -37,6 +37,7 @@ public void testAddingTypeStatement() throws RDFParseException, UnsupportedRDFormatException, IOException, InterruptedException { ShaclSail shaclSail = new ShaclSail(new MemoryStore()); shaclSail.init(); + boolean requiresRdfsSubClassReasoner = shaclSail.isRdfsSubClassReasoning(); Utils.loadShapeData(shaclSail, "reduceNumberOfPlansTest/shacl.trig"); addDummyData(shaclSail); @@ -44,7 +45,7 @@ public void testAddingTypeStatement() try (ShaclSailConnection connection = (ShaclSailConnection) shaclSail.getConnection()) { connection.begin(); - connection.prepareValidation(new ValidationSettings()); + connection.prepareValidation(new ValidationSettings(), requiresRdfsSubClassReasoner); try (ConnectionsGroup connectionsGroup = connection.getConnectionsGroup()) { @@ -61,7 +62,7 @@ public void testAddingTypeStatement() IRI person1 = Utils.Ex.createIri(); connection.addStatement(person1, RDF.TYPE, Utils.Ex.Person); - connection.prepareValidation(new ValidationSettings()); + connection.prepareValidation(new ValidationSettings(), requiresRdfsSubClassReasoner); try (ConnectionsGroup connectionsGroup = connection.getConnectionsGroup()) { @@ -93,6 +94,7 @@ public void testRemovingPredicate() throws RDF4JException, UnsupportedRDFormatException, IOException, InterruptedException { ShaclSail shaclSail = new ShaclSail(new MemoryStore()); shaclSail.init(); + boolean requiresRdfsSubClassReasoner = shaclSail.isRdfsSubClassReasoning(); Utils.loadShapeData(shaclSail, "reduceNumberOfPlansTest/shacl.trig"); addDummyData(shaclSail); @@ -114,7 +116,7 @@ public void testRemovingPredicate() connection.removeStatements(person1, Utils.Ex.ssn, vf.createLiteral("b")); - connection.prepareValidation(new ValidationSettings()); + connection.prepareValidation(new ValidationSettings(), requiresRdfsSubClassReasoner); try (ConnectionsGroup connectionsGroup = connection.getConnectionsGroup()) { @@ -131,7 +133,7 @@ public void testRemovingPredicate() connection.removeStatements(person1, Utils.Ex.ssn, vf.createLiteral("a")); - connection.prepareValidation(new ValidationSettings()); + connection.prepareValidation(new ValidationSettings(), requiresRdfsSubClassReasoner); try (ConnectionsGroup connectionsGroup = connection.getConnectionsGroup()) { @@ -147,7 +149,7 @@ public void testRemovingPredicate() } connection.removeStatements(person1, Utils.Ex.name, vf.createLiteral("c")); - connection.prepareValidation(new ValidationSettings()); + connection.prepareValidation(new ValidationSettings(), requiresRdfsSubClassReasoner); try (ConnectionsGroup connectionsGroup = connection.getConnectionsGroup()) { diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclIncludeInferredCombinationsTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclIncludeInferredCombinationsTest.java new file mode 100644 index 00000000000..b03c9eaad9b --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclIncludeInferredCombinationsTest.java @@ -0,0 +1,227 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.rdf4j.common.transaction.QueryEvaluationMode; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class ShaclIncludeInferredCombinationsTest extends AbstractShaclReasoningCombinationTest { + + static Stream reasoningCases() { + List cases = List.of( + new ReasoningCase("include-target-class-inferred-type", + includeTargetClassShapes(), includeTargetClassData(), + includeTargetClassUpdate1(), includeTargetClassUpdate2(), + true, false), + new ReasoningCase("include-target-subjects-of-inferred", + includeTargetSubjectsShapes(), includeTargetSubjectsData(), + includeTargetSubjectsUpdate1(), includeTargetSubjectsUpdate2(), + false, true), + new ReasoningCase("include-target-objects-of-inferred", + includeTargetObjectsShapes(), includeTargetObjectsData(), + includeTargetObjectsUpdate1(), includeTargetObjectsUpdate2(), + false, true) + ); + + return cases.stream() + .flatMap(testCase -> Stream.of( + Arguments.of(testCase, false), + Arguments.of(testCase, true) + )); + } + + @ParameterizedTest(name = "{0} enabled={1}") + @MethodSource("reasoningCases") + void includeInferredCombinations(ReasoningCase testCase, boolean enabled) { + runAllModes(testCase, enabled, false, enabled); + } + + @Override + protected NotifyingSail createDataSail() { + MemoryStore memoryStore = new MemoryStore(); + memoryStore.setDefaultQueryEvaluationMode(QueryEvaluationMode.STRICT); + return new SchemaCachingRDFSInferencer(memoryStore, false); + } + + @Override + protected String ontologyTurtle() { + return String.join("\n", + "@prefix tr: .", + "@prefix owl: .", + "@prefix rdfs: .", + "", + "tr:Train a owl:Class .", + "tr:Railcar a owl:Class .", + "tr:FreightWagon a owl:Class ;", + " rdfs:subClassOf tr:Railcar .", + "tr:hasRailcar a owl:ObjectProperty ;", + " rdfs:subPropertyOf tr:relatedRailcar .", + "tr:relatedRailcar a owl:ObjectProperty .", + "tr:isRailcarOf a owl:ObjectProperty .", + "tr:hasBrake a owl:ObjectProperty .", + "" + ); + } + + private static String includeTargetClassShapes() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "", + "tr:TrainRailcarShape a sh:NodeShape ;", + " sh:targetClass tr:Train ;", + " sh:property [", + " sh:path [ sh:inversePath tr:isRailcarOf ] ;", + " sh:class tr:Railcar ;", + " sh:minCount 1", + " ] .", + "" + ); + } + + private static String includeTargetClassData() { + return String.join("\n", + "@prefix tr: .", + "", + " a tr:Train .", + " a tr:FreightWagon ;", + " tr:isRailcarOf .", + "" + ); + } + + private static String includeTargetClassUpdate1() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:Train .", + " }", + "}" + ); + } + + private static String includeTargetClassUpdate2() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:FreightWagon ;", + " tr:isRailcarOf .", + " }", + "}" + ); + } + + private static String includeTargetSubjectsShapes() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "", + "tr:RailcarBrakeShape a sh:NodeShape ;", + " sh:targetSubjectsOf tr:relatedRailcar ;", + " sh:property [", + " sh:path tr:hasBrake ;", + " sh:minCount 1", + " ] .", + "" + ); + } + + private static String includeTargetSubjectsData() { + return String.join("\n", + "@prefix tr: .", + "", + " tr:hasRailcar .", + " a tr:FreightWagon .", + "" + ); + } + + private static String includeTargetSubjectsUpdate1() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " tr:hasRailcar .", + " }", + "}" + ); + } + + private static String includeTargetSubjectsUpdate2() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:FreightWagon .", + " }", + "}" + ); + } + + private static String includeTargetObjectsShapes() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "", + "tr:RailcarBrakeShape a sh:NodeShape ;", + " sh:targetObjectsOf tr:relatedRailcar ;", + " sh:property [", + " sh:path tr:hasBrake ;", + " sh:minCount 1", + " ] .", + "" + ); + } + + private static String includeTargetObjectsData() { + return String.join("\n", + "@prefix tr: .", + "", + " tr:hasRailcar .", + " a tr:FreightWagon .", + "" + ); + } + + private static String includeTargetObjectsUpdate1() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " tr:hasRailcar .", + " }", + "}" + ); + } + + private static String includeTargetObjectsUpdate2() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:FreightWagon .", + " }", + "}" + ); + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningCombinationsTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningCombinationsTest.java new file mode 100644 index 00000000000..86b9e6824dc --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningCombinationsTest.java @@ -0,0 +1,205 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import java.util.List; +import java.util.stream.Stream; + +import org.eclipse.rdf4j.common.transaction.QueryEvaluationMode; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class ShaclReasoningCombinationsTest extends AbstractShaclReasoningCombinationTest { + + static Stream reasoningCases() { + List cases = List.of( + new ReasoningCase("rdfs-target-class-mincount", + rdfsTargetClassShapes(), rdfsTargetClassData(), + rdfsTargetClassUpdate1(), rdfsTargetClassUpdate2(), + false, true), + new ReasoningCase("rdfs-target-subjects-of-class", + rdfsTargetSubjectsShapes(), rdfsTargetSubjectsData(), + rdfsTargetSubjectsUpdate1(), rdfsTargetSubjectsUpdate2(), + true, false), + new ReasoningCase("rdfs-target-objects-of-class", + rdfsTargetObjectsShapes(), rdfsTargetObjectsData(), + rdfsTargetObjectsUpdate1(), rdfsTargetObjectsUpdate2(), + true, false) + ); + + return cases.stream() + .flatMap(testCase -> Stream.of( + Arguments.of(testCase, false), + Arguments.of(testCase, true) + )); + } + + @ParameterizedTest(name = "{0} enabled={1}") + @MethodSource("reasoningCases") + void reasoningCombinations(ReasoningCase testCase, boolean enabled) { + runAllModes(testCase, enabled, enabled, true); + } + + @Override + protected NotifyingSail createDataSail() { + MemoryStore memoryStore = new MemoryStore(); + memoryStore.setDefaultQueryEvaluationMode(QueryEvaluationMode.STRICT); + return memoryStore; + } + + @Override + protected String ontologyTurtle() { + return String.join("\n", + "@prefix tr: .", + "@prefix owl: .", + "@prefix rdfs: .", + "", + "tr:Train a owl:Class .", + "tr:Railcar a owl:Class .", + "tr:FreightWagon a owl:Class ;", + " rdfs:subClassOf tr:Railcar .", + "tr:hasRailcar a owl:ObjectProperty .", + "tr:hasBrake a owl:ObjectProperty .", + "" + ); + } + + private static String rdfsTargetClassShapes() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "", + "tr:RailcarBrakeShape a sh:NodeShape ;", + " sh:targetClass tr:Railcar ;", + " sh:property [", + " sh:path tr:hasBrake ;", + " sh:minCount 1", + " ] .", + "" + ); + } + + private static String rdfsTargetClassData() { + return String.join("\n", + "@prefix tr: .", + "", + " a tr:Train ;", + " tr:hasRailcar , .", + " a tr:FreightWagon ;", + " tr:hasBrake .", + " a tr:FreightWagon .", + "" + ); + } + + private static String rdfsTargetClassUpdate1() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:Train ;", + " tr:hasRailcar .", + " a tr:FreightWagon ;", + " tr:hasBrake .", + " }", + "}" + ); + } + + private static String rdfsTargetClassUpdate2() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " tr:hasRailcar .", + " a tr:FreightWagon .", + " }", + "}" + ); + } + + private static String rdfsTargetSubjectsShapes() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "", + "tr:TrainRailcarClassShape a sh:NodeShape ;", + " sh:targetSubjectsOf tr:hasRailcar ;", + " sh:property [", + " sh:path tr:hasRailcar ;", + " sh:class tr:Railcar ;", + " sh:minCount 1", + " ] .", + "" + ); + } + + private static String rdfsTargetSubjectsData() { + return String.join("\n", + "@prefix tr: .", + "", + " tr:hasRailcar .", + " a tr:FreightWagon .", + "" + ); + } + + private static String rdfsTargetSubjectsUpdate1() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " tr:hasRailcar .", + " }", + "}" + ); + } + + private static String rdfsTargetSubjectsUpdate2() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:FreightWagon .", + " }", + "}" + ); + } + + private static String rdfsTargetObjectsShapes() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "", + "tr:RailcarClassShape a sh:NodeShape ;", + " sh:targetObjectsOf tr:hasRailcar ;", + " sh:class tr:Railcar .", + "" + ); + } + + private static String rdfsTargetObjectsData() { + return rdfsTargetSubjectsData(); + } + + private static String rdfsTargetObjectsUpdate1() { + return rdfsTargetSubjectsUpdate1(); + } + + private static String rdfsTargetObjectsUpdate2() { + return rdfsTargetSubjectsUpdate2(); + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningDefaultsTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningDefaultsTest.java new file mode 100644 index 00000000000..abbe4113e09 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningDefaultsTest.java @@ -0,0 +1,291 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import java.io.StringReader; +import java.util.Set; + +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.common.transaction.QueryEvaluationMode; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.util.Values; +import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.repository.RepositoryConnection; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ShaclReasoningDefaultsTest { + + private static final IRI DATA_GRAPH = Values.iri("urn:data"); + private static final IRI ONTOLOGY_GRAPH = Values.iri("urn:ontology"); + + @Test + void defaultsEnableRdfsSubClassReasoningInShaclSail() { + SailRepository repository = new SailRepository(createShaclSail(new MemoryStore())); + try { + repository.init(); + loadShapes(repository, rdfsShapesTurtle()); + loadOntology(repository); + + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE); + addTurtle(connection, rdfsDataTurtle(), DATA_GRAPH); + commitExpectingViolation(connection, "default rdfs reasoning should flag missing brake"); + } + } finally { + repository.shutDown(); + } + } + + @Test + void defaultsEnableRdfsSubClassReasoningInShaclValidator() { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + SailRepository dataRepo = new SailRepository(new MemoryStore()); + try { + shapesRepo.init(); + dataRepo.init(); + + loadShapes(shapesRepo, rdfsShapesTurtle()); + loadOntology(dataRepo); + try (RepositoryConnection connection = dataRepo.getConnection()) { + connection.begin(IsolationLevels.NONE); + addTurtle(connection, rdfsDataTurtle(), DATA_GRAPH); + connection.commit(); + } + + ValidationReport report = ShaclValidator.builder() + .setEclipseRdf4jShaclExtensions(true) + .setCacheSelectNodes(true) + .setParallelValidation(false) + .withShapes(shapesRepo.getSail()) + .build() + .validate(dataRepo.getSail()); + + Assertions.assertFalse(report.conforms(), "default rdfs reasoning should detect violation"); + } finally { + try { + shapesRepo.shutDown(); + } finally { + dataRepo.shutDown(); + } + } + } + + @Test + void defaultsEnableIncludeInferredInShaclSail() { + SailRepository repository = new SailRepository(createShaclSail(createInferencingSail())); + try { + repository.init(); + loadShapes(repository, includeShapesTurtle()); + loadOntology(repository); + + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE); + connection.prepareUpdate(QueryLanguage.SPARQL, includeDataUpdate()).execute(); + commitExpectingConformance(connection, "default includeInferred should accept inferred type"); + } + } finally { + repository.shutDown(); + } + } + + @Test + void defaultsEnableIncludeInferredInShaclValidator() { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + SailRepository dataRepo = new SailRepository(createInferencingSail()); + try { + shapesRepo.init(); + dataRepo.init(); + + loadShapes(shapesRepo, includeShapesTurtle()); + loadOntology(dataRepo); + try (RepositoryConnection connection = dataRepo.getConnection()) { + connection.begin(IsolationLevels.NONE); + connection.prepareUpdate(QueryLanguage.SPARQL, includeDataUpdate()).execute(); + connection.commit(); + } + + ValidationReport report = ShaclValidator.builder() + .setEclipseRdf4jShaclExtensions(true) + .setCacheSelectNodes(true) + .setParallelValidation(false) + .withShapes(shapesRepo.getSail()) + .build() + .validate(dataRepo.getSail()); + + Assertions.assertTrue(report.conforms(), "default includeInferred should permit inferred type"); + } finally { + try { + shapesRepo.shutDown(); + } finally { + dataRepo.shutDown(); + } + } + } + + private static ShaclSail createShaclSail(NotifyingSail baseSail) { + ShaclSail shaclSail = new ShaclSail(baseSail); + shaclSail.setLogValidationPlans(false); + shaclSail.setCacheSelectNodes(true); + shaclSail.setParallelValidation(false); + shaclSail.setLogValidationViolations(false); + shaclSail.setGlobalLogValidationExecution(false); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setDashDataShapes(false); + shaclSail.setPerformanceLogging(false); + shaclSail.setSerializableValidation(false); + shaclSail.setShapesGraphs(Set.of(RDF4J.SHACL_SHAPE_GRAPH)); + return shaclSail; + } + + private static NotifyingSail createInferencingSail() { + MemoryStore memoryStore = new MemoryStore(); + memoryStore.setDefaultQueryEvaluationMode(QueryEvaluationMode.STRICT); + return new SchemaCachingRDFSInferencer(memoryStore, false); + } + + private static void loadShapes(SailRepository repository, String turtle) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ShaclSail.TransactionSettings.ValidationApproach.Disabled); + addTurtle(connection, turtle, RDF4J.SHACL_SHAPE_GRAPH); + connection.commit(); + } + } + + private static void loadOntology(SailRepository repository) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ShaclSail.TransactionSettings.ValidationApproach.Disabled); + addTurtle(connection, ontologyTurtle(), ONTOLOGY_GRAPH); + connection.commit(); + } + } + + private static void addTurtle(RepositoryConnection connection, String turtle, IRI context) { + try { + connection.add(new StringReader(turtle), "", RDFFormat.TURTLE, context); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + + private static void commitExpectingViolation(RepositoryConnection connection, String label) { + try { + connection.commit(); + Assertions.fail(label); + } catch (RepositoryException e) { + connection.rollback(); + Throwable cause = e.getCause(); + if (!(cause instanceof ShaclSailValidationException)) { + throw e; + } + } + } + + private static void commitExpectingConformance(RepositoryConnection connection, String label) { + try { + connection.commit(); + } catch (RepositoryException e) { + connection.rollback(); + Throwable cause = e.getCause(); + if (cause instanceof ShaclSailValidationException) { + Assertions.fail(label); + } + throw e; + } + } + + private static String rdfsShapesTurtle() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "", + "tr:RailcarBrakeShape a sh:NodeShape ;", + " sh:targetClass tr:Railcar ;", + " sh:property [", + " sh:path tr:hasBrake ;", + " sh:minCount 1", + " ] .", + "" + ); + } + + private static String includeShapesTurtle() { + return String.join("\n", + "@prefix tr: .", + "@prefix sh: .", + "@prefix rsx: .", + "", + "tr:TrainShape a sh:NodeShape ;", + " sh:targetClass tr:Train ;", + " rsx:rdfsSubClassReasoning false ;", + " sh:property [", + " sh:path [ sh:inversePath tr:isRailcarOf ] ;", + " sh:class tr:Railcar ;", + " sh:minCount 1", + " ] .", + "" + ); + } + + private static String ontologyTurtle() { + return String.join("\n", + "@prefix tr: .", + "@prefix owl: .", + "@prefix rdfs: .", + "", + "tr:Train a owl:Class .", + "tr:Railcar a owl:Class .", + "tr:FreightWagon a owl:Class ;", + " rdfs:subClassOf tr:Railcar .", + "tr:hasRailcar a owl:ObjectProperty .", + "tr:isRailcarOf a owl:ObjectProperty ;", + " owl:inverseOf tr:hasRailcar .", + "tr:hasBrake a owl:ObjectProperty .", + "" + ); + } + + private static String rdfsDataTurtle() { + return String.join("\n", + "@prefix tr: .", + "", + " a tr:Train ;", + " tr:hasRailcar , .", + " a tr:FreightWagon ;", + " tr:hasBrake .", + " a tr:FreightWagon .", + "" + ); + } + + private static String includeDataUpdate() { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:Train .", + " a tr:FreightWagon ;", + " tr:isRailcarOf .", + " }", + "}" + ); + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningSettingsTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningSettingsTest.java new file mode 100644 index 00000000000..abe634e0bce --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclReasoningSettingsTest.java @@ -0,0 +1,486 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import java.io.StringReader; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.common.transaction.QueryEvaluationMode; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.util.Values; +import org.eclipse.rdf4j.model.vocabulary.RDF4J; +import org.eclipse.rdf4j.query.QueryLanguage; +import org.eclipse.rdf4j.repository.RepositoryConnection; +import org.eclipse.rdf4j.repository.RepositoryException; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.rio.RDFFormat; +import org.eclipse.rdf4j.sail.NotifyingSail; +import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.ShaclSail.TransactionSettings.ValidationApproach; +import org.eclipse.rdf4j.sail.shacl.ast.ContextWithShape; +import org.eclipse.rdf4j.sail.shacl.ast.Shape; +import org.eclipse.rdf4j.sail.shacl.results.ValidationReport; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class ShaclReasoningSettingsTest { + + private static final IRI DATA_GRAPH = Values.iri("urn:data"); + private static final IRI ONTOLOGY_GRAPH = Values.iri("urn:ontology"); + private static final IRI RAILCAR_SHAPE = Values.iri("https://example.com/trains/RailcarBrakeShape"); + private static final IRI TRAIN_SHAPE = Values.iri("https://example.com/trains/TrainShape"); + + enum Scenario { + RDFS, + INCLUDE_INFERRED + } + + enum InferencerMode { + NONE, + SCHEMA_CACHING + } + + static final class ReasoningCase { + final String name; + final Scenario scenario; + final InferencerMode inferencerMode; + final boolean globalRdfs; + final boolean globalInclude; + final Boolean shapeRdfsOverride; + final Boolean shapeIncludeOverride; + + ReasoningCase(String name, Scenario scenario, InferencerMode inferencerMode, boolean globalRdfs, + boolean globalInclude, Boolean shapeRdfsOverride, Boolean shapeIncludeOverride) { + this.name = name; + this.scenario = scenario; + this.inferencerMode = inferencerMode; + this.globalRdfs = globalRdfs; + this.globalInclude = globalInclude; + this.shapeRdfsOverride = shapeRdfsOverride; + this.shapeIncludeOverride = shapeIncludeOverride; + } + + @Override + public String toString() { + return name; + } + } + + static Stream rdfsCases() { + List cases = List.of( + new ReasoningCase("rdfs-globals-off", Scenario.RDFS, InferencerMode.NONE, false, false, null, null), + new ReasoningCase("rdfs-globals-on", Scenario.RDFS, InferencerMode.NONE, true, false, null, null), + new ReasoningCase("rdfs-shape-on", Scenario.RDFS, InferencerMode.NONE, false, false, true, null), + new ReasoningCase("rdfs-override-off", Scenario.RDFS, InferencerMode.NONE, true, false, false, null) + ); + return cases.stream().map(Arguments::of); + } + + static Stream includeCases() { + List cases = List.of( + new ReasoningCase("include-globals-off", Scenario.INCLUDE_INFERRED, InferencerMode.SCHEMA_CACHING, + false, false, false, null), + new ReasoningCase("include-globals-on", Scenario.INCLUDE_INFERRED, InferencerMode.SCHEMA_CACHING, + false, true, false, null), + new ReasoningCase("include-shape-on", Scenario.INCLUDE_INFERRED, InferencerMode.SCHEMA_CACHING, + false, false, false, true), + new ReasoningCase("include-override-off", Scenario.INCLUDE_INFERRED, InferencerMode.SCHEMA_CACHING, + false, true, false, false) + ); + return cases.stream().map(Arguments::of); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("rdfsCases") + void rdfsSubClassReasoningAcrossValidationModes(ReasoningCase testCase) { + runAllModes(testCase); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("includeCases") + void includeInferredStatementsAcrossValidationModes(ReasoningCase testCase) { + runAllModes(testCase); + } + + private static void runAllModes(ReasoningCase testCase) { + assertSingleTransaction(testCase); + assertBulkValidation(testCase); + assertMultiUpdateTransaction(testCase); + assertShaclValidator(testCase); + } + + private static void assertSingleTransaction(ReasoningCase testCase) { + SailRepository repository = createRepository(testCase); + try { + assertShapeOverridesParsed(repository, testCase); + if (testCase.scenario == Scenario.INCLUDE_INFERRED) { + loadBaseDataWithoutValidation(repository, testCase); + } + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE); + if (testCase.scenario == Scenario.INCLUDE_INFERRED) { + connection.prepareUpdate(QueryLanguage.SPARQL, updatePart2(testCase)).execute(); + } else { + addTurtle(connection, dataTurtle(testCase), DATA_GRAPH); + } + commitExpecting(testCase, connection, "single transaction"); + } + } finally { + repository.shutDown(); + } + } + + private static void assertBulkValidation(ReasoningCase testCase) { + SailRepository repository = createRepository(testCase); + try { + loadDataWithoutValidation(repository, testCase); + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(ValidationApproach.Bulk); + try { + connection.commit(); + Assertions.assertTrue(expectedConforms(testCase), "bulk validation should have failed"); + } catch (RepositoryException e) { + connection.rollback(); + Throwable cause = e.getCause(); + if (!(cause instanceof ShaclSailValidationException)) { + throw e; + } + ValidationReport report = ((ShaclSailValidationException) cause).getValidationReport(); + Assertions.assertFalse(expectedConforms(testCase), "bulk validation unexpectedly failed"); + Assertions.assertFalse(report.conforms(), "bulk validation report should not conform"); + } + } + } finally { + repository.shutDown(); + } + } + + private static void assertMultiUpdateTransaction(ReasoningCase testCase) { + SailRepository repository = createRepository(testCase); + try { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE); + connection.prepareUpdate(QueryLanguage.SPARQL, updatePart1(testCase)).execute(); + connection.prepareUpdate(QueryLanguage.SPARQL, updatePart2(testCase)).execute(); + commitExpecting(testCase, connection, "multi-update transaction"); + } + } finally { + repository.shutDown(); + } + } + + private static void assertShaclValidator(ReasoningCase testCase) { + SailRepository shapesRepo = new SailRepository(new MemoryStore()); + SailRepository dataRepo = new SailRepository(createDataSail(testCase)); + try { + shapesRepo.init(); + dataRepo.init(); + loadShapes(shapesRepo, testCase); + loadOntology(dataRepo, testCase); + loadData(dataRepo, testCase); + + ValidationReport report = ShaclValidator.builder() + .setRdfsSubClassReasoning(testCase.globalRdfs) + .setIncludeInferredStatements(testCase.globalInclude) + .setEclipseRdf4jShaclExtensions(true) + .setCacheSelectNodes(true) + .setParallelValidation(false) + .withShapes(shapesRepo.getSail()) + .build() + .validate(dataRepo.getSail()); + + Assertions.assertEquals(expectedConforms(testCase), report.conforms(), "ShaclValidator result mismatch"); + } finally { + try { + shapesRepo.shutDown(); + } finally { + dataRepo.shutDown(); + } + } + } + + private static void assertShapeOverridesParsed(SailRepository repository, ReasoningCase testCase) { + ShaclSail shaclSail = (ShaclSail) repository.getSail(); + IRI shapeId = testCase.scenario == Scenario.RDFS ? RAILCAR_SHAPE : TRAIN_SHAPE; + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE); + List shapes = shaclSail.getShapes(connection, + new IRI[] { RDF4J.SHACL_SHAPE_GRAPH }); + Shape shape = shapes.stream() + .map(ContextWithShape::getShape) + .filter(candidate -> shapeId.equals(candidate.getId())) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing shape: " + shapeId)); + Assertions.assertEquals(testCase.shapeRdfsOverride, shape.getRdfsSubClassReasoningOverride(), + "Shape RDFS override parse mismatch"); + Assertions.assertEquals(testCase.shapeIncludeOverride, shape.getIncludeInferredStatementsOverride(), + "Shape include-inferred override parse mismatch"); + connection.commit(); + } + } + + private static void commitExpecting(ReasoningCase testCase, RepositoryConnection connection, String label) { + boolean expectedConforms = expectedConforms(testCase); + try { + connection.commit(); + Assertions.assertTrue(expectedConforms, label + " should have failed"); + } catch (RepositoryException e) { + connection.rollback(); + Throwable cause = e.getCause(); + if (!(cause instanceof ShaclSailValidationException)) { + throw e; + } + Assertions.assertFalse(expectedConforms, label + " should have conformed"); + } + } + + private static SailRepository createRepository(ReasoningCase testCase) { + SailRepository repository = new SailRepository(createShaclSail(testCase)); + repository.init(); + loadShapes(repository, testCase); + loadOntology(repository, testCase); + return repository; + } + + private static ShaclSail createShaclSail(ReasoningCase testCase) { + NotifyingSail baseSail = createDataSail(testCase); + ShaclSail shaclSail = new ShaclSail(baseSail); + shaclSail.setLogValidationPlans(false); + shaclSail.setCacheSelectNodes(true); + shaclSail.setParallelValidation(false); + shaclSail.setLogValidationViolations(false); + shaclSail.setGlobalLogValidationExecution(false); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setDashDataShapes(false); + shaclSail.setPerformanceLogging(false); + shaclSail.setRdfsSubClassReasoning(testCase.globalRdfs); + shaclSail.setIncludeInferredStatements(testCase.globalInclude); + shaclSail.setSerializableValidation(false); + shaclSail.setShapesGraphs(Set.of(RDF4J.SHACL_SHAPE_GRAPH)); + return shaclSail; + } + + private static NotifyingSail createDataSail(ReasoningCase testCase) { + MemoryStore memoryStore = new MemoryStore(); + memoryStore.setDefaultQueryEvaluationMode(QueryEvaluationMode.STRICT); + if (testCase.inferencerMode == InferencerMode.SCHEMA_CACHING) { + return new SchemaCachingRDFSInferencer(memoryStore, false); + } + return memoryStore; + } + + private static void loadShapes(SailRepository repository, ReasoningCase testCase) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, shapesTurtle(testCase), RDF4J.SHACL_SHAPE_GRAPH); + connection.commit(); + } + } + + private static void loadOntology(SailRepository repository, ReasoningCase testCase) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, ontologyTurtle(testCase), ONTOLOGY_GRAPH); + connection.commit(); + } + } + + private static void loadDataWithoutValidation(SailRepository repository, ReasoningCase testCase) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, dataTurtle(testCase), DATA_GRAPH); + connection.commit(); + } + } + + private static void loadData(SailRepository repository, ReasoningCase testCase) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, dataTurtle(testCase), DATA_GRAPH); + connection.commit(); + } + } + + private static void loadBaseDataWithoutValidation(SailRepository repository, ReasoningCase testCase) { + try (RepositoryConnection connection = repository.getConnection()) { + connection.begin(IsolationLevels.NONE, ValidationApproach.Disabled); + addTurtle(connection, baseDataTurtle(testCase), DATA_GRAPH); + connection.commit(); + } + } + + private static void addTurtle(RepositoryConnection connection, String turtle, IRI context) { + try { + connection.add(new StringReader(turtle), "", RDFFormat.TURTLE, context); + } catch (java.io.IOException e) { + throw new RuntimeException(e); + } + } + + private static boolean expectedConforms(ReasoningCase testCase) { + if (testCase.scenario == Scenario.RDFS) { + boolean effectiveRdfs = testCase.shapeRdfsOverride != null + ? testCase.shapeRdfsOverride + : testCase.globalRdfs; + return !effectiveRdfs; + } + boolean effectiveInclude = testCase.shapeIncludeOverride != null + ? testCase.shapeIncludeOverride + : testCase.globalInclude; + return effectiveInclude; + } + + private static String shapesTurtle(ReasoningCase testCase) { + StringBuilder builder = new StringBuilder(); + builder.append("@prefix tr: .\n"); + builder.append("@prefix sh: .\n"); + builder.append("@prefix rsx: .\n"); + builder.append("\n"); + + if (testCase.scenario == Scenario.RDFS) { + builder.append("tr:RailcarBrakeShape a sh:NodeShape ;\n"); + builder.append(" sh:targetClass tr:Railcar ;\n"); + if (testCase.shapeRdfsOverride != null) { + builder.append(" rsx:rdfsSubClassReasoning " + testCase.shapeRdfsOverride + " ;\n"); + } + builder.append(" sh:property [\n"); + builder.append(" sh:path tr:hasBrake ;\n"); + builder.append(" sh:minCount 1\n"); + builder.append(" ] .\n"); + return builder.toString(); + } + + builder.append("tr:TrainShape a sh:NodeShape ;\n"); + builder.append(" sh:targetClass tr:Train ;\n"); + builder.append(" rsx:rdfsSubClassReasoning false ;\n"); + if (testCase.shapeIncludeOverride != null) { + builder.append(" rsx:includeInferredStatements " + testCase.shapeIncludeOverride + " ;\n"); + } + builder.append(" sh:property [\n"); + builder.append(" sh:path [ sh:inversePath tr:isRailcarOf ] ;\n"); + builder.append(" sh:class tr:Railcar ;\n"); + builder.append(" sh:minCount 1\n"); + builder.append(" ] .\n"); + return builder.toString(); + } + + private static String ontologyTurtle(ReasoningCase testCase) { + return String.join("\n", + "@prefix tr: .", + "@prefix owl: .", + "@prefix rdfs: .", + "", + "tr:Train a owl:Class .", + "tr:Railcar a owl:Class .", + "tr:FreightWagon a owl:Class ;", + " rdfs:subClassOf tr:Railcar .", + "tr:hasRailcar a owl:ObjectProperty .", + "tr:isRailcarOf a owl:ObjectProperty ;", + " owl:inverseOf tr:hasRailcar .", + "tr:hasBrake a owl:ObjectProperty .", + "" + ); + } + + private static String dataTurtle(ReasoningCase testCase) { + if (testCase.scenario == Scenario.RDFS) { + return String.join("\n", + "@prefix tr: .", + "", + " a tr:Train ;", + " tr:hasRailcar , .", + " a tr:FreightWagon ;", + " tr:hasBrake .", + " a tr:FreightWagon .", + "" + ); + } + + return String.join("\n", + "@prefix tr: .", + "", + " a tr:Train .", + " a tr:FreightWagon ;", + " tr:isRailcarOf .", + "" + ); + } + + private static String baseDataTurtle(ReasoningCase testCase) { + if (testCase.scenario != Scenario.INCLUDE_INFERRED) { + return dataTurtle(testCase); + } + return String.join("\n", + "@prefix tr: .", + "", + " a tr:Train .", + " a tr:FreightWagon .", + "" + ); + } + + private static String updatePart1(ReasoningCase testCase) { + if (testCase.scenario == Scenario.RDFS) { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:Train ;", + " tr:hasRailcar .", + " a tr:FreightWagon ;", + " tr:hasBrake .", + " }", + "}" + ); + } + + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " a tr:Train .", + " a tr:FreightWagon .", + " }", + "}" + ); + } + + private static String updatePart2(ReasoningCase testCase) { + if (testCase.scenario == Scenario.RDFS) { + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " tr:hasRailcar .", + " a tr:FreightWagon .", + " }", + "}" + ); + } + + return String.join("\n", + "PREFIX tr: ", + "INSERT DATA {", + " GRAPH {", + " tr:isRailcarOf .", + " }", + "}" + ); + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclSailInferredDeltaInternalsTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclSailInferredDeltaInternalsTest.java new file mode 100644 index 00000000000..6a563ba1f90 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclSailInferredDeltaInternalsTest.java @@ -0,0 +1,171 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.IRI; +import org.eclipse.rdf4j.model.Resource; +import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.vocabulary.RDF; +import org.eclipse.rdf4j.model.vocabulary.RDFS; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection; +import org.eclipse.rdf4j.sail.SailConnection; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup; +import org.eclipse.rdf4j.sail.shacl.wrapper.data.RdfsSubClassOfReasoner; +import org.junit.jupiter.api.Test; + +class ShaclSailInferredDeltaInternalsTest { + + private static final SimpleValueFactory VF = SimpleValueFactory.getInstance(); + + private static final Resource[] ALL_CONTEXTS = { null }; + + private static final IRI TARGET = VF.createIRI("urn:target"); + private static final IRI BASE_P = VF.createIRI("urn:baseP"); + private static final IRI BASE_O = VF.createIRI("urn:baseO"); + private static final IRI CLASS_SUB = VF.createIRI("urn:ClassSub"); + private static final IRI CLASS_SUPER = VF.createIRI("urn:ClassSuper"); + + @Test + void fillAddedAndRemovedRepositoriesShouldCombineExplicitAndInferredStoresBySettings() throws Exception { + try (ShaclConnectionHarness harness = new ShaclConnectionHarness()) { + harness.shaclConnection.rdfsSubClassOfReasoner = createReasoner(CLASS_SUB, CLASS_SUPER); + + Statement explicitType = VF.createStatement(TARGET, RDF.TYPE, CLASS_SUB); + Statement baseInferred = VF.createStatement(TARGET, BASE_P, BASE_O); + + harness.shaclConnection.statementAdded(explicitType, false); + harness.shaclConnection.statementAdded(baseInferred, true); + harness.shaclConnection.fillAddedAndRemovedStatementRepositories(); + + assertAddedVisibility(harness.shaclConnection, false, false, true, false, false); + assertAddedVisibility(harness.shaclConnection, true, false, true, true, false); + assertAddedVisibility(harness.shaclConnection, false, true, true, false, true); + assertAddedVisibility(harness.shaclConnection, true, true, true, true, true); + } + } + + @Test + void fillInferredRepositoryShouldFilterStatementsPresentInBothAddedAndRemovedInferredSets() throws Exception { + try (ShaclConnectionHarness harness = new ShaclConnectionHarness()) { + Statement inferredStatement = VF.createStatement(TARGET, BASE_P, BASE_O); + + harness.shaclConnection.statementAdded(inferredStatement, true); + harness.shaclConnection.statementRemoved(inferredStatement, true); + harness.shaclConnection.fillAddedAndRemovedStatementRepositories(); + + try (ConnectionsGroup connectionsGroup = harness.shaclConnection.getConnectionsGroup( + harness.shaclConnection.getWrappedConnection(), null, true, false)) { + assertNotNull(connectionsGroup.getAddedStatements(), + "Expected inferred added-store to be created when inferred additions are tracked"); + assertNotNull(connectionsGroup.getRemovedStatements(), + "Expected inferred removed-store to be created when inferred removals are tracked"); + assertFalse( + connectionsGroup.getAddedStatements().hasStatement(TARGET, BASE_P, BASE_O, true, ALL_CONTEXTS), + "Expected overlapping inferred statement to be filtered from added-store"); + assertFalse( + connectionsGroup.getRemovedStatements() + .hasStatement(TARGET, BASE_P, BASE_O, true, ALL_CONTEXTS), + "Expected overlapping inferred statement to be filtered from removed-store"); + } + } + } + + private static void assertAddedVisibility(ShaclSailConnection shaclConnection, boolean includeInferred, + boolean useRdfsSubClassReasoning, boolean expectExplicitType, boolean expectBaseInferred, + boolean expectRdfsInferredType) { + try (ConnectionsGroup connectionsGroup = shaclConnection.getConnectionsGroup( + shaclConnection.getWrappedConnection(), + null, includeInferred, useRdfsSubClassReasoning)) { + SailConnection addedStatements = connectionsGroup.getAddedStatements(); + assertNotNull(addedStatements, "Expected added-statement store to be available"); + assertEquals(expectExplicitType, + addedStatements.hasStatement(TARGET, RDF.TYPE, CLASS_SUB, true, ALL_CONTEXTS), + "Explicit type statement visibility mismatch"); + assertEquals(expectBaseInferred, + addedStatements.hasStatement(TARGET, BASE_P, BASE_O, true, ALL_CONTEXTS), + "Base inferred statement visibility mismatch"); + assertEquals(expectRdfsInferredType, + addedStatements.hasStatement(TARGET, RDF.TYPE, CLASS_SUPER, true, ALL_CONTEXTS), + "RDFS inferred type statement visibility mismatch"); + } + } + + private static void assertEquals(boolean expected, boolean actual, String message) { + if (expected) { + assertTrue(actual, message); + } else { + assertFalse(actual, message); + } + } + + private static RdfsSubClassOfReasoner createReasoner(IRI subClass, IRI superClass) { + MemoryStore memoryStore = new MemoryStore(); + memoryStore.init(); + try (SailConnection connection = memoryStore.getConnection()) { + connection.begin(IsolationLevels.NONE); + connection.addStatement(subClass, RDFS.SUBCLASSOF, superClass); + connection.commit(); + + connection.begin(IsolationLevels.NONE); + return RdfsSubClassOfReasoner.createReasoner(connection, new ValidationSettings()); + } finally { + memoryStore.shutDown(); + } + } + + private static final class ShaclConnectionHarness implements AutoCloseable { + private final SailRepository repository; + private final SailRepositoryConnection repositoryConnection; + private final ShaclSailConnection shaclConnection; + + private ShaclConnectionHarness() { + ShaclSail shaclSail = new ShaclSail(new MemoryStore()); + shaclSail.setSerializableValidation(false); + repository = new SailRepository(shaclSail); + repository.init(); + + try (SailRepositoryConnection seedConnection = repository.getConnection()) { + seedConnection.begin(IsolationLevels.NONE, ShaclSail.TransactionSettings.ValidationApproach.Disabled); + seedConnection.add(VF.createIRI("urn:seed"), VF.createIRI("urn:seedPredicate"), + VF.createIRI("urn:seedObject")); + seedConnection.commit(); + } + + repositoryConnection = repository.getConnection(); + repositoryConnection.begin(IsolationLevels.NONE); + shaclConnection = (ShaclSailConnection) repositoryConnection.getSailConnection(); + } + + @Override + public void close() { + try { + try { + repositoryConnection.rollback(); + } catch (Exception ignored) { + // ignore + } + repositoryConnection.close(); + } finally { + repository.shutDown(); + } + } + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclTestWithSchemaCachingRdfsInferencerTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclTestWithSchemaCachingRdfsInferencerTest.java new file mode 100644 index 00000000000..7b46591a097 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShaclTestWithSchemaCachingRdfsInferencerTest.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import java.util.Set; + +import org.eclipse.rdf4j.common.transaction.IsolationLevel; +import org.eclipse.rdf4j.common.transaction.QueryEvaluationMode; +import org.eclipse.rdf4j.repository.sail.SailRepository; +import org.eclipse.rdf4j.sail.inferencer.fc.SchemaCachingRDFSInferencer; +import org.eclipse.rdf4j.sail.memory.MemoryStore; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * @author HÃ¥vard Ottestad + */ +@Tag("slow") +public class ShaclTestWithSchemaCachingRdfsInferencerTest extends ShaclTest { + + private static final Set IGNORED_TEST_CASE_PREFIXES = Set.of( + "test-cases/datatype/allObjects/", + "test-cases/hasValue/targetShapeAnd/", + "test-cases/hasValue/targetShapeAnd2/", + "test-cases/hasValue/targetShapeAndOr/", + "test-cases/hasValue/targetShapeAndOr3/", + "test-cases/hasValue/targetShapeOr/", + "test-cases/hasValueIn/targetShapeOr/" + ); + + @ParameterizedTest + @MethodSource("testsToRunWithIsolationLevel") + public void test(TestCase testCase, IsolationLevel isolationLevel) { + if (ignoredTest(testCase)) { + return; + } + runWithAutomaticLogging(() -> runTestCase(testCase, isolationLevel, false)); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testSingleTransaction(TestCase testCase) { + if (ignoredTest(testCase)) { + return; + } + runWithAutomaticLogging(() -> runTestCaseSingleTransaction(testCase)); + } + + @ParameterizedTest + @MethodSource("testsToRunWithIsolationLevel") + public void testRevalidation(TestCase testCase, IsolationLevel isolationLevel) { + if (ignoredTest(testCase)) { + return; + } + runWithAutomaticLogging(() -> runTestCaseRevalidate(testCase, isolationLevel)); + } + + @ParameterizedTest + @MethodSource("testsToRunWithIsolationLevel") + public void testNonEmpty(TestCase testCase, IsolationLevel isolationLevel) { + if (ignoredTest(testCase)) { + return; + } + runWithAutomaticLogging(() -> runTestCase(testCase, isolationLevel, true)); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testParsing(TestCase testCase) { + if (ignoredTest(testCase)) { + return; + } + runWithAutomaticLogging(() -> runParsingTest(testCase)); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testReferenceImplementation(TestCase testCase) { + if (ignoredTest(testCase)) { + return; + } + runWithAutomaticLogging(() -> referenceImplementationTestCaseValidation(testCase)); + } + + @ParameterizedTest + @MethodSource("testCases") + public void testShaclValidator(TestCase testCase) { + if (ignoredTest(testCase)) { + return; + } + runWithAutomaticLogging(() -> runWithShaclValidator(testCase)); + } + + private static boolean ignoredTest(TestCase testCase) { + String testCasePath = testCase.getTestCasePath(); + return IGNORED_TEST_CASE_PREFIXES.stream().anyMatch(testCasePath::startsWith); + } + + @Override + SailRepository getShaclSail(TestCase testCase) { + MemoryStore memoryStore = new MemoryStore(); + memoryStore.setDefaultQueryEvaluationMode(QueryEvaluationMode.STRICT); + + SchemaCachingRDFSInferencer inferencer = new SchemaCachingRDFSInferencer(memoryStore, false); + + ShaclSail shaclSail = new ShaclSail(inferencer); + SailRepository repository = new SailRepository(shaclSail); + + shaclSail.setLogValidationPlans(fullLogging); + shaclSail.setCacheSelectNodes(true); + shaclSail.setParallelValidation(false); + shaclSail.setLogValidationViolations(fullLogging); + shaclSail.setGlobalLogValidationExecution(fullLogging); + shaclSail.setEclipseRdf4jShaclExtensions(true); + shaclSail.setDashDataShapes(true); + shaclSail.setPerformanceLogging(false); + shaclSail.setRdfsSubClassReasoning(false); + shaclSail.setIncludeInferredStatements(true); + shaclSail.setSerializableValidation(false); + shaclSail.setShapesGraphs(SHAPE_GRAPHS); + + repository.init(); + + try { + Utils.loadShapeData(repository, testCase.getShacl()); + if (testCase.hasInitialData()) { + Utils.loadInitialData(repository, testCase.getInitialData()); + } + } catch (Exception e) { + repository.shutDown(); + throw new RuntimeException(e); + } + + return repository; + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShapeValidationContainerLifecycleTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShapeValidationContainerLifecycleTest.java new file mode 100644 index 00000000000..601e8ea13a2 --- /dev/null +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShapeValidationContainerLifecycleTest.java @@ -0,0 +1,51 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse RDF4J contributors. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Distribution License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + *******************************************************************************/ +// Some portions generated by Codex + +package org.eclipse.rdf4j.sail.shacl; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.eclipse.rdf4j.sail.shacl.ast.Shape; +import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode; +import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +class ShapeValidationContainerLifecycleTest { + + @Test + void emptyPlanShouldCloseOwnedConnectionsGroup() { + Shape shape = mock(Shape.class); + PlanNode planNode = mock(PlanNode.class); + ConnectionsGroup connectionsGroup = mock(ConnectionsGroup.class); + + when(planNode.isGuaranteedEmpty()).thenReturn(true); + + ShapeValidationContainer container = new ShapeValidationContainer( + shape, + () -> planNode, + false, + false, + 1, + false, + false, + LoggerFactory.getLogger(ShapeValidationContainerLifecycleTest.class), + connectionsGroup, + true); + + assertFalse(container.hasPlanNode()); + verify(connectionsGroup).close(); + } +} diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShapesGraphTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShapesGraphTest.java index 9a3ae7f6d55..409733ea82a 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShapesGraphTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/ShapesGraphTest.java @@ -304,6 +304,7 @@ public void testValidationRequired() throws IOException, InterruptedException { ShaclSail shaclSail = new ShaclSail(new MemoryStore()); SailRepository repository = new SailRepository(shaclSail); + boolean requiresRdfsSubClassReasoner = shaclSail.isRdfsSubClassReasoning(); shaclSail.setShapesGraphs(Set.of( Values.iri(EX, "peopleKnowPeopleShapes"), @@ -316,7 +317,7 @@ public void testValidationRequired() throws IOException, InterruptedException { connection.begin(); connection.addStatement(Values.bnode(), RDF.TYPE, FOAF.PERSON, data1); - connection.prepareValidation(new ValidationSettings()); + connection.prepareValidation(new ValidationSettings(), requiresRdfsSubClassReasoner); try (ConnectionsGroup connectionsGroup = connection.getConnectionsGroup()) { diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfigTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfigTest.java index 2638a59b295..d0d08d08ce2 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfigTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailConfigTest.java @@ -15,6 +15,7 @@ import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.DASH_DATA_SHAPES; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.ECLIPSE_RDF4J_SHACL_EXTENSIONS; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.GLOBAL_LOG_VALIDATION_EXECUTION; +import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.INCLUDE_INFERRED_STATEMENTS; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.LOG_VALIDATION_PLANS; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.LOG_VALIDATION_VIOLATIONS; import static org.eclipse.rdf4j.sail.shacl.config.ShaclSailSchema.PARALLEL_VALIDATION; @@ -58,6 +59,7 @@ public void defaultsCorrectlySet() { assertThat(shaclSailConfig.isCacheSelectNodes()).isTrue(); assertThat(shaclSailConfig.isGlobalLogValidationExecution()).isFalse(); assertThat(shaclSailConfig.isRdfsSubClassReasoning()).isTrue(); + assertThat(shaclSailConfig.isIncludeInferredStatements()).isTrue(); assertThat(shaclSailConfig.isPerformanceLogging()).isFalse(); assertThat(shaclSailConfig.isSerializableValidation()).isTrue(); assertThat(shaclSailConfig.isEclipseRdf4jShaclExtensions()).isFalse(); @@ -69,6 +71,32 @@ public void defaultsCorrectlySet() { } + @Test + public void shaclConfigVocabulariesExposeIncludeInferredStatementsConstant() throws Exception { + IRI modern = (IRI) CONFIG.Shacl.class.getField("includeInferredStatements").get(null); + IRI legacy = (IRI) ShaclSailSchema.class.getField("INCLUDE_INFERRED_STATEMENTS").get(null); + + assertThat(modern.stringValue()).isEqualTo(CONFIG.NAMESPACE + "shacl.includeInferredStatements"); + assertThat(legacy.stringValue()).isEqualTo(ShaclSailSchema.NAMESPACE + "includeInferredStatements"); + } + + @Test + public void parseIncludeInferredStatementsFromModelSetsValueCorrectly() { + ShaclSailConfig shaclSailConfig = new ShaclSailConfig(); + BNode implNode = vf.createBNode(); + ModelBuilder mb = new ModelBuilder().subject(implNode); + + mb.add(CONFIG.Shacl.includeInferredStatements, false); + mb.add(INCLUDE_INFERRED_STATEMENTS, true); + + shaclSailConfig.parse(mb.build(), implNode); + + Model exported = new TreeModel(); + Resource exportedNode = shaclSailConfig.export(exported); + assertThat(exported.contains(exportedNode, CONFIG.Shacl.includeInferredStatements, Values.literal(false))) + .isTrue(); + } + @Test public void parseFromModelSetValuesCorrectly() { ShaclSailConfig shaclSailConfig = new ShaclSailConfig(); @@ -83,6 +111,7 @@ public void parseFromModelSetValuesCorrectly() { mb.add(CONFIG.Shacl.cacheSelectNodes, false); mb.add(CONFIG.Shacl.globalLogValidationExecution, false); mb.add(CONFIG.Shacl.rdfsSubClassReasoning, true); + mb.add(CONFIG.Shacl.includeInferredStatements, false); mb.add(CONFIG.Shacl.performanceLogging, false); mb.add(CONFIG.Shacl.eclipseRdf4jShaclExtensions, false); mb.add(CONFIG.Shacl.dashDataShapes, false); @@ -99,6 +128,7 @@ public void parseFromModelSetValuesCorrectly() { mb.add(CACHE_SELECT_NODES, true); mb.add(GLOBAL_LOG_VALIDATION_EXECUTION, true); mb.add(RDFS_SUB_CLASS_REASONING, false); + mb.add(INCLUDE_INFERRED_STATEMENTS, true); mb.add(PERFORMANCE_LOGGING, true); mb.add(ECLIPSE_RDF4J_SHACL_EXTENSIONS, true); mb.add(DASH_DATA_SHAPES, true); @@ -122,6 +152,7 @@ public void parseFromModelSetValuesCorrectly() { assertThat(shaclSailConfig.isCacheSelectNodes()).isFalse(); assertThat(shaclSailConfig.isGlobalLogValidationExecution()).isFalse(); assertThat(shaclSailConfig.isRdfsSubClassReasoning()).isTrue(); + assertThat(shaclSailConfig.isIncludeInferredStatements()).isFalse(); assertThat(shaclSailConfig.isPerformanceLogging()).isFalse(); assertThat(shaclSailConfig.isSerializableValidation()).isTrue(); assertThat(shaclSailConfig.isEclipseRdf4jShaclExtensions()).isFalse(); @@ -187,6 +218,7 @@ public void exportAddsAllConfigData() { Assertions.assertTrue(m.contains(node, CONFIG.Shacl.cacheSelectNodes, null)); Assertions.assertTrue(m.contains(node, CONFIG.Shacl.globalLogValidationExecution, null)); Assertions.assertTrue(m.contains(node, CONFIG.Shacl.rdfsSubClassReasoning, null)); + Assertions.assertTrue(m.contains(node, CONFIG.Shacl.includeInferredStatements, null)); Assertions.assertTrue(m.contains(node, CONFIG.Shacl.performanceLogging, null)); Assertions.assertTrue(m.contains(node, CONFIG.Shacl.serializableValidation, null)); Assertions.assertTrue(m.contains(node, CONFIG.Shacl.eclipseRdf4jShaclExtensions, null)); diff --git a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactoryTest.java b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactoryTest.java index e49aa9ef95d..5ec4a06c4f3 100644 --- a/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactoryTest.java +++ b/core/sail/shacl/src/test/java/org/eclipse/rdf4j/sail/shacl/config/ShaclSailFactoryTest.java @@ -18,7 +18,11 @@ import org.eclipse.rdf4j.common.transaction.IsolationLevel; import org.eclipse.rdf4j.common.transaction.IsolationLevels; +import org.eclipse.rdf4j.model.BNode; import org.eclipse.rdf4j.model.ValueFactory; +import org.eclipse.rdf4j.model.impl.SimpleValueFactory; +import org.eclipse.rdf4j.model.util.ModelBuilder; +import org.eclipse.rdf4j.model.vocabulary.CONFIG; import org.eclipse.rdf4j.sail.NotifyingSailConnection; import org.eclipse.rdf4j.sail.SailException; import org.eclipse.rdf4j.sail.helpers.AbstractNotifyingSail; @@ -127,6 +131,7 @@ public void getSailWithCustomConfigSetsConfigurationCorrectly() { config.setPerformanceLogging(!config.isPerformanceLogging()); config.setSerializableValidation(!config.isSerializableValidation()); config.setRdfsSubClassReasoning(!config.isRdfsSubClassReasoning()); + config.setIncludeInferredStatements(!config.isIncludeInferredStatements()); config.setEclipseRdf4jShaclExtensions(!config.isEclipseRdf4jShaclExtensions()); config.setDashDataShapes(!config.isDashDataShapes()); @@ -138,6 +143,21 @@ public void getSailWithCustomConfigSetsConfigurationCorrectly() { } + @Test + public void getSailReadsIncludeInferredStatementsFromParsedConfig() { + ShaclSailFactory subject = new ShaclSailFactory(); + ShaclSailConfig config = new ShaclSailConfig(); + BNode implNode = SimpleValueFactory.getInstance().createBNode(); + ModelBuilder mb = new ModelBuilder().subject(implNode); + + mb.add(CONFIG.Shacl.includeInferredStatements, false); + + config.parse(mb.build(), implNode); + + ShaclSail sail = (ShaclSail) subject.getSail(config); + assertThat(sail.isIncludeInferredStatements()).isFalse(); + } + private void assertMatchesConfig(ShaclSail sail, ShaclSailConfig config) { assertThat(sail.isCacheSelectNodes()).isEqualTo(config.isCacheSelectNodes()); assertThat(sail.isGlobalLogValidationExecution()).isEqualTo(config.isGlobalLogValidationExecution()); @@ -148,6 +168,7 @@ private void assertMatchesConfig(ShaclSail sail, ShaclSailConfig config) { assertThat(sail.isPerformanceLogging()).isEqualTo(config.isPerformanceLogging()); assertThat(sail.isSerializableValidation()).isEqualTo(config.isSerializableValidation()); assertThat(sail.isRdfsSubClassReasoning()).isEqualTo(config.isRdfsSubClassReasoning()); + assertThat(sail.isIncludeInferredStatements()).isEqualTo(config.isIncludeInferredStatements()); assertThat(sail.isEclipseRdf4jShaclExtensions()).isEqualTo(config.isEclipseRdf4jShaclExtensions()); assertThat(sail.isDashDataShapes()).isEqualTo(config.isDashDataShapes()); assertThat(sail.getValidationResultsLimitTotal()).isEqualTo(config.getValidationResultsLimitTotal()); diff --git a/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/query2.rq b/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/query2.rq index 3854ff2657a..d790c33ddf4 100644 --- a/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/query2.rq +++ b/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/query2.rq @@ -16,6 +16,10 @@ INSERT { ex:validPerson1 ex:iriOrMinLength5String []. } -WHERE{?a ?b ?c.} +WHERE{ + +ex:validPerson1 ex:iriOrMinLength5String "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis id elementum turpis. Suspendisse metus.". + +} diff --git a/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/report.ttl b/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/report.ttl index 892f060ae36..b9451f0a002 100644 --- a/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/report.ttl +++ b/core/sail/shacl/src/test/resources/test-cases/or/nodeKindMinLength/invalid/case4/report.ttl @@ -16,22 +16,13 @@ sh:resultPath ex:iriOrMinLength5String; sh:resultSeverity sh:Violation; sh:sourceConstraintComponent sh:OrConstraintComponent; - sh:sourceShape _:5089a376325a403b926b5c31d5e95e7215198; - sh:value [] - ], [ a sh:ValidationResult; - rsx:shapesGraph rdf4j:SHACLShapeGraph; - sh:focusNode ex:validPerson1; - sh:resultPath ex:iriOrMinLength5String; - sh:resultSeverity sh:Violation; - sh:sourceConstraintComponent sh:OrConstraintComponent; - sh:sourceShape _:5089a376325a403b926b5c31d5e95e7215198; + sh:sourceShape [ a sh:PropertyShape; + sh:or ([ a sh:NodeShape; + sh:nodeKind sh:IRI + ] [ a sh:NodeShape; + sh:minLength 100 + ]); + sh:path ex:iriOrMinLength5String + ]; sh:value [] ] . - -_:5089a376325a403b926b5c31d5e95e7215198 a sh:PropertyShape; - sh:or ([ a sh:NodeShape; - sh:nodeKind sh:IRI - ] [ a sh:NodeShape; - sh:minLength 100 - ]); - sh:path ex:iriOrMinLength5String . diff --git a/site/static/shacl/extensions.html b/site/static/shacl/extensions.html index 826c4e9843b..9a154153858 100644 --- a/site/static/shacl/extensions.html +++ b/site/static/shacl/extensions.html @@ -257,6 +257,58 @@
Parties (defined as foaf:Organization with appropriate dc:type) must stick +
+

Per-shape Reasoning Controls

+ +

+ Shapes may override the global SHACL reasoning settings with RSX properties. + rsx:rdfsSubClassReasoning controls whether built-in RDFS subclass reasoning is applied for a shape, + and rsx:includeInferredStatements controls whether inferred statements from the underlying store + are considered for that shape. When RDF4J SHACL extensions are enabled, both settings default to + true. +

+ +

Example

+
+@prefix ex: <http://example.com/ns#> .
+@prefix rsx: <http://rdf4j.org/shacl-extensions#> .
+@prefix sh: <http://www.w3.org/ns/shacl#> .
+
+ex:RailcarBrakeShape a sh:NodeShape;
+  sh:targetObjectsOf ex:relatedRailcar;
+  sh:property [
+    sh:path ex:hasBrake;
+    sh:minCount 1
+  ].
+
+ex:RailcarBrakeShapeNoInferred a sh:NodeShape;
+  rsx:includeInferredStatements false;
+  sh:targetObjectsOf ex:relatedRailcar;
+  sh:property [
+    sh:path ex:hasBrake;
+    sh:minCount 1
+  ].
+    
+
+@prefix ex: <http://example.com/ns#> .
+
+ex:train1 ex:hasRailcar ex:wagon1.
+ex:wagon1 a ex:Railcar.
+    
+
+@prefix ex: <http://example.com/ns#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+
+ex:hasRailcar rdfs:subPropertyOf ex:relatedRailcar.
+    
+

+ With defaults, the inferred ex:train1 ex:relatedRailcar ex:wagon1 statement is included, + so ex:RailcarBrakeShape targets ex:wagon1 and fails because it has no brake. + The ex:RailcarBrakeShapeNoInferred override excludes inferred statements, so the inferred + target is not selected and the shape passes. +

+
+

Validation and Graphs

diff --git a/site/static/shacl/extensions.ttl b/site/static/shacl/extensions.ttl index 02ae0c97eaf..c7e0bc7fe22 100644 --- a/site/static/shacl/extensions.ttl +++ b/site/static/shacl/extensions.ttl @@ -14,6 +14,22 @@ rsx:targetShape rdfs:range sh:Shape ; rdfs:isDefinedBy rsx: . +rsx:rdfsSubClassReasoning + a rdf:Property ; + rdfs:label "rdfs subclass reasoning"@en ; + rdfs:comment "Overrides built-in RDFS subclass reasoning for a shape."@en ; + rdfs:domain sh:Shape ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy rsx: . + +rsx:includeInferredStatements + a rdf:Property ; + rdfs:label "include inferred statements"@en ; + rdfs:comment "Overrides inclusion of inferred statements during validation for a shape."@en ; + rdfs:domain sh:Shape ; + rdfs:range xsd:boolean ; + rdfs:isDefinedBy rsx: . + rsx:dataGraph a rdf:Property ;