diff --git a/src/test/java/fr/inria/corese/core/next/query/architecture/NextQueryLegacyBoundaryTest.java b/src/test/java/fr/inria/corese/core/next/query/architecture/NextQueryLegacyBoundaryTest.java new file mode 100644 index 000000000..5f243f4bf --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/architecture/NextQueryLegacyBoundaryTest.java @@ -0,0 +1,189 @@ +package fr.inria.corese.core.next.query.architecture; + +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.ToolProvider; + +import org.junit.jupiter.api.Test; + +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.tree.ImportTree; +import com.sun.source.tree.MemberSelectTree; +import com.sun.source.util.JavacTask; +import com.sun.source.util.TreeScanner; + +/** + * Freezes the legacy dependencies that still exist in {@code next.query}. + * + *

The allowlist is a baseline, not a list of acceptable long-term dependencies. Existing + * entries are removed as the decoupling progresses. Any new entry fails this test and must be + * reviewed explicitly. + */ +class NextQueryLegacyBoundaryTest { + + private static final Path SOURCE_DIRECTORY = + Path.of("src/main/java/fr/inria/corese/core/next/query"); + private static final Path ALLOWLIST = Path.of( + "src/test/resources/fr/inria/corese/core/next/query/architecture/" + + "legacy-dependency-allowlist.txt"); + + private static final Pattern QUALIFIED_REFERENCE = Pattern.compile( + "fr\\.inria\\.corese\\.core\\.(?:" + + "(?:sparql|kgram)(?:\\.[A-Za-z_$][\\w$]*)+" + + "|Graph(?:\\.[A-Za-z_$][\\w$]*)*" + + "|next\\.data\\.impl\\.adapter(?:\\.[A-Za-z_$][\\w$]*)+" + + ")"); + + @Test + void legacyDependenciesMustMatchTheReviewedBaseline() throws IOException { + Path projectRoot = findProjectRoot(); + TreeSet expected = readAllowlist(projectRoot.resolve(ALLOWLIST)); + TreeSet actual = scanDependencies(projectRoot.resolve(SOURCE_DIRECTORY)); + + TreeSet added = new TreeSet<>(actual); + added.removeAll(expected); + + TreeSet removed = new TreeSet<>(expected); + removed.removeAll(actual); + + if (!added.isEmpty() || !removed.isEmpty()) { + fail("Legacy dependency baseline changed.\n" + + formatChanges("Unexpected dependencies", "+ ", added) + + formatChanges("Dependencies no longer present", "- ", removed) + + "Review the change, then update " + ALLOWLIST + "."); + } + } + + private static TreeSet scanDependencies(Path sourceRoot) throws IOException { + List sourceFiles; + try (Stream files = Files.walk(sourceRoot)) { + sourceFiles = files.filter(path -> path.toString().endsWith(".java")) + .sorted() + .toList(); + } + + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new IllegalStateException("A JDK is required to scan Java sources"); + } + + Map dependencies = new TreeMap<>(); + try (StandardJavaFileManager fileManager = + compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)) { + JavacTask task = (JavacTask) compiler.getTask( + null, + fileManager, + null, + List.of("-proc:none"), + null, + fileManager.getJavaFileObjectsFromPaths(sourceFiles)); + + for (CompilationUnitTree unit : task.parse()) { + scanCompilationUnit(sourceRoot, unit, dependencies); + } + } + + TreeSet result = new TreeSet<>(); + dependencies.forEach((dependency, count) -> result.add(dependency + "|" + count)); + return result; + } + + private static void scanCompilationUnit( + Path sourceRoot, + CompilationUnitTree unit, + Map dependencies) { + Path sourceFile = Path.of(unit.getSourceFile().toUri()); + String relativePath = sourceRoot.relativize(sourceFile).toString().replace('\\', '/'); + + for (ImportTree importTree : unit.getImports()) { + String target = importTree.getQualifiedIdentifier().toString(); + if (isForbiddenDependency(target)) { + String kind = importTree.isStatic() ? "STATIC_IMPORT" : "IMPORT"; + increment(dependencies, relativePath, kind, target); + } + } + + new TreeScanner() { + @Override + public Void visitImport(ImportTree node, Void unused) { + return null; + } + + @Override + public Void visitMemberSelect(MemberSelectTree node, Void unused) { + Matcher matcher = QUALIFIED_REFERENCE.matcher(node.toString()); + if (matcher.matches()) { + increment( + dependencies, + relativePath, + "QUALIFIED_REFERENCE", + matcher.group()); + return null; + } + return super.visitMemberSelect(node, unused); + } + }.scan(unit, null); + } + + private static boolean isForbiddenDependency(String target) { + return target.startsWith("fr.inria.corese.core.sparql.") + || target.startsWith("fr.inria.corese.core.kgram.") + || target.equals("fr.inria.corese.core.Graph") + || target.startsWith("fr.inria.corese.core.Graph.") + || target.startsWith("fr.inria.corese.core.next.data.impl.adapter."); + } + + private static void increment( + Map dependencies, + String path, + String kind, + String target) { + dependencies.merge(path + "|" + kind + "|" + target, 1, Integer::sum); + } + + private static TreeSet readAllowlist(Path allowlist) throws IOException { + TreeSet entries = new TreeSet<>(); + for (String line : Files.readAllLines(allowlist, StandardCharsets.UTF_8)) { + String entry = line.trim(); + if (!entry.isEmpty() && !entry.startsWith("#")) { + entries.add(entry); + } + } + return entries; + } + + private static Path findProjectRoot() { + Path current = Path.of("").toAbsolutePath().normalize(); + while (current != null) { + if (Files.isDirectory(current.resolve(SOURCE_DIRECTORY))) { + return current; + } + current = current.getParent(); + } + throw new IllegalStateException("Cannot locate project root containing " + SOURCE_DIRECTORY); + } + + private static String formatChanges(String title, String prefix, TreeSet changes) { + if (changes.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder(title).append(":\n"); + changes.forEach(change -> result.append(prefix).append(change).append('\n')); + return result.append('\n').toString(); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java index 42d9e22ce..4e8ad3fc5 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java @@ -196,6 +196,26 @@ void selectWithDatasetFromReturnsNothingForEmptyNamedGraph() { assertFalse(result.hasNext(), "No results expected for an empty named graph"); } + @Test + @DisplayName("Inline FROM restricts results to the selected graph") + void selectWithInlineFromRestrictsToNamedGraph() { + String graph1 = "http://example.org/graph1"; + String graph2 = "http://example.org/graph2"; + String carol = "http://example.org/carol"; + insertInGraph(iri(ALICE), iri(KNOWS), iri(BOB), iri(graph1)); + insertInGraph(iri(ALICE), iri(KNOWS), iri(carol), iri(graph2)); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?o + FROM + WHERE { ?o } + """); + + assertTrue(result.hasNext()); + assertEquals(BOB, result.next().getValue("o").stringValue()); + assertFalse(result.hasNext()); + } + // ------------------------------------------------------------------------- // CONSTRUCT / graph evaluation // ------------------------------------------------------------------------- @@ -255,6 +275,62 @@ void selectWithLiteralInitialBindingFiltersResults() { assertFalse(result.hasNext(), "Only the triple with literal 'Bob' should match"); } + // ------------------------------------------------------------------------- + // Current executable solution-modifier scope + // ------------------------------------------------------------------------- + + @Test + @DisplayName("DISTINCT executes end-to-end in the next runtime") + void distinctRunsEndToEnd() { + String carol = "http://example.org/carol"; + insert(iri(BOB), iri(KNOWS), iri(carol)); + insert(iri(carol), iri(NAME), valueFactory.createLiteral("Carol")); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT DISTINCT ?p WHERE { ?s ?p ?o } + """); + + List predicates = result.stream() + .map(binding -> binding.getValue("p").stringValue()) + .toList(); + assertEquals(2, predicates.size()); + assertEquals(Set.of(KNOWS, NAME), Set.copyOf(predicates)); + } + + @Test + @DisplayName("LIMIT and OFFSET execute end-to-end in the next runtime") + void limitAndOffsetRunEndToEnd() { + insert(iri(BOB), iri(KNOWS), iri("http://example.org/carol")); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?s WHERE { ?s ?p ?o } + ORDER BY ?s + LIMIT 1 + OFFSET 1 + """); + + assertTrue(result.hasNext()); + assertEquals(BOB, result.next().getValue("s").stringValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("ORDER BY variable executes end-to-end in the next runtime") + void orderByVariableRunsEndToEnd() { + String aaron = "http://example.org/aaron"; + insert(iri(ALICE), iri(KNOWS), iri(aaron)); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?o WHERE { ?s ?o } + ORDER BY ?o + """); + + List objects = result.stream() + .map(binding -> binding.getValue("o").stringValue()) + .toList(); + assertEquals(List.of(aaron, BOB), objects); + } + // ------------------------------------------------------------------------- // Timeout // ------------------------------------------------------------------------- @@ -293,9 +369,7 @@ void queryTimeoutExceptionThrownWhenDeadlineExceeded() { assertThrows(QueryTimeoutException.class, () -> executor.evaluateTuple( "SELECT * WHERE { ?s1 ?p1 ?o1 . ?s2 ?p2 ?o2 }", - null, null, 1L) // 1 ms - .stream() - .count()); + null, null, 1L)); // 1 ms } // ------------------------------------------------------------------------- diff --git a/src/test/resources/fr/inria/corese/core/next/query/architecture/legacy-dependency-allowlist.txt b/src/test/resources/fr/inria/corese/core/next/query/architecture/legacy-dependency-allowlist.txt new file mode 100644 index 000000000..121f91378 --- /dev/null +++ b/src/test/resources/fr/inria/corese/core/next/query/architecture/legacy-dependency-allowlist.txt @@ -0,0 +1,87 @@ +# Reviewed baseline for legacy dependencies in src/main/java/fr/inria/corese/core/next/query. +# Format: relative-path|kind|fully-qualified-reference|occurrence-count +impl/query/CoreseUpdate.java|IMPORT|fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory|1 +impl/repository/CoreseRepository.java|IMPORT|fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory|1 +impl/result/CoreseBindingSet.java|IMPORT|fr.inria.corese.core.next.data.impl.adapter.CoreseValueConverter|1 +impl/sparql/bridge/AstBackedExistTerm.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.ASTBuffer|1 +impl/sparql/bridge/AstBackedExistTerm.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Term|1 +impl/sparql/bridge/AstBackedExpr.java|IMPORT|fr.inria.corese.core.sparql.api.Computer|1 +impl/sparql/bridge/AstBackedExpr.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +impl/sparql/bridge/AstBackedExpr.java|IMPORT|fr.inria.corese.core.sparql.triple.function.term.Binding|1 +impl/sparql/bridge/AstBackedExpr.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Expression|1 +impl/sparql/bridge/CoreseAstQueryBuilder.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Atom|1 +impl/sparql/bridge/CoreseAstQueryBuilder.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Expression|1 +impl/sparql/bridge/CoreseAstQueryBuilder.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Variable|1 +impl/sparql/bridge/KgramNodeConverter.java|IMPORT|fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory|1 +impl/sparql/bridge/KgramNodeConverter.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +impl/sparql/bridge/NextDatatypeValueAdapter.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +impl/sparql/bridge/NextDatatypeValueAdapter.java|IMPORT|fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException|1 +impl/sparql/bridge/NextFilterFromAst.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Expression|1 +impl/sparql/bridge/SparqlAstToExpression.java|IMPORT|fr.inria.corese.core.sparql.datatype.RDF|1 +impl/sparql/bridge/SparqlAstToExpression.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.*|1 +impl/sparql/bridge/SparqlAstToExpression.java|QUALIFIED_REFERENCE|fr.inria.corese.core.sparql.triple.cst.Keyword.SEAND|1 +impl/sparql/bridge/SparqlAstToExpression.java|QUALIFIED_REFERENCE|fr.inria.corese.core.sparql.triple.cst.Keyword.SEOR|1 +impl/sparql/bridge/SparqlAstToExpression.java|QUALIFIED_REFERENCE|fr.inria.corese.core.sparql.triple.parser.NSManager.nsm|1 +impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Processor|1 +impl/sparql/execution/NextSparqlPipelineExecutor.java|IMPORT|fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory|1 +kgram/adapter/BindingAdapter.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/adapter/BindingAdapter.java|IMPORT|fr.inria.corese.core.sparql.triple.function.term.Binding|1 +kgram/adapter/BindingAdapter.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.api.core.Expr|6 +kgram/adapter/BindingAdapter.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.api.core.Node|2 +kgram/adapter/BindingAdapter.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.api.query.ProcessVisitor|2 +kgram/adapter/BindingAdapter.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.core.Exp|2 +kgram/adapter/BindingAdapter.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.core.Mappings|4 +kgram/adapter/DatatypeAdapter.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/adapter/DatatypeAdapter.java|IMPORT|fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException|1 +kgram/adapter/TripleParserEvalSupport.java|IMPORT|fr.inria.corese.core.sparql.api.Computer|1 +kgram/adapter/TripleParserEvalSupport.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/adapter/TripleParserEvalSupport.java|IMPORT|fr.inria.corese.core.sparql.exceptions.EngineException|1 +kgram/adapter/TripleParserEvalSupport.java|IMPORT|fr.inria.corese.core.sparql.triple.function.term.Binding|1 +kgram/adapter/TripleParserEvalSupport.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Expression|1 +kgram/adapter/TripleParserEvalSupport.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.api.query.Environment|1 +kgram/adapter/TripleParserEvalSupport.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.api.query.Producer|1 +kgram/api/core/DatatypeValue.java|IMPORT|fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException|1 +kgram/api/core/Edge.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/api/core/Expr.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/api/core/Filter.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Expression|1 +kgram/api/core/Node.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/api/core/PointerType.java|STATIC_IMPORT|fr.inria.corese.core.kgram.api.core.ExpType.DT|1 +kgram/api/core/Regex.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/api/core/TripleStore.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/api/query/Environment.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/api/query/Environment.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.ASTExtension|1 +kgram/api/query/ProcessVisitor.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/api/query/Producer.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/CompleteSPARQL.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/Eval.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/Eval.java|IMPORT|fr.inria.corese.core.sparql.datatype.DatatypeMap|1 +kgram/core/Eval.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.api.query.ProcessVisitor|1 +kgram/core/EvalSPARQL.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/Exp.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Expression|1 +kgram/core/Mapping.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/Mappings.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/Memory.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/Memory.java|IMPORT|fr.inria.corese.core.sparql.triple.function.term.Binding|1 +kgram/core/Memory.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.ASTExtension|1 +kgram/core/Memory.java|QUALIFIED_REFERENCE|fr.inria.corese.core.kgram.api.core.Expr|1 +kgram/core/ProcessVisitorDefault.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/core/ProcessVisitorDefault.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Metadata|1 +kgram/core/Query.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.*|1 +kgram/filter/Extension.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/filter/FilterPattern.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/tool/EnvironmentImpl.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/tool/EnvironmentImpl.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.ASTExtension|1 +kgram/tool/KgramNodes.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/tool/KgramNodes.java|IMPORT|fr.inria.corese.core.sparql.datatype.DatatypeMap|1 +kgram/tool/NodeImpl.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/tool/NodeImpl.java|IMPORT|fr.inria.corese.core.sparql.datatype.DatatypeMap|1 +kgram/tool/NodeImpl.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Atom|1 +kgram/tool/NodeImpl.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Constant|1 +kgram/tool/NodeImpl.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Variable|1 +kgram/tool/ProducerDefault.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/tool/StorageManagerKgramValues.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/tool/StorageManagerKgramValues.java|IMPORT|fr.inria.corese.core.sparql.datatype.DatatypeMap|1 +kgram/tool/StorageManagerKgramValues.java|IMPORT|fr.inria.corese.core.sparql.triple.parser.Constant|1 +kgram/tool/StorageManagerProducer.java|IMPORT|fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory|1 +kgram/tool/StorageManagerProducer.java|IMPORT|fr.inria.corese.core.sparql.api.IDatatype|1 +kgram/tool/StorageManagerProducer.java|QUALIFIED_REFERENCE|fr.inria.corese.core.sparql.triple.parser.ASTExtension|1