Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ public class SimpleValueFactory extends AbstractValueFactory {
private final static String uniqueIdPrefix = UUID.randomUUID().toString().replace("-", "");
private final static AtomicLong uniqueIdSuffix = new AtomicLong();

// Pre-built strings for lengths 0 through 9
private static final String[] RANDOMIZE_LENGTH = new String[10];

static {
StringBuilder sb = new StringBuilder();
for (int i = 0; i <= 9; i++) {
RANDOMIZE_LENGTH[i] = sb.toString();
sb.append(i);
}
}

private static final DatatypeFactory datatypeFactory;

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

@Override
public BNode createBNode() {
return createBNode(uniqueIdPrefix + uniqueIdSuffix.incrementAndGet());
long l = uniqueIdSuffix.incrementAndGet();
// reverse the string representation of the long to ensure that the BNode IDs are not monotonically increasing
StringBuilder sb = new StringBuilder(Long.toString(l));
sb.reverse();
sb.append(uniqueIdPrefix).append(RANDOMIZE_LENGTH[(int) (Math.abs(l % RANDOMIZE_LENGTH.length))]);
return createBNode(sb.toString());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*******************************************************************************
* 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.model.impl;

import static org.junit.jupiter.api.Assertions.assertThrows;

import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicLong;

import org.junit.jupiter.api.Test;

/**
* Reproduces overflow in SimpleValueFactory#createBNode() when the atomic counter wraps to Long.MIN_VALUE, which
* results in a negative index into the RANDOMIZE_LENGTH array and throws ArrayIndexOutOfBoundsException.
*/
public class SimpleValueFactoryOverflowTest {

@Test
void overflowAtMinValue() throws Exception {
// Access the private static counter
Field f = SimpleValueFactory.class.getDeclaredField("uniqueIdSuffix");
f.setAccessible(true);
AtomicLong counter = (AtomicLong) f.get(null);

// Preserve original value to avoid leaking state across tests
long original = counter.get();

synchronized (SimpleValueFactory.class) {
try {
// Force next increment to wrap from Long.MAX_VALUE to Long.MIN_VALUE
counter.set(Long.MAX_VALUE);

SimpleValueFactory.getInstance().createBNode();
} finally {
// Restore the original value
counter.set(original);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
@Experimental
public interface Explanation {

Object tupleExpr();

/**
* The different levels that the query explanation can be at.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,23 @@
public class ExplanationImpl implements Explanation {

private final GenericPlanNode genericPlanNode;
private final Object tupleExpr;

public ExplanationImpl(GenericPlanNode genericPlanNode, boolean timedOut) {
public ExplanationImpl(GenericPlanNode genericPlanNode, boolean timedOut, Object tupleExpr) {
this.genericPlanNode = genericPlanNode;
this.tupleExpr = tupleExpr;
if (timedOut) {
genericPlanNode.setTimedOut(timedOut);
}
}

ObjectMapper objectMapper = new ObjectMapper();

@Override
public Object tupleExpr() {
return tupleExpr;
}

@Override
public GenericPlanNode toGenericPlanNode() {
return genericPlanNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
package org.eclipse.rdf4j.query.algebra.evaluation;

import java.util.Comparator;
import java.util.EnumSet;
import java.util.Set;

import org.eclipse.rdf4j.common.annotation.Experimental;
Expand All @@ -22,7 +21,6 @@
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Triple;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.query.QueryEvaluationException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ public void meet(Var node) throws QueryEvaluationException {
// We can skip constants that are only used in StatementPatterns since these are never added to the
// BindingSet anyway
if (!(node.isConstant() && node.getParentNode() instanceof StatementPattern)) {
Var replacement = new Var(varNames.computeIfAbsent(node.getName(), k -> k), node.getValue(),
Var replacement = Var.of(varNames.computeIfAbsent(node.getName(), k -> k), node.getValue(),
node.isAnonymous(), node.isConstant());
node.replaceWith(replacement);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1252,8 +1252,32 @@ protected QueryValueEvaluationStep prepare(Coalesce node, QueryEvaluationContext

protected QueryValueEvaluationStep prepare(Compare node, QueryEvaluationContext context) {
boolean strict = QueryEvaluationMode.STRICT == getQueryEvaluationMode();
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compare(leftVal, rightVal, node.getOperator(), strict)), context);

Compare.CompareOp operator = node.getOperator();
switch (operator) {
case EQ:
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compareEQ(leftVal, rightVal, strict)), context);
case NE:
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compareNE(leftVal, rightVal, strict)), context);
case LT:
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compareLT(leftVal, rightVal, strict)), context);
case LE:
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compareLE(leftVal, rightVal, strict)), context);
case GE:
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compareGE(leftVal, rightVal, strict)), context);
case GT:
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compareGT(leftVal, rightVal, strict)), context);
default:
return supplyBinaryValueEvaluation(node, (leftVal, rightVal) -> BooleanLiteral
.valueOf(QueryEvaluationUtil.compare(leftVal, rightVal, node.getOperator(), strict)), context);
}

}

private BiFunction<Value, Value, Value> mathOperationApplier(MathExpr node,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ public class EvaluationStatistics {
private final static String uniqueIdPrefix = UUID.randomUUID().toString().replace("-", "");
private final static AtomicLong uniqueIdSuffix = new AtomicLong();

// Pre-built strings for lengths 0 through 9
private static final String[] RANDOMIZE_LENGTH = new String[10];
static {
StringBuilder sb = new StringBuilder();
for (int i = 0; i <= 9; i++) {
RANDOMIZE_LENGTH[i] = sb.toString();
sb.append(i);
}
}

private CardinalityCalculator calculator;

public double getCardinality(TupleExpr expr) {
Expand All @@ -66,6 +76,10 @@ protected CardinalityCalculator createCardinalityCalculator() {
return new CardinalityCalculator();
}

public boolean supportsJoinEstimation() {
return false;
}

/*-----------------------------------*
* Inner class CardinalityCalculator *
*-----------------------------------*/
Expand Down Expand Up @@ -117,7 +131,11 @@ public void meet(ZeroLengthPath node) {

@Override
public void meet(ArbitraryLengthPath node) {
final Var pathVar = new Var("_anon_" + uniqueIdPrefix + uniqueIdSuffix.incrementAndGet(), true);
long suffix = uniqueIdSuffix.getAndIncrement();
final Var pathVar = Var.of(
"_anon_path_" + uniqueIdPrefix + suffix
+ RANDOMIZE_LENGTH[(int) (Math.abs(suffix % RANDOMIZE_LENGTH.length))],
true);
// cardinality of ALP is determined based on the cost of a
// single ?s ?p ?o ?c pattern where ?p is unbound, compensating for the fact that
// the length of the path is unknown but expected to be _at least_ twice that of a normal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import org.eclipse.rdf4j.query.algebra.evaluation.QueryValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.QueryEvaluationContext;
import org.eclipse.rdf4j.query.algebra.evaluation.impl.evaluationsteps.values.ScopedQueryValueEvaluationStep;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.*;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.BadlyDesignedLeftJoinIterator;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.HashJoinIteration;
import org.eclipse.rdf4j.query.algebra.evaluation.iterator.LeftJoinIterator;
import org.eclipse.rdf4j.query.algebra.helpers.TupleExprs;
import org.eclipse.rdf4j.query.algebra.helpers.collectors.VarNameCollector;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
import org.eclipse.rdf4j.common.iteration.IndexReportingIterator;
import org.eclipse.rdf4j.common.order.StatementOrder;
import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Statement;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.ValueFactory;
import org.eclipse.rdf4j.model.base.CoreDatatype;
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.model.vocabulary.SESAME;
import org.eclipse.rdf4j.query.BindingSet;
Expand Down Expand Up @@ -75,7 +78,6 @@ public class StatementPatternQueryEvaluationStep implements QueryEvaluationStep
public StatementPatternQueryEvaluationStep(StatementPattern statementPattern, QueryEvaluationContext context,
TripleSource tripleSource) {
super();
this.statementPattern = statementPattern;
this.order = statementPattern.getStatementOrder();
this.context = context;
this.tripleSource = tripleSource;
Expand Down Expand Up @@ -106,6 +108,13 @@ public StatementPatternQueryEvaluationStep(StatementPattern statementPattern, Qu
Var objVar = statementPattern.getObjectVar();
Var conVar = statementPattern.getContextVar();

subjVar = replaceValueWithNewValue(subjVar, tripleSource.getValueFactory());
predVar = replaceValueWithNewValue(predVar, tripleSource.getValueFactory());
objVar = replaceValueWithNewValue(objVar, tripleSource.getValueFactory());
conVar = replaceValueWithNewValue(conVar, tripleSource.getValueFactory());

this.statementPattern = new StatementPattern(subjVar, predVar, objVar, conVar);

// First create the getters before removing duplicate vars since we need the getters when creating
// JoinStatementWithBindingSetIterator. If there are duplicate vars, for instance ?v1 as both subject and
// context then we still need to bind the value from ?v1 in the subject and context arguments of
Expand Down Expand Up @@ -153,6 +162,55 @@ public StatementPatternQueryEvaluationStep(StatementPattern statementPattern, Qu

}

private Var replaceValueWithNewValue(Var var, ValueFactory valueFactory) {
if (var == null) {
return null;
} else if (!var.hasValue()) {
return var.clone();
} else {
Var ret = getVarWithNewValue(var, valueFactory);
ret.setVariableScopeChange(var.isVariableScopeChange());
return ret;
}
}

private static Var getVarWithNewValue(Var var, ValueFactory valueFactory) {
boolean constant = var.isConstant();
boolean anonymous = var.isAnonymous();

Value value = var.getValue();
if (value.isIRI()) {
return Var.of(var.getName(), valueFactory.createIRI(value.stringValue()), anonymous, constant);
} else if (value.isBNode()) {
return Var.of(var.getName(), valueFactory.createBNode(value.stringValue()), anonymous, constant);
} else if (value.isLiteral()) {
// preserve label + (language | datatype)
Literal lit = (Literal) value;

// If the literal has a language tag, recreate it with the same language
if (lit.getLanguage().isPresent()) {
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel(), lit.getLanguage().get()),
anonymous, constant);
}

CoreDatatype coreDatatype = lit.getCoreDatatype();
if (coreDatatype != CoreDatatype.NONE) {
// If the literal has a core datatype, recreate it with the same core datatype
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel(), coreDatatype), anonymous,
constant);
}

// Otherwise, preserve the datatype (falls back to xsd:string if none)
IRI dt = lit.getDatatype();
if (dt != null) {
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel(), dt), anonymous, constant);
} else {
return Var.of(var.getName(), valueFactory.createLiteral(lit.getLabel()), anonymous, constant);
}
}
return var;
}

// test if the variable must remain unbound for this solution see
// https://www.w3.org/TR/sparql11-query/#assignment
private static Predicate<BindingSet> getUnboundTest(QueryEvaluationContext context, Var s, Var p,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,9 @@ protected CloseableIteration<BindingSet> createNextIteration(Value subject, Valu
return QueryEvaluationStep.EMPTY_ITERATION;
}

Var subjVar = new Var(VARNAME_SUBJECT, subject);
Var predVar = new Var(VARNAME_PREDICATE);
Var objVar = new Var(VARNAME_OBJECT, object);
Var subjVar = Var.of(VARNAME_SUBJECT, subject);
Var predVar = Var.of(VARNAME_PREDICATE);
Var objVar = Var.of(VARNAME_OBJECT, object);

StatementPattern pattern = new StatementPattern(subjVar, predVar, objVar);
return strategy.evaluate(pattern, parentBindings);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
*******************************************************************************/
package org.eclipse.rdf4j.query.algebra.evaluation.iterator;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Set;
import java.util.function.BiConsumer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,7 @@ public void meet(Var var) {

private Var createAnonVar(String varName, Value v, boolean anonymous) {
namedIntermediateJoins.add(varName);
return new Var(varName, v, anonymous, false);
return Var.of(varName, v, anonymous, false);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ private CloseableIteration<BindingSet> createIteration() throws QueryEvaluationE
}

public Var createAnonVar(String varName) {
return new Var(varName, true);
return Var.of(varName, true);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public VarVisitor(BindingSet bindings) {
public void meet(Var var) {
if (!var.hasValue() && bindings.hasBinding(var.getName())) {
Value value = bindings.getValue(var.getName());
Var replacement = new Var(var.getName(), value, var.isAnonymous(), var.isConstant());
Var replacement = Var.of(var.getName(), value, var.isAnonymous(), var.isConstant());
var.replaceWith(replacement);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void meet(Service node) throws RuntimeException {
public void meet(Var var) {
if (bindingSet != null && bindingSet.hasBinding(var.getName())) {
Value replacementValue = bindingSet.getValue(var.getName());
var.replaceWith(new Var(var.getName(), replacementValue, var.isAnonymous(), var.isConstant()));
var.replaceWith(Var.of(var.getName(), replacementValue, var.isAnonymous(), var.isConstant()));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,9 @@ public void optimize(TupleExpr tupleExpr, Dataset dataset, BindingSet bindings)

Var lostVar;
if (value == null) {
lostVar = new Var(name);
lostVar = Var.of(name);
} else {
lostVar = new Var(name, value);
lostVar = Var.of(name, value);
}

ext.addElement(new ExtensionElem(lostVar, name));
Expand Down
Loading
Loading