Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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}.
*
* <p>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<String> expected = readAllowlist(projectRoot.resolve(ALLOWLIST));
TreeSet<String> actual = scanDependencies(projectRoot.resolve(SOURCE_DIRECTORY));

TreeSet<String> added = new TreeSet<>(actual);
added.removeAll(expected);

TreeSet<String> 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<String> scanDependencies(Path sourceRoot) throws IOException {
List<Path> sourceFiles;
try (Stream<Path> 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<String, Integer> 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<String> result = new TreeSet<>();
dependencies.forEach((dependency, count) -> result.add(dependency + "|" + count));
return result;
}

private static void scanCompilationUnit(
Path sourceRoot,
CompilationUnitTree unit,
Map<String, Integer> 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<Void, Void>() {
@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<String, Integer> dependencies,
String path,
String kind,
String target) {
dependencies.merge(path + "|" + kind + "|" + target, 1, Integer::sum);
}

private static TreeSet<String> readAllowlist(Path allowlist) throws IOException {
TreeSet<String> 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<String> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://example.org/graph1>
WHERE { <http://example.org/alice> <http://example.org/knows> ?o }
""");

assertTrue(result.hasNext());
assertEquals(BOB, result.next().getValue("o").stringValue());
assertFalse(result.hasNext());
}

// -------------------------------------------------------------------------
// CONSTRUCT / graph evaluation
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -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<String> 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 <http://example.org/knows> ?o }
ORDER BY ?o
""");

List<String> objects = result.stream()
.map(binding -> binding.getValue("o").stringValue())
.toList();
assertEquals(List.of(aaron, BOB), objects);
}

// -------------------------------------------------------------------------
// Timeout
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -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
}

// -------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Loading