ServiceNpsUnionFuser.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.sparql.ir.util.transform;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.rdf4j.query.algebra.Var;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrBGP;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrGraph;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrNode;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrPathTriple;
import org.eclipse.rdf4j.queryrender.sparql.ir.IrUnion;
/**
* Helper to fuse a UNION of two bare NPS path triples in a SERVICE body into a single negated property set triple.
*
* Shape fused: - { { ?s !ex:p ?o } UNION { ?o !ex:q ?s } } => { ?s !(ex:p|^ex:q) ?o } - { { ?s !ex:p ?o } UNION { ?s
* !ex:q ?o } } => { ?s !(ex:p|ex:q) ?o }
*/
public final class ServiceNpsUnionFuser {
private ServiceNpsUnionFuser() {
}
public static IrBGP fuse(IrBGP bgp) {
if (bgp == null || bgp.getLines().isEmpty()) {
return bgp;
}
// Exact-body UNION case
if (bgp.getLines().size() == 1 && bgp.getLines().get(0) instanceof IrUnion) {
IrNode fused = tryFuseUnion((IrUnion) bgp.getLines().get(0));
if ((fused instanceof IrPathTriple || fused instanceof IrGraph)) {
IrBGP nw = new IrBGP(bgp.isNewScope());
nw.add(fused);
return nw;
}
if (fused instanceof IrBGP) {
// If the fuser already produced a BGP (should be rare after not preserving new-scope),
// use it directly to avoid introducing nested brace layers.
return (IrBGP) fused;
}
}
// Inline UNION case: scan and replace
boolean replaced = false;
List<IrNode> out = new ArrayList<>();
for (IrNode ln : bgp.getLines()) {
if (ln instanceof IrUnion) {
IrNode fused = tryFuseUnion((IrUnion) ln);
if ((fused instanceof IrPathTriple || fused instanceof IrGraph)) {
out.add(fused);
replaced = true;
continue;
}
if (fused instanceof IrBGP) {
out.add(fused);
replaced = true;
continue;
}
}
out.add(ln);
}
if (!replaced) {
return bgp;
}
IrBGP nw = new IrBGP(bgp.isNewScope());
out.forEach(nw::add);
return nw;
}
private static IrNode tryFuseUnion(IrUnion u) {
if (u == null || u.getBranches().size() != 2) {
return u;
}
// Respect explicit UNION new scopes: only fuse when both branches share an _anon_path_* variable
// under an allowed role mapping (s-s, s-o, o-s, o-p). Otherwise, preserve the UNION.
if (BaseTransform.unionIsExplicitAndAllBranchesScoped(u)) {
return u;
}
// Robustly unwrap each branch: allow nested single-child BGP groups and an optional GRAPH wrapper.
// holder for extracted branch shape
Branch b1 = extractBranch(u.getBranches().get(0));
Branch b2 = extractBranch(u.getBranches().get(1));
if (b1 == null || b2 == null) {
return u;
}
IrPathTriple p1 = b1.pt;
IrPathTriple p2 = b2.pt;
Var graphRef = b1.graph;
// Graph refs must match (both null or equal)
if ((graphRef == null && b2.graph != null) || (graphRef != null && b2.graph == null)
|| (graphRef != null && !eqVarOrValue(graphRef, b2.graph))) {
return u;
}
Var sCanon = p1.getSubject();
Var oCanon = p1.getObject();
// Normalize compact NPS forms
String m1 = BaseTransform.normalizeCompactNps(p1.getPathText());
String m2 = BaseTransform.normalizeCompactNps(p2.getPathText());
if (m1 == null || m2 == null) {
return u;
}
// Align branch 2 orientation to branch 1
String add2 = m2;
if (eqVarOrValue(sCanon, p2.getObject()) && eqVarOrValue(oCanon, p2.getSubject())) {
String inv = BaseTransform.invertNegatedPropertySet(m2);
if (inv == null) {
return u;
}
add2 = inv;
} else if (!(eqVarOrValue(sCanon, p2.getSubject()) && eqVarOrValue(oCanon, p2.getObject()))) {
return u;
}
String merged = BaseTransform.mergeNpsMembers(m1, add2);
Set<Var> pv = new HashSet<>();
pv.addAll(p1.getPathVars());
pv.addAll(p2.getPathVars());
IrPathTriple fused = new IrPathTriple(sCanon, p1.getSubjectOverride(), merged, oCanon, p1.getObjectOverride(),
pv, u.isNewScope());
IrNode out = fused;
if (graphRef != null) {
IrBGP inner = new IrBGP(false);
inner.add(fused);
out = new IrGraph(graphRef, inner, false);
}
// Preserve explicit UNION new-scope grouping by wrapping the fused result in a grouped BGP.
if (u.isNewScope()) {
IrBGP grp = new IrBGP(false);
grp.add(out);
return grp;
}
return out;
}
/** extract a single IrPathTriple (possibly under a single GRAPH) from a branch consisting only of wrappers. */
private static Branch extractBranch(IrBGP b) {
Branch out = new Branch();
if (b == null || b.getLines() == null || b.getLines().isEmpty()) {
return null;
}
// unwrap chains of single-child BGPs
IrNode cur = singleChild(b);
while (cur instanceof IrBGP) {
IrNode inner = singleChild((IrBGP) cur);
if (inner == null) {
break;
}
cur = inner;
}
if (cur instanceof IrGraph) {
IrGraph g = (IrGraph) cur;
out.graph = g.getGraph();
cur = singleChild(g.getWhere());
while (cur instanceof IrBGP) {
IrNode inner = singleChild((IrBGP) cur);
if (inner == null) {
break;
}
cur = inner;
}
}
if (cur instanceof IrPathTriple) {
out.pt = (IrPathTriple) cur;
return out;
}
return null;
}
private static final class Branch {
Var graph;
IrPathTriple pt;
}
private static IrNode singleChild(IrBGP b) {
if (b == null) {
return null;
}
List<IrNode> ls = b.getLines();
if (ls == null || ls.size() != 1) {
return null;
}
return ls.get(0);
}
private static boolean eqVarOrValue(Var a, Var b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
if (a.hasValue() && b.hasValue()) {
return a.getValue().equals(b.getValue());
}
if (!a.hasValue() && !b.hasValue()) {
String an = a.getName();
String bn = b.getName();
return an != null && an.equals(bn);
}
return false;
}
}