From d1c3cab8d6e4ee490b7d5071dbb856bb8fd8f150 Mon Sep 17 00:00:00 2001 From: Sebastian Faubel <498891+faubulous@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:37:07 +0200 Subject: [PATCH] GH-4160: Apply the filter-disjunction rewrite only when the disjuncts are mutually exclusive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TransformFilterDisjunction rewrites filter(e1 || e2, P) into a disjunction that evaluates P once per disjunct, so a solution satisfying k disjuncts is returned k times where the filter returns it once. FILTER(?x = :c || ?x = :c) returns every matching row twice under the default optimizer, and FILTER(?x = :c || ?x != :d) returns rows with ?x = :c twice; disabling optFilterDisjunction restores the correct multiset. Several existing algebra tests pinned the unsound expansions; their expectation is now that the filter is left alone. Two changes together restore filter semantics: Repeated disjuncts are dropped first — (A || A) is A — so the degenerate duplicate that generated queries really contain (LDBC SPB emits FILTER(?pf = :c || ?pf = :c)) collapses to a single equality that grounds the pattern, rather than being either doubled (before) or left unoptimized (declined). Both = and sameTerm are symmetric, so disjuncts are compared on the operator, the variable and the constant rather than on argument order, and the two writings of one test collapse as well. The expansion is then applied only when every remaining disjunct tests one and the same variable against a constant (= or sameTerm) and no one term can satisfy two of those tests, which makes the rewrite an exact partition. Two sameTerm disjuncts are exclusive exactly when the terms differ: sameTerm matches by term, and value distinctness is not enough, because NaN is not value-equal to itself while every term equal to NaN satisfies both disjuncts - FILTER(sameTerm(?x, "NaN"^^ xsd:double) || sameTerm("NaN"^^xsd:double, ?x)) returned its row twice. Where a disjunct is =, a solution satisfying both makes the constants value-equal, so NodeValue.notSameValueAs decides, with an indeterminate comparison treated as possibly equal. The motivating case — ?x IN (...) over distinct constants, including mixed IRI/literal lists — keeps its expansion; every other disjunction is now evaluated as the filter it is. IRIs and blank nodes compare by term (NVCompare.sameValueAs is sameTerm for those value spaces), so they are checked for distinctness by set membership and a long ?x IN (:a, :b, ...) list costs O(n). Literals have no canonical representative - numeric comparison promotes to the wider of the two operand types, which makes value equality a property of the pair and not transitive - so pairs involving a literal stay pairwise. Co-Authored-By: Claude Fable 5 Co-Authored-By: Claude Opus 5 (1M context) --- .../optimize/TransformFilterDisjunction.java | 193 +++++++++++++-- .../optimize/TestTransformFilters.java | 220 +++++++++++++++--- 2 files changed, 370 insertions(+), 43 deletions(-) diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/optimize/TransformFilterDisjunction.java b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/optimize/TransformFilterDisjunction.java index 44d4614c63e..3eb703f6daf 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/optimize/TransformFilterDisjunction.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/optimize/TransformFilterDisjunction.java @@ -27,13 +27,20 @@ import java.util.Set ; import org.apache.jena.atlas.logging.Log ; +import org.apache.jena.graph.Node ; import org.apache.jena.sparql.algebra.Op ; import org.apache.jena.sparql.algebra.TransformCopy ; import org.apache.jena.sparql.algebra.op.OpDisjunction ; import org.apache.jena.sparql.algebra.op.OpFilter ; +import org.apache.jena.sparql.core.Var ; +import org.apache.jena.sparql.expr.E_Equals ; import org.apache.jena.sparql.expr.E_LogicalOr ; +import org.apache.jena.sparql.expr.E_SameTerm ; import org.apache.jena.sparql.expr.Expr ; +import org.apache.jena.sparql.expr.ExprEvalException ; +import org.apache.jena.sparql.expr.ExprFunction2 ; import org.apache.jena.sparql.expr.ExprList ; +import org.apache.jena.sparql.expr.NodeValue ; /** * Filter disjunction. This covers the case of @@ -42,6 +49,14 @@ * where either or both of {@code expr1} and {@code expr2} are equalities that help * ground the pattern. This includes {@code ?x IN (....)} so this optimization can a * significant improvement. + *

+ * The rewrite evaluates the pattern once per disjunct, so it is only sound when at + * most one disjunct can be true of any one solution; otherwise a solution satisfying + * several disjuncts is returned once per satisfied disjunct where the filter returns + * it once. Repeated disjuncts are dropped first, and the rewrite is then applied only + * when every disjunct tests the same variable against a constant ({@code =} or + * {@code sameTerm}) and no one term can satisfy two of those tests. Any other + * disjunction is left as a filter. */ public class TransformFilterDisjunction extends TransformCopy { @@ -66,9 +81,6 @@ public Op transform(OpFilter opFilter, final Op subOp) { ExprList exprList2 = new ExprList(); Op newOp = subOp; - // remember what's been seen so that FILTER(?x = || ?x = ) does not - // result in two transforms. - Set doneSoFar = new HashSet<>(); for ( Expr expr : exprList ) { if ( !isDisjunction(expr) ) { @@ -77,26 +89,23 @@ public Op transform(OpFilter opFilter, final Op subOp) { continue; } -// // Relies on expression equality. -// if ( doneSoFar.contains(expr) ) -// continue ; -// // Must be canonical: ?x = is the same as = ?x -// doneSoFar.add(expr) ; - Op op2 = expandDisjunction(expr, newOp); - if ( op2 != null ) - newOp = op2; + if ( op2 == null ) { + // A disjunction this transform can not rewrite soundly. + // Leave it as a filter expression. + exprList2.add(expr); + continue; + } + newOp = op2; } + if ( newOp == subOp ) + // No disjunction was expanded. + return super.transform(opFilter, subOp); + if ( exprList2.isEmpty() ) return newOp; - // There should have been at least on disjunction. - if ( newOp == subOp ) { - Log.warn(this, "FilterDisjunction assumption failure: didn't find a disjunction after all"); - return super.transform(opFilter, subOp); - } - // Put the non-disjunctions outside the disjunction and the pattern rewrite. Op opOther = OpFilter.filterBy(exprList2, newOp); if ( opOther instanceof OpFilter ) { @@ -113,9 +122,27 @@ private boolean isDisjunction(Expr expr) { return (expr instanceof E_LogicalOr); } + /** + * Expand a disjunction into a union of the pattern grounded per disjunct, or null + * when that is not possible or not sound. + */ public static Op expandDisjunction(Expr expr, Op subOp) { List exprList = explodeDisjunction(new ArrayList(), expr); + // (A || A) is A: drop repeated disjuncts rather than build identical branches. + // Generated queries really do contain the same disjunct twice - LDBC SPB writes + // FILTER(?pf = :c || ?pf = :c) - and a single disjunct then grounds the pattern. + List distinct = new ArrayList<>(exprList.size()); + Set seen = new HashSet<>(); + for ( Expr e : exprList ) { + if ( seen.add(dedupKey(e)) ) + distinct.add(e); + } + exprList = distinct; + + if ( !isSafeDisjunction(exprList) ) + return null; + // All disjunctions - some can be done efficiently via assignments, // some can not (value tests). List exprList2 = null; @@ -148,6 +175,138 @@ public static Op expandDisjunction(Expr expr, Op subOp) { return op; } + /** + * Is at most one disjunct true of any one solution? Each branch of the expansion + * re-evaluates the pattern, so a solution that satisfies {@code k} disjuncts comes + * back {@code k} times where the filter returns it once. For example + *
+     *  FILTER(?x = :c || ?x != :d)
+ * must not be expanded: a solution with {@code ?x = :c} satisfies both disjuncts. + *

+ * The safe case is: every disjunct tests one and the same variable against a + * constant, and the constants are pairwise known not to be satisfied by the same + * term. Constants whose comparison is indeterminate (an unknown datatype, a + * timezone-less date) are treated as possibly equal. + */ + private static boolean isSafeDisjunction(List exprList) { + Var var = null; + List constants = new ArrayList<>(exprList.size()); + for ( Expr e : exprList ) { + NodeValue constant = constantTestedAgainst(e, var); + if ( constant == null ) + return false; + if ( var == null ) + var = singleVariable(e); + constants.add(constant); + } + + // This runs once per query at optimize time, not per solution, but ?x IN (...) + // lists can be long in generated queries so avoid the pairwise comparison where + // there is a cheaper test. For IRIs and blank nodes NVCompare.sameValueAs is + // sameTerm, so they have a canonical representative and distinctness is set + // membership. Literals have no such representative - numeric comparison promotes + // to the wider of the two types, which makes value equality a property of the + // pair and not transitive - so any pair involving one stays pairwise. + List literals = new ArrayList<>(); + List nonLiterals = new ArrayList<>(); + Set distinctTerms = new HashSet<>(); + for ( int i = 0 ; i < constants.size() ; i++ ) { + NodeValue nv = constants.get(i); + if ( nv.isIRI() || nv.isBlank() ) { + if ( !distinctTerms.add(nv.asNode()) ) + // The same term twice: both disjuncts are true of that term. + return false; + nonLiterals.add(i); + } else + literals.add(i); + } + + for ( int a = 0 ; a < literals.size() ; a++ ) { + int i = literals.get(a); + for ( int b = a + 1 ; b < literals.size() ; b++ ) { + if ( !provablyExclusive(exprList, constants, i, literals.get(b)) ) + return false; + } + for ( int j : nonLiterals ) { + if ( !provablyExclusive(exprList, constants, i, j) ) + return false; + } + } + return true; + } + + /** + * Can no one term satisfy both disjuncts? {@code sameTerm} matches by term, so two + * {@code sameTerm} tests exclude each other exactly when the terms differ; value + * distinctness is not enough because {@code NaN} is not value-equal to itself, yet + * every term equal to {@code NaN} satisfies both. Where at least one disjunct is + * {@code =}, a solution satisfying both makes the constants value-equal, so proving + * the values different proves the disjuncts exclusive. + */ + private static boolean provablyExclusive(List exprList, List constants, int i, int j) { + return provablyExclusive(exprList.get(i), constants.get(i), exprList.get(j), constants.get(j)); + } + + /*package*/ static boolean provablyExclusive(Expr e1, NodeValue nv1, Expr e2, NodeValue nv2) { + if ( e1 instanceof E_SameTerm && e2 instanceof E_SameTerm ) + return !nv1.asNode().equals(nv2.asNode()); + return provablyDistinctValues(nv1, nv2); + } + + /** + * A key equating disjuncts that are the same test. {@code =} and {@code sameTerm} + * are symmetric, so a variable/constant test keys on the operator, the variable and + * the constant and not on the argument order - FILTER(sameTerm(?x, :c) || + * sameTerm(:c, ?x)) is one test written twice. Any other shape keys on itself. + */ + private static Object dedupKey(Expr e) { + NodeValue constant = constantTestedAgainst(e, null); + if ( constant == null ) + return e; + return List.of(e.getClass(), singleVariable(e), constant.asNode()); + } + + /** + * The constant of a {@code variable = constant} or {@code sameTerm(variable, constant)} + * disjunct (either argument order), where the variable is {@code var} - or any + * variable when {@code var} is null. Null when the disjunct has another shape. + */ + private static NodeValue constantTestedAgainst(Expr e, Var var) { + if ( !(e instanceof E_Equals) && !(e instanceof E_SameTerm) ) + return null; + ExprFunction2 test = (ExprFunction2)e; + Expr left = test.getArg1(); + Expr right = test.getArg2(); + Expr varExpr = null; + Expr constExpr = null; + if ( left.isVariable() && right.isConstant() ) { + varExpr = left; + constExpr = right; + } else if ( right.isVariable() && left.isConstant() ) { + varExpr = right; + constExpr = left; + } else + return null; + if ( var != null && !var.equals(varExpr.asVar()) ) + return null; + return constExpr.getConstant(); + } + + /** The variable of a disjunct {@link #constantTestedAgainst} accepted. */ + private static Var singleVariable(Expr e) { + ExprFunction2 test = (ExprFunction2)e; + return test.getArg1().isVariable() ? test.getArg1().asVar() : test.getArg2().asVar(); + } + + /*package*/ static boolean provablyDistinctValues(NodeValue nv1, NodeValue nv2) { + try { + return NodeValue.notSameValueAs(nv1, nv2); + } catch (ExprEvalException ex) { + // Indeterminate comparison: can not prove the disjuncts mutually exclusive. + return false; + } + } + /** Explode an expr into a list of disjunctions */ private static List explodeDisjunction(List exprList, Expr expr) { if ( !(expr instanceof E_LogicalOr) ) { diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformFilters.java b/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformFilters.java index 53163864d4d..db6f183446b 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformFilters.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/algebra/optimize/TestTransformFilters.java @@ -23,14 +23,33 @@ import static org.apache.jena.sparql.algebra.optimize.TransformTests.check; import static org.apache.jena.sparql.algebra.optimize.TransformTests.testOp; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; +import org.apache.jena.atlas.iterator.Iter; import org.apache.jena.atlas.lib.StrUtils; +import org.apache.jena.datatypes.xsd.XSDDatatype; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.rdf.model.ModelFactory; import org.apache.jena.sparql.algebra.Op; import org.apache.jena.sparql.algebra.Transform; import org.apache.jena.sparql.algebra.TransformCopy; import org.apache.jena.sparql.algebra.op.OpTable; +import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.expr.E_Equals; +import org.apache.jena.sparql.expr.E_SameTerm; +import org.apache.jena.sparql.expr.Expr; +import org.apache.jena.sparql.expr.ExprVar; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.sse.SSE; /** Tests of transforms related to filters */ public class TestTransformFilters @@ -272,23 +291,18 @@ public Op transform(OpTable opTable) { ")"); } + // A solution with ?x = satisfies both disjuncts, so expanding would return + // it from both branches where the filter returns it once. Not transformed. @Test public void disjunction02() { testOp("(filter (|| (= ?x ) (!= ?x )) (bgp ( ?s ?p ?x)) )", t_disjunction, - "(disjunction ", - "(assign ((?x )) (bgp ( ?s ?p )))", - "(filter (!= ?x ) (bgp ( ?s ?p ?x)))", - ")"); + (String[])null); } @Test public void disjunction03() { testOp("(filter (|| (!= ?x ) (= ?x )) (bgp ( ?s ?p ?x)) )", t_disjunction, - // Note - reordering of disjunction terms. - "(disjunction ", - "(assign ((?x )) (bgp ( ?s ?p )))", - "(filter (!= ?x ) (bgp ( ?s ?p ?x)))", - ")"); + (String[])null); } @Test public void disjunction04() { @@ -300,33 +314,187 @@ public Op transform(OpTable opTable) { @Test public void disjunction05() { testOp("(filter (exprlist (|| (= ?x ) (!= ?x ))) (bgp ( ?s ?p ?x)) )", t_disjunction, - " (disjunction", - " (assign ((?x )) (bgp ( ?s ?p )))", - " (filter (!= ?x ) (bgp ( ?s ?p ?x)))", - ")" - ); + (String[])null); } @Test public void disjunction06() { testOp("(filter (exprlist (lang ?x) (|| (= ?x ) (!= ?x ))) (bgp ( ?s ?p ?x)) )", t_disjunction, - "(filter (lang ?x)", - " (disjunction", - " (assign ((?x )) (bgp ( ?s ?p )))", - " (filter (!= ?x ) (bgp ( ?s ?p ?x)))", - "))" - ); + (String[])null); } @Test public void disjunction07() { testOp("(filter (exprlist (|| (= ?x ) (!= ?x )) (lang ?x) ) (bgp ( ?s ?p ?x)) )", t_disjunction, - "(filter (lang ?x)", - " (disjunction", - " (assign ((?x )) (bgp ( ?s ?p )))", - " (filter (!= ?x ) (bgp ( ?s ?p ?x)))", - "))" - ); + (String[])null); + } + + // (A || A) is A: the repeated disjunct is dropped — two identical branches would + // return every solution twice — and the single equality then grounds the pattern. + @Test public void disjunction08() { + testOp("(filter (|| (= ?x ) (= ?x )) (bgp ( ?s ?p ?x)) )", + t_disjunction, + "(assign ((?x )) (bgp ( ?s ?p )))"); + } + + // The same, for a disjunct shape the transform cannot ground: deduplicated to a + // single disjunct, there is nothing to expand and the filter is left alone. + @Test public void disjunction08a() { + testOp("(filter (|| (!= ?x ) (!= ?x )) (bgp ( ?s ?p ?x)) )", + t_disjunction, + (String[])null); + } + + // Different terms, same value: a solution with ?x = 1 satisfies both disjuncts. + @Test public void disjunction09() { + testOp("(filter (|| (= ?x 1) (= ?x \"01\"^^)) (bgp ( ?s ?p ?x)) )", + t_disjunction, + (String[])null); + } + + // Different variables: a solution can satisfy both disjuncts. + @Test public void disjunction10() { + testOp("(filter (|| (= ?x ) (= ?y )) (bgp ( ?s ?p ?x) (?s ?q ?y)) )", + t_disjunction, + (String[])null); + } + + // Simple literals are pairwise distinct values, so the expansion is sound. + @Test public void disjunction11() { + testOp("(filter (|| (= ?x \"a\") (= ?x \"b\")) (bgp ( ?s ?p ?x)) )", + t_disjunction, + "(disjunction ", + "(assign ((?x \"a\")) (bgp ( ?s ?p \"a\")))", + "(assign ((?x \"b\")) (bgp ( ?s ?p \"b\")))", + ")"); + } + + // An IRI and a number are distinct values, so the ?x IN ( 2) shape keeps its + // expansion; the numeric equality is not substitutable and stays a filter branch. + @Test public void disjunction12() { + testOp("(filter (|| (= ?x ) (= ?x 2)) (bgp ( ?s ?p ?x)) )", + t_disjunction, + "(disjunction ", + "(assign ((?x )) (bgp ( ?s ?p )))", + "(filter (= ?x 2) (bgp ( ?s ?p ?x)))", + ")"); + } + + // The expansion must not change the number of results: each solution once, + // however many disjuncts it satisfies. Executed, not matched on plan shape, + // with the default optimizer. + @Test public void disjunctionMultiplicity01() { + checkDisjunctionRowCount("FILTER(?x = || ?x = )", 1); + } + + // Only s1 qualifies (x=a passes both disjuncts; x=b passes neither) — and it must + // come back once. The unsound expansion returned it from both branches. + @Test public void disjunctionMultiplicity02() { + checkDisjunctionRowCount("FILTER(?x = || ?x != )", 1); + } + + @Test public void disjunctionMultiplicity03() { + checkDisjunctionRowCount("FILTER(?x = || ?x = )", 2); + } + + private static void checkDisjunctionRowCount(String filter, int expected) { + Graph graph = SSE.parseGraph(StrUtils.strjoinNL + ("(graph" + ," (triple )" + ," (triple )" + ,")")); + String queryString = "SELECT * { ?s ?x " + filter + " }"; + Query query = QueryFactory.create(queryString); + try ( QueryExecution qExec = QueryExecutionFactory.create(query, ModelFactory.createModelForGraph(graph)) ) { + long count = Iter.count(qExec.execSelect()); + assertEquals(expected, count, ()->"Row count differs from filter semantics: "+filter); + } + } + + // GH-4160: the exclusivity test must not judge sameTerm disjuncts by value + // distinctness. NaN is not value-equal to itself, so the two symmetric writings of + // one sameTerm test were taken to be mutually exclusive and the row came back twice. + @Test public void disjunctionMultiplicity04() { + checkRowCount(numericGraph(), + "FILTER( sameTerm(?x, \"NaN\"^^xsd:double) || sameTerm(\"NaN\"^^xsd:double, ?x) )", + 1); + } + + // The mixed IRI/literal case still expands - the IRI branch grounds the pattern and + // the double is left as a filter - and returns each solution once. + @Test public void disjunctionMultiplicity05() { + checkRowCount(numericGraph(), + "FILTER( ?x = \"1.5\"^^xsd:double || ?x = )", + 1); + } + + private static Graph numericGraph() { + return SSE.parseGraph(StrUtils.strjoinNL + ("(graph" + ," (triple \"NaN\"^^xsd:double)" + ," (triple \"1.5\"^^xsd:double)" + ,")")); + } + + private static void checkRowCount(Graph graph, String filter, int expected) { + String queryString = "PREFIX xsd: \n" + + "SELECT * { ?s ?x " + filter + " }"; + Query query = QueryFactory.create(queryString); + try ( QueryExecution qExec = QueryExecutionFactory.create(query, ModelFactory.createModelForGraph(graph)) ) { + long count = Iter.count(qExec.execSelect()); + assertEquals(expected, count, ()->"Row count differs from filter semantics: "+filter); + } + } + + private static NodeValue nvDouble(String lex) { return NodeValue.makeNode(NodeFactory.createLiteralDT(lex, XSDDatatype.XSDdouble)); } + private static NodeValue nvInteger(String lex) { return NodeValue.makeNode(NodeFactory.createLiteralDT(lex, XSDDatatype.XSDinteger)); } + private static NodeValue nvDateTime(String lex) { return NodeValue.makeNode(NodeFactory.createLiteralDT(lex, XSDDatatype.XSDdateTime)); } + private static NodeValue nvIRI(String uri) { return NodeValue.makeNode(NodeFactory.createURI(uri)); } + + // provablyDistinctValues is value distinctness. NaN is not value-equal to itself - + // the reason sameTerm disjuncts may not be judged by it. + @Test public void provablyDistinctValuesNaN() { + assertTrue(TransformFilterDisjunction.provablyDistinctValues(nvDouble("NaN"), nvDouble("NaN"))); + } + + // Value-equal constants with different terms: not distinct, so not exclusive. + @Test public void provablyDistinctValuesSameValue() { + assertFalse(TransformFilterDisjunction.provablyDistinctValues(nvInteger("1"), nvInteger("01"))); + assertFalse(TransformFilterDisjunction.provablyDistinctValues(nvInteger("1"), nvDouble("1.0"))); + } + + @Test public void provablyDistinctValuesIRIs() { + assertTrue(TransformFilterDisjunction.provablyDistinctValues(nvIRI("http://example/a"), nvIRI("http://example/b"))); + assertFalse(TransformFilterDisjunction.provablyDistinctValues(nvIRI("http://example/a"), nvIRI("http://example/a"))); + } + + // A timezone-less dateTime against one with a timezone is indeterminate, so the + // constants can not be proved different. + @Test public void provablyDistinctValuesIndeterminate() { + assertFalse(TransformFilterDisjunction.provablyDistinctValues(nvDateTime("2000-01-01T00:00:00"), + nvDateTime("2000-01-01T00:00:00Z"))); + } + + // sameTerm matches by term: identical terms are never exclusive, and differing + // terms always are, whatever the value comparison says. + @Test public void provablyExclusiveSameTerm() { + Expr vx = new ExprVar(Var.alloc("x")); + NodeValue nan = nvDouble("NaN"); + assertFalse(TransformFilterDisjunction.provablyExclusive(new E_SameTerm(vx, nan), nan, + new E_SameTerm(nan, vx), nan)); + NodeValue i1 = nvInteger("1"), i01 = nvInteger("01"); + assertTrue(TransformFilterDisjunction.provablyExclusive(new E_SameTerm(vx, i1), i1, + new E_SameTerm(vx, i01), i01)); + } + + // Where a disjunct is "=", value distinctness is the right test. + @Test public void provablyExclusiveEquals() { + Expr vx = new ExprVar(Var.alloc("x")); + NodeValue i1 = nvInteger("1"), i01 = nvInteger("01"); + assertFalse(TransformFilterDisjunction.provablyExclusive(new E_Equals(vx, i1), i1, + new E_Equals(vx, i01), i01)); + assertTrue(TransformFilterDisjunction.provablyExclusive(new E_Equals(vx, nvIRI("http://example/a")), nvIRI("http://example/a"), + new E_SameTerm(vx, nvIRI("http://example/b")), nvIRI("http://example/b"))); } @Test public void oneOf1() {