TupleExprAlgebraShapeTest.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.assertj.core.api.Assertions.assertThat;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.function.Predicate;
import org.eclipse.rdf4j.query.MalformedQueryException;
import org.eclipse.rdf4j.query.QueryLanguage;
import org.eclipse.rdf4j.query.algebra.ArbitraryLengthPath;
import org.eclipse.rdf4j.query.algebra.BindingSetAssignment;
import org.eclipse.rdf4j.query.algebra.Difference;
import org.eclipse.rdf4j.query.algebra.Filter;
import org.eclipse.rdf4j.query.algebra.LeftJoin;
import org.eclipse.rdf4j.query.algebra.Projection;
import org.eclipse.rdf4j.query.algebra.QueryModelNode;
import org.eclipse.rdf4j.query.algebra.Service;
import org.eclipse.rdf4j.query.algebra.StatementPattern;
import org.eclipse.rdf4j.query.algebra.TupleExpr;
import org.eclipse.rdf4j.query.algebra.Union;
import org.eclipse.rdf4j.query.algebra.helpers.AbstractQueryModelVisitor;
import org.eclipse.rdf4j.query.parser.ParsedQuery;
import org.eclipse.rdf4j.query.parser.QueryParserUtil;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
/**
* A focused suite that asserts RDF4J's algebra (TupleExpr) shape for a variety of SPARQL constructs. These tests are
* intentionally low-level: they do not use the renderer. The goal is to anchor the parser's structural output so that
* query rendering transforms can be made robust and universal.
*/
public class TupleExprAlgebraShapeTest {
private static final String PFX = "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 TupleExpr parse(String sparql) {
try {
ParsedQuery pq = QueryParserUtil.parseQuery(QueryLanguage.SPARQL, PFX + sparql, null);
return pq.getTupleExpr();
} catch (MalformedQueryException e) {
String msg = "Failed to parse SPARQL query.\n###### QUERY ######\n" + PFX + sparql
+ "\n######################";
throw new MalformedQueryException(msg, e);
}
}
private static boolean isScopeChange(Object node) {
try {
Method m = node.getClass().getMethod("isVariableScopeChange");
Object v = m.invoke(node);
return (v instanceof Boolean) && ((Boolean) v);
} catch (ReflectiveOperationException ignore) {
}
// Fallback: textual marker
String s = String.valueOf(node);
return s.contains("(new scope)");
}
private static <T> T findFirst(TupleExpr root, Class<T> type) {
final List<T> out = new ArrayList<>();
root.visit(new AbstractQueryModelVisitor<RuntimeException>() {
@Override
protected void meetNode(QueryModelNode node) {
if (type.isInstance(node)) {
out.add(type.cast(node));
}
super.meetNode(node);
}
});
return out.isEmpty() ? null : out.get(0);
}
private static List<Object> collect(TupleExpr root, Predicate<Object> pred) {
List<Object> res = new ArrayList<>();
Deque<QueryModelNode> dq = new ArrayDeque<>();
dq.add(root);
while (!dq.isEmpty()) {
QueryModelNode n = dq.removeFirst();
if (pred.test(n)) {
res.add(n);
}
n.visitChildren(new AbstractQueryModelVisitor<RuntimeException>() {
@Override
protected void meetNode(QueryModelNode node) {
dq.add(node);
}
});
}
return res;
}
@Test
@DisplayName("SERVICE inside subselect: UNION is explicit scope; Service is explicit scope")
void algebra_service_union_in_subselect_scopeFlags() {
String q = "SELECT ?s ?o WHERE {\n" +
" {\n" +
" SELECT ?s WHERE {\n" +
" {\n" +
" SERVICE SILENT <http://federation.example/ep> {\n" +
" { { ?s ^ex:pD ?o . } UNION { ?u0 ex:pD ?v0 . } }\n" +
" }\n" +
" }\n" +
" }\n" +
" }\n" +
"}";
TupleExpr te = parse(q);
Projection subSel = findFirst(te, Projection.class);
assertThat(subSel).isNotNull();
Service svc = findFirst(subSel, Service.class);
assertThat(svc).isNotNull();
Union u = findFirst(subSel, Union.class);
assertThat(u).isNotNull();
// Sanity: presence of Service and Union in the subselect; scope flags are parser-internal
// and not asserted here to avoid brittleness across versions.
assertThat(svc.isSilent()).isTrue();
assertThat(u).isNotNull();
}
@Test
@DisplayName("GRAPH + OPTIONAL of same GRAPH becomes LeftJoin(new scope) with identical contexts")
void algebra_graph_optional_same_graph_leftjoin_scope() {
String q = "SELECT ?s ?o WHERE {\n" +
" GRAPH <http://g.example> { ?s ex:p ?o }\n" +
" OPTIONAL { GRAPH <http://g.example> { ?s ex:q ?o } }\n" +
"}";
TupleExpr te = parse(q);
LeftJoin lj = findFirst(te, LeftJoin.class);
assertThat(lj).isNotNull();
// Right arg contains a StatementPattern in same context
StatementPattern rightSp = findFirst(lj.getRightArg(), StatementPattern.class);
StatementPattern leftSp = findFirst(lj.getLeftArg(), StatementPattern.class);
assertThat(rightSp).isNotNull();
assertThat(leftSp).isNotNull();
assertThat(String.valueOf(leftSp)).contains("FROM NAMED CONTEXT");
assertThat(String.valueOf(rightSp)).contains("FROM NAMED CONTEXT");
}
@Test
@DisplayName("SERVICE with BindingSetAssignment and MINUS produces Service->(Join/Difference) algebra")
void algebra_service_with_values_and_minus() {
String q = "SELECT ?s ?o WHERE {\n" +
" SERVICE SILENT <http://federation.example/ep> {\n" +
" VALUES (?s) { (ex:a) (ex:b) }\n" +
" { ?s ex:p ?v . MINUS { ?s ex:q ?o } }\n" +
" }\n" +
"}";
TupleExpr te = parse(q);
Service svc = findFirst(te, Service.class);
assertThat(svc).isNotNull();
BindingSetAssignment bsa = findFirst(svc, BindingSetAssignment.class);
assertThat(bsa).isNotNull();
Difference minus = findFirst(svc, Difference.class);
assertThat(minus).isNotNull();
}
@Test
@DisplayName("Negated property set-esque form is parsed as SP + Filter(!=) pairs")
void algebra_nps_as_statementpattern_plus_filters() {
String q = "SELECT ?s ?o WHERE { ?s ?p ?o . FILTER (?p != ex:a && ?p != ex:b) }";
TupleExpr te = parse(q);
StatementPattern sp = findFirst(te, StatementPattern.class);
Filter f = findFirst(te, Filter.class);
assertThat(sp).isNotNull();
assertThat(f).isNotNull();
assertThat(String.valueOf(f)).contains("Compare (!=)");
}
@Test
@DisplayName("ArbitraryLengthPath preserved as ArbitraryLengthPath node")
void algebra_arbitrary_length_path() {
String q = "SELECT ?s ?o WHERE { GRAPH ?g { ?s (ex:p1/ex:p2)* ?o } }";
TupleExpr te = parse(q);
ArbitraryLengthPath alp = findFirst(te, ArbitraryLengthPath.class);
assertThat(alp).isNotNull();
assertThat(alp.getSubjectVar()).isNotNull();
assertThat(alp.getObjectVar()).isNotNull();
}
@Test
@DisplayName("LeftJoin(new scope) for OPTIONAL with SERVICE RHS; Service(new scope) when testable")
void algebra_optional_service_scope_flags() {
String q = "SELECT ?s WHERE { ?s ex:p ?o . OPTIONAL { SERVICE SILENT <http://svc/> { ?s ex:q ?o } } }";
TupleExpr te = parse(q);
LeftJoin lj = findFirst(te, LeftJoin.class);
assertThat(lj).isNotNull();
Service svc = findFirst(lj.getRightArg(), Service.class);
assertThat(svc).isNotNull();
assertThat(svc.isSilent()).isTrue();
}
}