Skip to content

Commit 1fd83f7

Browse files
authored
GH-5604 New Query Renderer implementation (#5601)
1 parent 7ce9837 commit 1fd83f7

170 files changed

Lines changed: 30161 additions & 717 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

core/model/src/main/java/org/eclipse/rdf4j/model/impl/SimpleValueFactory.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ public class SimpleValueFactory extends AbstractValueFactory {
4949
private final static String uniqueIdPrefix = UUID.randomUUID().toString().replace("-", "");
5050
private final static AtomicLong uniqueIdSuffix = new AtomicLong();
5151

52+
// Pre-built strings for lengths 0 through 9
53+
private static final String[] RANDOMIZE_LENGTH = new String[10];
54+
55+
static {
56+
StringBuilder sb = new StringBuilder();
57+
for (int i = 0; i <= 9; i++) {
58+
RANDOMIZE_LENGTH[i] = sb.toString();
59+
sb.append(i);
60+
}
61+
}
62+
5263
private static final DatatypeFactory datatypeFactory;
5364

5465
static {
@@ -130,7 +141,12 @@ public Triple createTriple(Resource subject, IRI predicate, Value object) {
130141

131142
@Override
132143
public BNode createBNode() {
133-
return createBNode(uniqueIdPrefix + uniqueIdSuffix.incrementAndGet());
144+
long l = uniqueIdSuffix.incrementAndGet();
145+
// reverse the string representation of the long to ensure that the BNode IDs are not monotonically increasing
146+
StringBuilder sb = new StringBuilder(Long.toString(l));
147+
sb.reverse();
148+
sb.append(uniqueIdPrefix).append(RANDOMIZE_LENGTH[(int) (Math.abs(l % RANDOMIZE_LENGTH.length))]);
149+
return createBNode(sb.toString());
134150
}
135151

136152
/**
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*******************************************************************************
2+
* Copyright (c) 2025 Eclipse RDF4J contributors.
3+
*
4+
* All rights reserved. This program and the accompanying materials
5+
* are made available under the terms of the Eclipse Distribution License v1.0
6+
* which accompanies this distribution, and is available at
7+
* http://www.eclipse.org/org/documents/edl-v10.php.
8+
*
9+
* SPDX-License-Identifier: BSD-3-Clause
10+
*******************************************************************************/
11+
12+
package org.eclipse.rdf4j.model.impl;
13+
14+
import java.lang.reflect.Field;
15+
import java.util.concurrent.atomic.AtomicLong;
16+
17+
import org.junit.jupiter.api.Test;
18+
19+
/**
20+
* Reproduces overflow in SimpleValueFactory#createBNode() when the atomic counter wraps to Long.MIN_VALUE, which
21+
* results in a negative index into the RANDOMIZE_LENGTH array and throws ArrayIndexOutOfBoundsException.
22+
*/
23+
public class SimpleValueFactoryOverflowTest {
24+
25+
@Test
26+
void overflowAtMinValue() throws Exception {
27+
// Access the private static counter
28+
Field counterField = SimpleValueFactory.class.getDeclaredField("uniqueIdSuffix");
29+
counterField.setAccessible(true);
30+
AtomicLong counter = (AtomicLong) counterField.get(null);
31+
32+
// Preserve original value to avoid leaking state across tests
33+
long original = counter.get();
34+
35+
synchronized (SimpleValueFactory.class) {
36+
try {
37+
// Force next increment to wrap from Long.MAX_VALUE to Long.MIN_VALUE
38+
counter.set(Long.MAX_VALUE);
39+
40+
SimpleValueFactory.getInstance().createBNode();
41+
} finally {
42+
// Restore the original value
43+
counter.set(original);
44+
}
45+
}
46+
}
47+
48+
@Test
49+
void overflowAtMaxValue() throws Exception {
50+
// Access the private static counter
51+
Field counterField = SimpleValueFactory.class.getDeclaredField("uniqueIdSuffix");
52+
counterField.setAccessible(true);
53+
AtomicLong counter = (AtomicLong) counterField.get(null);
54+
55+
// Preserve original value to avoid leaking state across tests
56+
long original = counter.get();
57+
58+
synchronized (SimpleValueFactory.class) {
59+
try {
60+
// Force next increment to wrap from Long.MAX_VALUE to Long.MIN_VALUE
61+
counter.set(Long.MIN_VALUE);
62+
63+
SimpleValueFactory.getInstance().createBNode();
64+
} finally {
65+
// Restore the original value
66+
counter.set(original);
67+
}
68+
}
69+
}
70+
}

core/query/src/main/java/org/eclipse/rdf4j/query/explanation/Explanation.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
@Experimental
2323
public interface Explanation {
2424

25+
Object tupleExpr();
26+
2527
/**
2628
* The different levels that the query explanation can be at.
2729
*

core/query/src/main/java/org/eclipse/rdf4j/query/explanation/ExplanationImpl.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,23 @@
2727
public class ExplanationImpl implements Explanation {
2828

2929
private final GenericPlanNode genericPlanNode;
30+
private final Object tupleExpr;
3031

31-
public ExplanationImpl(GenericPlanNode genericPlanNode, boolean timedOut) {
32+
public ExplanationImpl(GenericPlanNode genericPlanNode, boolean timedOut, Object tupleExpr) {
3233
this.genericPlanNode = genericPlanNode;
34+
this.tupleExpr = tupleExpr;
3335
if (timedOut) {
3436
genericPlanNode.setTimedOut(timedOut);
3537
}
3638
}
3739

3840
ObjectMapper objectMapper = new ObjectMapper();
3941

42+
@Override
43+
public Object tupleExpr() {
44+
return tupleExpr;
45+
}
46+
4047
@Override
4148
public GenericPlanNode toGenericPlanNode() {
4249
return genericPlanNode;

core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/ArrayBindingBasedQueryEvaluationContext.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,7 @@ public void meet(Var node) throws QueryEvaluationException {
353353
// We can skip constants that are only used in StatementPatterns since these are never added to the
354354
// BindingSet anyway
355355
if (!(node.isConstant() && node.getParentNode() instanceof StatementPattern)) {
356-
Var replacement = new Var(varNames.computeIfAbsent(node.getName(), k -> k), node.getValue(),
356+
Var replacement = Var.of(varNames.computeIfAbsent(node.getName(), k -> k), node.getValue(),
357357
node.isAnonymous(), node.isConstant());
358358
node.replaceWith(replacement);
359359
}

core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/DefaultEvaluationStrategy.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,8 +1252,32 @@ protected QueryValueEvaluationStep prepare(Coalesce node, QueryEvaluationContext
12521252

12531253
protected QueryValueEvaluationStep prepare(Compare node, QueryEvaluationContext context) {
12541254
boolean strict = QueryEvaluationMode.STRICT == getQueryEvaluationMode();
1255-
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1256-
.valueOf(QueryEvaluationUtil.compare(leftVal, rightVal, node.getOperator(), strict)), context);
1255+
1256+
Compare.CompareOp operator = node.getOperator();
1257+
switch (operator) {
1258+
case EQ:
1259+
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1260+
.valueOf(QueryEvaluationUtil.compareEQ(leftVal, rightVal, strict)), context);
1261+
case NE:
1262+
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1263+
.valueOf(QueryEvaluationUtil.compareNE(leftVal, rightVal, strict)), context);
1264+
case LT:
1265+
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1266+
.valueOf(QueryEvaluationUtil.compareLT(leftVal, rightVal, strict)), context);
1267+
case LE:
1268+
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1269+
.valueOf(QueryEvaluationUtil.compareLE(leftVal, rightVal, strict)), context);
1270+
case GE:
1271+
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1272+
.valueOf(QueryEvaluationUtil.compareGE(leftVal, rightVal, strict)), context);
1273+
case GT:
1274+
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1275+
.valueOf(QueryEvaluationUtil.compareGT(leftVal, rightVal, strict)), context);
1276+
default:
1277+
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
1278+
.valueOf(QueryEvaluationUtil.compare(leftVal, rightVal, node.getOperator(), strict)), context);
1279+
}
1280+
12571281
}
12581282

12591283
private BiFunction<Value, Value, Value> mathOperationApplier(MathExpr node,

core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/EvaluationStatistics.java

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ public class EvaluationStatistics {
4646
private final static String uniqueIdPrefix = UUID.randomUUID().toString().replace("-", "");
4747
private final static AtomicLong uniqueIdSuffix = new AtomicLong();
4848

49+
// Pre-built strings for lengths 0 through 9
50+
private static final String[] RANDOMIZE_LENGTH = new String[10];
51+
static {
52+
StringBuilder sb = new StringBuilder();
53+
for (int i = 0; i <= 9; i++) {
54+
RANDOMIZE_LENGTH[i] = sb.toString();
55+
sb.append(i);
56+
}
57+
}
58+
4959
private CardinalityCalculator calculator;
5060

5161
public double getCardinality(TupleExpr expr) {
@@ -66,6 +76,10 @@ protected CardinalityCalculator createCardinalityCalculator() {
6676
return new CardinalityCalculator();
6777
}
6878

79+
public boolean supportsJoinEstimation() {
80+
return false;
81+
}
82+
6983
/*-----------------------------------*
7084
* Inner class CardinalityCalculator *
7185
*-----------------------------------*/
@@ -117,7 +131,11 @@ public void meet(ZeroLengthPath node) {
117131

118132
@Override
119133
public void meet(ArbitraryLengthPath node) {
120-
final Var pathVar = new Var("_anon_" + uniqueIdPrefix + uniqueIdSuffix.incrementAndGet(), true);
134+
long suffix = uniqueIdSuffix.getAndIncrement();
135+
final Var pathVar = Var.of(
136+
"_anon_path_" + uniqueIdPrefix + suffix
137+
+ RANDOMIZE_LENGTH[(int) (Math.abs(suffix % RANDOMIZE_LENGTH.length))],
138+
true);
121139
// cardinality of ALP is determined based on the cost of a
122140
// single ?s ?p ?o ?c pattern where ?p is unbound, compensating for the fact that
123141
// the length of the path is unknown but expected to be _at least_ twice that of a normal

core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/impl/evaluationsteps/StatementPatternQueryEvaluationStep.java

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import org.eclipse.rdf4j.common.iteration.IndexReportingIterator;
2222
import org.eclipse.rdf4j.common.order.StatementOrder;
2323
import org.eclipse.rdf4j.model.IRI;
24+
import org.eclipse.rdf4j.model.Literal;
2425
import org.eclipse.rdf4j.model.Resource;
2526
import org.eclipse.rdf4j.model.Statement;
2627
import org.eclipse.rdf4j.model.Value;
28+
import org.eclipse.rdf4j.model.ValueFactory;
29+
import org.eclipse.rdf4j.model.base.CoreDatatype;
2730
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
2831
import org.eclipse.rdf4j.model.vocabulary.SESAME;
2932
import org.eclipse.rdf4j.query.BindingSet;
@@ -75,7 +78,6 @@ public class StatementPatternQueryEvaluationStep implements QueryEvaluationStep
7578
public StatementPatternQueryEvaluationStep(StatementPattern statementPattern, QueryEvaluationContext context,
7679
TripleSource tripleSource) {
7780
super();
78-
this.statementPattern = statementPattern;
7981
this.order = statementPattern.getStatementOrder();
8082
this.context = context;
8183
this.tripleSource = tripleSource;
@@ -106,6 +108,14 @@ public StatementPatternQueryEvaluationStep(StatementPattern statementPattern, Qu
106108
Var objVar = statementPattern.getObjectVar();
107109
Var conVar = statementPattern.getContextVar();
108110

111+
subjVar = replaceValueWithNewValue(subjVar, tripleSource.getValueFactory());
112+
predVar = replaceValueWithNewValue(predVar, tripleSource.getValueFactory());
113+
objVar = replaceValueWithNewValue(objVar, tripleSource.getValueFactory());
114+
conVar = replaceValueWithNewValue(conVar, tripleSource.getValueFactory());
115+
116+
this.statementPattern = new StatementPattern(statementPattern.getScope(), subjVar, predVar, objVar, conVar);
117+
this.statementPattern.setVariableScopeChange(statementPattern.isVariableScopeChange());
118+
109119
// First create the getters before removing duplicate vars since we need the getters when creating
110120
// JoinStatementWithBindingSetIterator. If there are duplicate vars, for instance ?v1 as both subject and
111121
// context then we still need to bind the value from ?v1 in the subject and context arguments of
@@ -153,6 +163,55 @@ public StatementPatternQueryEvaluationStep(StatementPattern statementPattern, Qu
153163

154164
}
155165

166+
private Var replaceValueWithNewValue(Var var, ValueFactory valueFactory) {
167+
if (var == null) {
168+
return null;
169+
} else if (!var.hasValue()) {
170+
return var.clone();
171+
} else {
172+
Var ret = getVarWithNewValue(var, valueFactory);
173+
ret.setVariableScopeChange(var.isVariableScopeChange());
174+
return ret;
175+
}
176+
}
177+
178+
private static Var getVarWithNewValue(Var var, ValueFactory valueFactory) {
179+
boolean constant = var.isConstant();
180+
boolean anonymous = var.isAnonymous();
181+
182+
Value value = var.getValue();
183+
if (value.isIRI()) {
184+
return Var.of(var.getName(), valueFactory.createIRI(value.stringValue()), anonymous, constant);
185+
} else if (value.isBNode()) {
186+
return Var.of(var.getName(), valueFactory.createBNode(value.stringValue()), anonymous, constant);
187+
} else if (value.isLiteral()) {
188+
// preserve label + (language | datatype)
189+
Literal lit = (Literal) value;
190+
191+
// If the literal has a language tag, recreate it with the same language
192+
if (lit.getLanguage().isPresent()) {
193+
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel(), lit.getLanguage().get()),
194+
anonymous, constant);
195+
}
196+
197+
CoreDatatype coreDatatype = lit.getCoreDatatype();
198+
if (coreDatatype != CoreDatatype.NONE) {
199+
// If the literal has a core datatype, recreate it with the same core datatype
200+
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel(), coreDatatype), anonymous,
201+
constant);
202+
}
203+
204+
// Otherwise, preserve the datatype (falls back to xsd:string if none)
205+
IRI dt = lit.getDatatype();
206+
if (dt != null) {
207+
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel(), dt), anonymous, constant);
208+
} else {
209+
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel()), anonymous, constant);
210+
}
211+
}
212+
return var;
213+
}
214+
156215
// test if the variable must remain unbound for this solution see
157216
// https://www.w3.org/TR/sparql11-query/#assignment
158217
private static Predicate<BindingSet> getUnboundTest(QueryEvaluationContext context, Var s, Var p,

core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/DescribeIteration.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,9 @@ protected CloseableIteration<BindingSet> createNextIteration(Value subject, Valu
210210
return QueryEvaluationStep.EMPTY_ITERATION;
211211
}
212212

213-
Var subjVar = new Var(VARNAME_SUBJECT, subject);
214-
Var predVar = new Var(VARNAME_PREDICATE);
215-
Var objVar = new Var(VARNAME_OBJECT, object);
213+
Var subjVar = Var.of(VARNAME_SUBJECT, subject);
214+
Var predVar = Var.of(VARNAME_PREDICATE);
215+
Var objVar = Var.of(VARNAME_OBJECT, object);
216216

217217
StatementPattern pattern = new StatementPattern(subjVar, predVar, objVar);
218218
return strategy.evaluate(pattern, parentBindings);

core/queryalgebra/evaluation/src/main/java/org/eclipse/rdf4j/query/algebra/evaluation/iterator/PathIteration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ public void meet(Var var) {
627627

628628
private Var createAnonVar(String varName, Value v, boolean anonymous) {
629629
namedIntermediateJoins.add(varName);
630-
return new Var(varName, v, anonymous, false);
630+
return Var.of(varName, v, anonymous, false);
631631
}
632632

633633
}

0 commit comments

Comments
 (0)