TupleExprIRRendererExplorationTest.java
/*******************************************************************************
* Copyright (c) 2025 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
******************************************************************************/
package org.eclipse.rdf4j.queryrender;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.eclipse.rdf4j.query.MalformedQueryException;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.parser.ParsedQuery;
import org.eclipse.rdf4j.query.parser.QueryParserUtil;
import org.eclipse.rdf4j.queryrender.sparql.TupleExprIRRenderer;
import org.eclipse.rdf4j.queryrender.sparql.TupleExprToIrConverter;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrSelect;
import org.eclipse.rdf4j.queryrender.sparql.ir.util.IrDebug;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* Exploration tests: parse selected SPARQL queries, dump their TupleExpr, convert to IR and dump the IR, render back to
* SPARQL, and dump the rendered TupleExpr. Artifacts are written to surefire-reports for inspection.
*
* These tests are intentionally permissive (no strict textual assertions) and are meant to aid root-cause analysis and
* to stabilize future transforms.
*/
public class TupleExprIRRendererExplorationTest {
private static final String SPARQL_PREFIX = "PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>\n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>\n" +
"PREFIX foaf: <http://xmlns.com/foaf/0.1/>\n" +
"PREFIX ex: <http://ex/>\n" +
"PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>\n";
private static TupleExprIRRenderer.Config cfg() {
TupleExprIRRenderer.Config style = new TupleExprIRRenderer.Config();
style.prefixes.put("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
style.prefixes.put("rdfs", "http://www.w3.org/2000/01/rdf-schema#");
style.prefixes.put("foaf", "http://xmlns.com/foaf/0.1/");
style.prefixes.put("ex", "http://ex/");
style.prefixes.put("xsd", "http://www.w3.org/2001/XMLSchema#");
style.valuesPreserveOrder = true;
return style;
}
private static TupleExpr parseAlgebra(String sparql) {
try {
ParsedQuery pq = QueryParserUtil.parseQuery(QueryLanguage.SPARQL, sparql, null);
return pq.getTupleExpr();
} catch (MalformedQueryException e) {
throw new MalformedQueryException(
"Failed to parse SPARQL query.\n###### QUERY ######\n" + sparql + "\n\n######################",
e);
}
}
private static void writeReportFile(String base, String label, String content) {
Path dir = Paths.get("target", "surefire-reports");
try {
Files.createDirectories(dir);
Path file = dir.resolve(base + "_" + label + ".txt");
Files.writeString(file, content == null ? "" : content, StandardCharsets.UTF_8);
} catch (IOException ioe) {
System.err.println("[explore] Failed to write " + label + ": " + ioe);
}
}
private static void dump(String baseName, String body, TupleExprIRRenderer.Config style) {
// 1) Original SPARQL + TupleExpr
String input = SPARQL_PREFIX + body;
TupleExpr te = parseAlgebra(input);
assertNotNull(te);
// 2) IR (transformed) via converter
TupleExprIRRenderer renderer = new TupleExprIRRenderer(style);
TupleExprToIrConverter conv = new TupleExprToIrConverter(renderer);
IrSelect ir = conv.toIRSelect(te);
// 3) Render back to SPARQL
String rendered = renderer.render(te, null).trim();
// 4) Parse rendered TupleExpr for comparison reference
TupleExpr teRendered;
try {
teRendered = parseAlgebra(rendered);
} catch (Throwable t) {
teRendered = null;
}
// 5) Write artifacts
writeReportFile(baseName, "SPARQL_input", input);
writeReportFile(baseName, "TupleExpr_input", VarNameNormalizer.normalizeVars(te.toString()));
writeReportFile(baseName, "IR_transformed", IrDebug.dump(ir));
writeReportFile(baseName, "SPARQL_rendered", rendered);
writeReportFile(baseName, "TupleExpr_rendered",
teRendered != null ? VarNameNormalizer.normalizeVars(teRendered.toString())
: "<rendered parse failed>\n" + rendered);
}
private static String render(String body, TupleExprIRRenderer.Config style) {
TupleExpr te = parseAlgebra(SPARQL_PREFIX + body);
return new TupleExprIRRenderer(style).render(te, null).trim();
}
private static String algebra(String sparql) {
TupleExpr te = parseAlgebra(sparql);
return VarNameNormalizer.normalizeVars(te.toString());
}
// Optional helper left in place for local checks; not used in exploratory tests
private static void assertSemanticRoundTrip(String body) {
}
@Test
@DisplayName("Explore: SERVICE body with UNION of bare NPS")
void explore_serviceUnionBareNps() {
String q = "SELECT ?s ?o WHERE {\n" +
" {\n" +
" SERVICE SILENT <http://federation.example/ep> {\n" +
" { ?s !ex:pA ?o . } UNION { ?o !<http://example.org/p/I1> ?s . }\n" +
" }\n" +
" }\n" +
"}";
dump("Exploration_serviceUnionBareNps", q, cfg());
// Exploratory: artifacts only; no strict assertions
}
@Test
@DisplayName("Explore: SERVICE + GRAPH branches with NPS UNION")
void explore_serviceGraphUnionBareNps() {
String q = "SELECT ?s ?o WHERE {\n" +
" {\n" +
" SERVICE SILENT <http://federation.example/ep> {\n" +
" { GRAPH <http://graphs.example/g0> { ?s !ex:pA ?o . } } UNION { GRAPH <http://graphs.example/g0> { ?o !<http://example.org/p/I1> ?s . } }\n"
+
" }\n" +
" }\n" +
"}";
dump("Exploration_serviceGraphUnionBareNps", q, cfg());
// Exploratory: artifacts only; no strict assertions
}
@Test
@DisplayName("Explore: SERVICE + VALUES/MINUS with NPS UNION")
void explore_serviceValuesMinusUnionBareNps() {
String q = "SELECT ?s ?o WHERE {\n" +
" {\n" +
" SERVICE SILENT <http://federation.example/ep> {\n" +
" { VALUES ?s { ex:s1 ex:s2 } { ?s ex:pB ?v0 . MINUS { { ?s !ex:pA ?o . } UNION { ?o !foaf:knows ?s . } } } }\n"
+
" }\n" +
" }\n" +
"}";
dump("Exploration_serviceValuesMinusUnionBareNps", q, cfg());
// Exploratory: artifacts only; no strict assertions
}
@Test
@DisplayName("Explore: nested SELECT with SERVICE + single path")
void explore_nestedSelectServiceSinglePath() {
String q = "SELECT ?s WHERE {\n" +
" { SELECT ?s WHERE {\n" +
" SERVICE SILENT <http://federation.example/ep> {\n" +
" { ?s ex:pZ ?o . }\n" +
" }\n" +
" } }\n" +
"}";
dump("Exploration_nestedSelectServiceSinglePath", q, cfg());
}
@Test
@DisplayName("Explore: FILTER EXISTS with GRAPH/OPTIONAL and NPS")
void explore_filterExistsGraphOptionalNps() {
String q = "SELECT ?s ?o WHERE {\n" +
" GRAPH <http://graphs.example/g1> { ?s ex:pC ?u1 . }\n" +
" FILTER EXISTS { { GRAPH <http://graphs.example/g1> { ?s ex:pA ?o . } OPTIONAL { GRAPH <http://graphs.example/g1> { ?s !(<http://example.org/p/I0>) ?o . } } } }\n"
+
"}";
dump("Exploration_filterExistsGraphOptionalNps", q, cfg());
}
}