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