From a25742dd9c0ffc765c195d4a3f2732de9813e0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Mon, 7 Sep 2026 16:40:50 +0200 Subject: [PATCH 1/2] refactor(query): decouple kgram execution engine from next query ast (#568) - Replace legacy IDatatype with DatatypeValue across next.query.impl.kgram - Introduce native expression evaluators in next.query.impl.sparql.bridge - Remove legacy bridging adapters (BindingAdapter, DatatypeAdapter, KgramNodeConverter, SparqlAstToExpression, StorageManagerKgramValues) - Add DatatypeValue and RdfValueOrder contracts to next.data - Enforce clean module boundaries with NextModuleBoundaryTest --- .../next/data/api/model/DatatypeValue.java | 214 ++++++ .../next/data/api/model/RdfValueOrder.java | 190 ++++++ .../corese/core/next/data/api/term/Value.java | 8 +- .../impl/kgram/adapter/BindingAdapter.java | 205 ------ .../impl/kgram/adapter/DatatypeAdapter.java | 105 --- .../adapter/TripleParserEvalSupport.java | 38 -- .../impl/kgram/api/core/BindingContext.java | 8 +- .../impl/kgram/api/core/DatatypeValue.java | 98 --- .../next/query/impl/kgram/api/core/Edge.java | 15 +- .../next/query/impl/kgram/api/core/Expr.java | 10 +- .../query/impl/kgram/api/core/Filter.java | 20 +- .../next/query/impl/kgram/api/core/Node.java | 13 +- .../impl/kgram/api/core/PointerType.java | 6 +- .../next/query/impl/kgram/api/core/Regex.java | 4 +- .../impl/kgram/api/core/TripleStore.java | 4 +- .../impl/kgram/api/query/Environment.java | 11 +- .../query/impl/kgram/api/query/Evaluator.java | 12 + .../impl/kgram/api/query/ProcessVisitor.java | 51 +- .../query/impl/kgram/api/query/Producer.java | 17 +- .../next/query/impl/kgram/core/Checker.java | 11 +- .../query/impl/kgram/core/CompleteSPARQL.java | 72 +- .../core/next/query/impl/kgram/core/Eval.java | 388 ++++++----- .../next/query/impl/kgram/core/EvalGraph.java | 86 ++- .../next/query/impl/kgram/core/EvalJoin.java | 39 +- .../query/impl/kgram/core/EvalSPARQL.java | 72 +- .../core/next/query/impl/kgram/core/Exp.java | 336 +++++----- .../query/impl/kgram/core/IterableEntity.java | 50 +- .../next/query/impl/kgram/core/Mapping.java | 33 +- .../next/query/impl/kgram/core/Mappings.java | 270 ++++---- .../next/query/impl/kgram/core/Memory.java | 268 ++++---- .../kgram/core/ProcessVisitorDefault.java | 40 +- .../next/query/impl/kgram/core/Query.java | 369 ++++------ .../query/impl/kgram/event/EventImpl.java | 94 +-- .../kgram/execution/SparqlKgramEvaluator.java | 21 +- .../next/query/impl/kgram/filter/Checker.java | 66 +- .../next/query/impl/kgram/filter/Compile.java | 30 +- .../query/impl/kgram/filter/Extension.java | 4 +- .../impl/kgram/filter/FilterPattern.java | 13 +- .../query/impl/kgram/path/PathFinder.java | 634 ++++++++---------- .../impl/qpv1/BasicPatternGenerator.java | 45 +- .../impl/qpv1/HeuristicsBasedEstimation.java | 41 +- .../impl/kgram/tool/ApproximateSearchEnv.java | 38 +- .../impl/kgram/tool/EnvironmentImpl.java | 25 +- .../query/impl/kgram/tool/KgramNodes.java | 19 +- .../next/query/impl/kgram/tool/NodeImpl.java | 130 ++-- .../impl/kgram/tool/ProducerDefault.java | 12 +- .../impl/kgram/tool/StorageManagerEdge.java | 8 +- .../kgram/tool/StorageManagerKgramValues.java | 112 ---- .../kgram/tool/StorageManagerProducer.java | 69 +- .../next/query/impl/query/CoreseUpdate.java | 13 +- .../impl/repository/CoreseRepository.java | 14 +- .../impl/sparql/ast/QueryPrologueAst.java | 8 +- .../sparql/bridge/AstBackedExistTerm.java | 55 -- .../impl/sparql/bridge/AstBackedExpr.java | 318 +++++---- .../sparql/bridge/CoreseAstQueryBuilder.java | 184 ++--- .../sparql/bridge/KgramNodeConverter.java | 46 -- .../NativeBooleanExpressionEvaluator.java | 185 +++++ .../bridge/NativeEvaluationContext.java | 141 ++++ .../bridge/NativeExpressionEvaluator.java | 96 +++ .../bridge/NativeIriExpressionEvaluator.java | 41 ++ .../NativeNumericExpressionEvaluator.java | 285 ++++++++ .../NativeStringExpressionEvaluator.java | 324 +++++++++ .../NativeTemporalExpressionEvaluator.java | 86 +++ .../sparql/bridge/NativeValueComparison.java | 78 +++ .../bridge/NextDatatypeValueAdapter.java | 91 --- .../impl/sparql/bridge/NextFilterFromAst.java | 26 +- .../sparql/bridge/SparqlAstToExpression.java | 554 --------------- .../SparqlBuiltinFunctionNameResolver.java | 28 - .../sparql/bridge/SparqlTermResolver.java | 132 ++++ .../impl/sparql/bridge/WhereCompiler.java | 34 +- .../sparql/execution/CoreseBindingSet.java | 23 +- .../execution/NextSparqlPipelineExecutor.java | 98 ++- .../sparql/triple/function/core/Extern.java | 19 +- .../triple/function/script/JavaDScall.java | 14 +- .../architecture/NextModuleBoundaryTest.java | 51 +- .../data/api/model/RdfValueOrderTest.java | 80 +++ .../kgram/adapter/BindingAdapterTest.java | 257 ------- .../kgram/adapter/DatatypeAdapterTest.java | 347 ---------- .../next/query/impl/kgram/core/EvalTest.java | 70 +- .../query/impl/kgram/core/MappingsTest.java | 8 +- .../next/query/impl/kgram/core/QueryTest.java | 24 +- .../kgram/execution/RdfTermMatcherTest.java | 24 +- .../kgram/tool/StorageManagerEdgeTest.java | 6 +- .../tool/StorageManagerProducerTest.java | 9 +- .../impl/sparql/bridge/AstBackedEdgeTest.java | 50 +- .../bridge/CoreseAstQueryBuilderTest.java | 20 + .../sparql/bridge/KgramNodeConverterTest.java | 87 --- .../bridge/SparqlAstToExpressionTest.java | 72 -- .../sparql/bridge/SparqlTermResolverTest.java | 54 ++ .../NextSparqlPipelineExecutorTest.java | 206 +++++- 90 files changed, 4281 insertions(+), 4401 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/data/api/model/DatatypeValue.java create mode 100644 src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapter.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapter.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/TripleParserEvalSupport.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/DatatypeValue.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerKgramValues.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverter.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeBooleanExpressionEvaluator.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeExpressionEvaluator.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeIriExpressionEvaluator.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeNumericExpressionEvaluator.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeStringExpressionEvaluator.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeTemporalExpressionEvaluator.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeValueComparison.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java delete mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java create mode 100644 src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java delete mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapterTest.java delete mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapterTest.java delete mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverterTest.java delete mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java create mode 100644 src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolverTest.java diff --git a/src/main/java/fr/inria/corese/core/next/data/api/model/DatatypeValue.java b/src/main/java/fr/inria/corese/core/next/data/api/model/DatatypeValue.java new file mode 100644 index 000000000..6a1d5aff6 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/data/api/model/DatatypeValue.java @@ -0,0 +1,214 @@ +package fr.inria.corese.core.next.data.api.model; + +import fr.inria.corese.core.next.data.api.exception.IncorrectOperationException; +import fr.inria.corese.core.next.data.api.literal.CoreDatatype; +import fr.inria.corese.core.next.data.api.literal.RDFDatatype; +import fr.inria.corese.core.next.data.api.literal.XSDDatatype; +import fr.inria.corese.core.next.data.api.term.BNode; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.data.api.term.Triple; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Objects; + +import javax.xml.datatype.DatatypeConstants; + +/** + * Runtime contract shared by RDF values and the query engine. + * + *

The contract deliberately contains no dependency on the historical Corese + * datatype hierarchy. Query execution can therefore manipulate values produced + * by any {@code next.data} implementation without unwrapping implementation + * objects.

+ */ +public interface DatatypeValue extends Serializable { + + /** Returns the lexical representation of this RDF value. */ + String stringValue(); + + /** Returns the lexical representation of this RDF value. */ + default String getLabel() { + return stringValue(); + } + + /** + * Returns the Java value used for value-space operations. + * + *

Implementations may override this method with a more specific value. + * The lexical form is a safe default for RDF resources and custom literals.

+ */ + default Object getValue() { + return stringValue(); + } + + /** Returns the datatype IRI of a literal, or {@code null} for non-literals. */ + default String getDatatypeURI() { + return this instanceof Literal literal && literal.getDatatype() != null + ? literal.getDatatype().stringValue() + : null; + } + + /** Returns whether this value is an RDF IRI. */ + default boolean isIRI() { + return this instanceof IRI; + } + + /** Returns whether this value is an RDF blank node. */ + default boolean isBNode() { + return this instanceof BNode; + } + + /** Returns whether this value is an RDF literal. */ + default boolean isLiteral() { + return this instanceof Literal; + } + + /** Returns whether this value is an RDF-star triple term. */ + default boolean isTriple() { + return this instanceof Triple; + } + + /** Returns whether this literal belongs to the XML Schema numeric family. */ + default boolean isNumber() { + if (!(this instanceof Literal literal)) { + return false; + } + CoreDatatype datatype = literal.getCoreDatatype(); + return datatype instanceof XSDDatatype xsd && switch (xsd) { + case BYTE, SHORT, INT, LONG, INTEGER, + UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT, UNSIGNED_LONG, + POSITIVE_INTEGER, NEGATIVE_INTEGER, NON_NEGATIVE_INTEGER, + NON_POSITIVE_INTEGER, DECIMAL, FLOAT, DOUBLE -> true; + default -> false; + }; + } + + /** Returns this literal as an integer. */ + default int intValue() { + if (this instanceof Literal literal) { + return literal.intValue(); + } + throw new IncorrectOperationException("Cannot convert a non-literal RDF value to int"); + } + + /** Returns this literal as a double. */ + default double doubleValue() { + if (this instanceof Literal literal) { + return literal.doubleValue(); + } + throw new IncorrectOperationException("Cannot convert a non-literal RDF value to double"); + } + + /** + * Computes the SPARQL effective boolean value. + * + *

Unsupported RDF terms have no effective boolean value and return + * {@code false}; expression evaluation is responsible for retaining the + * distinction between false and an evaluation error where required.

+ */ + default boolean isTrue() { + if (!(this instanceof Literal literal)) { + return false; + } + CoreDatatype datatype = literal.getCoreDatatype(); + if (datatype == XSDDatatype.BOOLEAN) { + return literal.booleanValue(); + } + if (isNumber()) { + double value = literal.doubleValue(); + return value != 0.0d && !Double.isNaN(value); + } + if (datatype == XSDDatatype.STRING || datatype == RDFDatatype.LANGSTRING) { + return !literal.getLabel().isEmpty(); + } + return false; + } + + /** RDF-term equality, without value-space coercion. */ + default boolean sameTerm(DatatypeValue other) { + return Objects.equals(this, other); + } + + /** + * SPARQL value equality for the value families required by the native + * execution pipeline. + */ + default boolean equalsWE(DatatypeValue other) { + if (other == null) { + return false; + } + if (this instanceof Literal left && other instanceof Literal right) { + return literalValueEquals(left, right); + } + return sameTerm(other); + } + + /** Compares two runtime values using the deterministic RDF term order. */ + default int compare(DatatypeValue other) { + return RdfValueOrder.compareValues(this, other); + } + + private static boolean isFloatingPoint(Literal literal) { + return literal.getCoreDatatype() == XSDDatatype.FLOAT + || literal.getCoreDatatype() == XSDDatatype.DOUBLE; + } + + private static boolean literalValueEquals(Literal left, Literal right) { + if (left.isNumber() && right.isNumber()) { + return numericValueEquals(left, right); + } + if (left.getCoreDatatype() == XSDDatatype.BOOLEAN + && right.getCoreDatatype() == XSDDatatype.BOOLEAN) { + return left.booleanValue() == right.booleanValue(); + } + if (isComparableCalendar(left, right)) { + return left.calendarValue().compare(right.calendarValue()) == DatatypeConstants.EQUAL; + } + if (left.getCoreDatatype() == RDFDatatype.LANGSTRING + && right.getCoreDatatype() == RDFDatatype.LANGSTRING) { + return left.getLabel().equals(right.getLabel()) + && left.getLanguage().orElse("") + .equalsIgnoreCase(right.getLanguage().orElse("")); + } + return left.sameTerm(right); + } + + private static boolean numericValueEquals(Literal left, Literal right) { + if (isFloatingPoint(left) || isFloatingPoint(right)) { + double leftValue = left.doubleValue(); + double rightValue = right.doubleValue(); + return !Double.isNaN(leftValue) + && !Double.isNaN(rightValue) + && leftValue == rightValue; + } + return decimalValue(left).compareTo(decimalValue(right)) == 0; + } + + private static boolean isComparableCalendar(Literal left, Literal right) { + if (left.getCoreDatatype() != right.getCoreDatatype()) { + return false; + } + return left.getCoreDatatype() instanceof XSDDatatype xsd && switch (xsd) { + case DATE, DATETIME, TIME -> true; + default -> false; + }; + } + + private static BigDecimal decimalValue(Literal literal) { + CoreDatatype datatype = literal.getCoreDatatype(); + if (datatype instanceof XSDDatatype xsd && switch (xsd) { + case BYTE, SHORT, INT, LONG, INTEGER, + UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT, UNSIGNED_LONG, + POSITIVE_INTEGER, NEGATIVE_INTEGER, NON_NEGATIVE_INTEGER, + NON_POSITIVE_INTEGER -> true; + default -> false; + }) { + BigInteger value = literal.integerValue(); + return new BigDecimal(value); + } + return literal.decimalValue(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java b/src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java new file mode 100644 index 000000000..345f279c6 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/data/api/model/RdfValueOrder.java @@ -0,0 +1,190 @@ +package fr.inria.corese.core.next.data.api.model; + +import fr.inria.corese.core.next.data.api.literal.CoreDatatype; +import fr.inria.corese.core.next.data.api.literal.RDFDatatype; +import fr.inria.corese.core.next.data.api.literal.XSDDatatype; +import fr.inria.corese.core.next.data.api.term.BNode; +import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.data.api.term.Triple; + +import java.math.BigDecimal; +import java.util.Comparator; + +/** + * Deterministic total order for RDF values used by query sorting and KGRAM + * collections. + * + *

The primary order follows SPARQL 1.1: blank nodes, IRIs, then literals. + * RDF 1.2 triple terms follow RDF 1.1 terms. Within a term family, comparison + * is deterministic and value-aware for compatible numeric and boolean + * literals, with datatype, language and lexical form as stable tie-breakers.

+ */ +public final class RdfValueOrder implements Comparator { + + /** Shared stateless comparator. */ + public static final RdfValueOrder INSTANCE = new RdfValueOrder(); + + private RdfValueOrder() { + } + + /** Compares two values, sorting {@code null} before every bound RDF term. */ + public static int compareValues(DatatypeValue left, DatatypeValue right) { + return compareInternal(left, right); + } + + @Override + public int compare(DatatypeValue left, DatatypeValue right) { + return compareInternal(left, right); + } + + private static int compareInternal(DatatypeValue left, DatatypeValue right) { + if (left == right) { + return 0; + } + if (left == null) { + return -1; + } + if (right == null) { + return 1; + } + + int kindComparison = Integer.compare(kindRank(left), kindRank(right)); + if (kindComparison != 0) { + return kindComparison; + } + if (left instanceof Literal leftLiteral && right instanceof Literal rightLiteral) { + return compareLiterals(leftLiteral, rightLiteral); + } + if (left instanceof Triple leftTriple && right instanceof Triple rightTriple) { + int subject = compareValues(leftTriple.subject(), rightTriple.subject()); + if (subject != 0) { + return subject; + } + int predicate = compareValues(leftTriple.predicate(), rightTriple.predicate()); + return predicate != 0 + ? predicate + : compareValues(leftTriple.object(), rightTriple.object()); + } + return left.stringValue().compareTo(right.stringValue()); + } + + private static int kindRank(DatatypeValue value) { + if (value instanceof BNode) { + return 0; + } + if (value instanceof IRI) { + return 1; + } + if (value instanceof Literal) { + return 2; + } + if (value instanceof Triple) { + return 3; + } + return 4; + } + + private static int compareLiterals(Literal left, Literal right) { + int familyComparison = Integer.compare(literalFamily(left), literalFamily(right)); + if (familyComparison != 0) { + return familyComparison; + } + + int valueComparison = compareCompatibleValues(left, right); + if (valueComparison != 0) { + return valueComparison; + } + + int datatypeComparison = datatype(left).compareTo(datatype(right)); + if (datatypeComparison != 0) { + return datatypeComparison; + } + int languageComparison = left.getLanguage().orElse("") + .compareToIgnoreCase(right.getLanguage().orElse("")); + return languageComparison != 0 + ? languageComparison + : left.getLabel().compareTo(right.getLabel()); + } + + private static int compareCompatibleValues(Literal left, Literal right) { + if (isNumeric(left) && isNumeric(right)) { + if (isFloatingPoint(left) || isFloatingPoint(right)) { + return compareFloatingPoint(left.doubleValue(), right.doubleValue()); + } + return decimalValue(left).compareTo(decimalValue(right)); + } + if (isBoolean(left) && isBoolean(right)) { + return Boolean.compare(left.booleanValue(), right.booleanValue()); + } + return left.getLabel().compareTo(right.getLabel()); + } + + private static int compareFloatingPoint(double left, double right) { + if (Double.isNaN(left)) { + return Double.isNaN(right) ? 0 : -1; + } + if (Double.isNaN(right)) { + return 1; + } + return Double.compare(left, right); + } + + private static BigDecimal decimalValue(Literal literal) { + if (isInteger(literal)) { + return new BigDecimal(literal.integerValue()); + } + return literal.decimalValue(); + } + + private static boolean isNumeric(Literal literal) { + CoreDatatype datatype = literal.getCoreDatatype(); + return datatype instanceof XSDDatatype xsd && switch (xsd) { + case BYTE, SHORT, INT, LONG, INTEGER, + UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT, UNSIGNED_LONG, + POSITIVE_INTEGER, NEGATIVE_INTEGER, NON_NEGATIVE_INTEGER, + NON_POSITIVE_INTEGER, DECIMAL, FLOAT, DOUBLE -> true; + default -> false; + }; + } + + private static boolean isInteger(Literal literal) { + CoreDatatype datatype = literal.getCoreDatatype(); + return datatype instanceof XSDDatatype xsd && switch (xsd) { + case BYTE, SHORT, INT, LONG, INTEGER, + UNSIGNED_BYTE, UNSIGNED_SHORT, UNSIGNED_INT, UNSIGNED_LONG, + POSITIVE_INTEGER, NEGATIVE_INTEGER, NON_NEGATIVE_INTEGER, + NON_POSITIVE_INTEGER -> true; + default -> false; + }; + } + + private static boolean isFloatingPoint(Literal literal) { + return literal.getCoreDatatype() == XSDDatatype.FLOAT + || literal.getCoreDatatype() == XSDDatatype.DOUBLE; + } + + private static boolean isBoolean(Literal literal) { + return literal.getCoreDatatype() == XSDDatatype.BOOLEAN; + } + + private static int literalFamily(Literal literal) { + if (isNumeric(literal)) { + return 0; + } + if (isBoolean(literal)) { + return 1; + } + if (literal.getCoreDatatype() == XSDDatatype.STRING) { + return 2; + } + if (literal.getCoreDatatype() == RDFDatatype.LANGSTRING) { + return 3; + } + return 4; + } + + private static String datatype(Literal literal) { + return literal.getDatatype() == null ? "" : literal.getDatatype().stringValue(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/Value.java b/src/main/java/fr/inria/corese/core/next/data/api/term/Value.java index 9e82ec66c..98e87856a 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/Value.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/Value.java @@ -1,15 +1,16 @@ package fr.inria.corese.core.next.data.api.term; -import java.io.Serializable; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; /** * Super interface of all elements of an RDF model (triple, nodes, etc). */ -public interface Value extends Serializable { +public interface Value extends DatatypeValue { /** * @return whether this value is a blank node */ + @Override default boolean isBNode() { return this instanceof BNode; } @@ -17,6 +18,7 @@ default boolean isBNode() { /** * @return whether this value is an IRI */ + @Override default boolean isIRI() { return this instanceof IRI; } @@ -31,6 +33,7 @@ default boolean isResource() { /** * @return whether this value is a literal */ + @Override default boolean isLiteral() { return this instanceof Literal; } @@ -38,6 +41,7 @@ default boolean isLiteral() { /** * @return whether this value is an RDF-star triple term */ + @Override default boolean isTriple() { return this instanceof Triple; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapter.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapter.java deleted file mode 100644 index 548e7f7b3..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapter.java +++ /dev/null @@ -1,205 +0,0 @@ -package fr.inria.corese.core.next.query.impl.kgram.adapter; - -import fr.inria.corese.core.next.query.impl.kgram.api.core.BindingContext; -import fr.inria.corese.core.next.query.impl.kgram.api.core.Expr; -import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; -import fr.inria.corese.core.next.query.impl.kgram.core.Exp; -import fr.inria.corese.core.next.query.impl.kgram.core.Mappings; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.function.term.Binding; - -import java.util.HashMap; -import java.util.Map; - -/** - * Adapter to use a {@link Binding} instance via the {@link BindingContext} interface. - * - * @param delegate The delegated Binding instance. - */ -public record BindingAdapter(Binding delegate) implements BindingContext { - - /** - * Constructs an adapter for the given Binding delegate. - * - * @param delegate the Binding instance to wrap. - * @throws IllegalArgumentException if delegate is null. - */ - public BindingAdapter { - if (delegate == null) { - throw new IllegalArgumentException("delegate cannot be null"); - } - } - - - /** - * Returns the underlying delegated Binding instance. - * - * @return the delegate. - */ - @Override - public Binding delegate() { - return delegate; - } - - @Override - public Node getValue(String variable) { - // Binding.get() requires an Expr, not a String. - // Look for the corresponding Expr variable label. - for (fr.inria.corese.core.kgram.api.core.Expr var : delegate.getVariables()) { - if (var.getLabel().equals(variable)) { - Object value = delegate.get(var); - return (value instanceof Node) ? (Node) value : null; - } - } - return null; - } - - @Override - public void setValue(String variable, Node value) { - // Binding.set() requires an Expr and IDatatype, not a String and Node. - // Search for the corresponding Expr variable. - for (fr.inria.corese.core.kgram.api.core.Expr var : delegate.getVariables()) { - if (var.getLabel().equals(variable)) { - // Convert Node to IDatatype - IDatatype dataValue = (value != null) ? value.getDatatypeValue() : null; - delegate.set(var, dataValue); - return; - } - } - } - - @Override - public boolean isDefined(String variable) { - for (fr.inria.corese.core.kgram.api.core.Expr var : delegate.getVariables()) { - if (var.getLabel().equals(variable)) { - // isBound() accepts a String label - return delegate.isBound(var.getLabel()); - } - } - return false; - } - - @Override - public Map getBindings() { - Map map = new HashMap<>(); - for (fr.inria.corese.core.kgram.api.core.Expr var : delegate.getVariables()) { - Object value = delegate.get(var); - if (value instanceof Node) { - map.put(var.getLabel(), (Node) value); - } - } - return map; - } - - @Override - public void copy(BindingContext other) { - if (other == null) { - return; - } - - if (other instanceof BindingAdapter) { - Binding otherBinding = ((BindingAdapter) other).delegate; - delegate.share(otherBinding); - } else { - // Copy variable by variable - for (Map.Entry entry : other.getBindings().entrySet()) { - setValue(entry.getKey(), entry.getValue()); - } - } - } - - /** - * Shares data between two contexts. - * Necessary for implementations like Memory.share() - */ - @Override - public void share(BindingContext source) { - if (source instanceof BindingAdapter) { - delegate.share(((BindingAdapter) source).delegate); - } - } - - /** - * Retrieves the value for an Expr variable. - * Used by Mapping.get(Expr) and Memory.get(Expr). - */ - @Override - public Object get(Expr varExpr) { - // Cast to the appropriate KGRAM Expr type - if (varExpr instanceof fr.inria.corese.core.kgram.api.core.Expr) { - return delegate.get((fr.inria.corese.core.kgram.api.core.Expr) varExpr); - } - // Fallback to label lookup - return getValue(varExpr.getLabel()); - } - - /** - * Retrieves the associated ProcessVisitor. - */ - @Override - public Object getVisitor() { - return delegate.getVisitor(); - } - - /** - * Sets the associated ProcessVisitor. - */ - @Override - public void setVisitor(Object visitor) { - if (visitor instanceof fr.inria.corese.core.kgram.api.query.ProcessVisitor) { - delegate.setVisitor((fr.inria.corese.core.kgram.api.query.ProcessVisitor) visitor); - } - } - - /** - * Visit method for reporting. - */ - @Override - public void visit(Object e, Object g, Object m1, Object m2) { - try { - // Check for "next" kgram types - if (e instanceof Exp && - (g == null || g instanceof Node) && - (m1 == null || m1 instanceof Mappings) && - (m2 == null || m2 instanceof Mappings)) { - - delegate.visit( - (Exp) e, - (Node) g, - (Mappings) m1, - (Mappings) m2 - ); - return; - } - - // Fallback to "legacy" kgram types - if (e instanceof fr.inria.corese.core.kgram.core.Exp && - (g == null || g instanceof fr.inria.corese.core.kgram.api.core.Node) && - (m1 == null || m1 instanceof fr.inria.corese.core.kgram.core.Mappings) && - (m2 == null || m2 instanceof fr.inria.corese.core.kgram.core.Mappings)) { - - delegate.visit( - (fr.inria.corese.core.kgram.core.Exp) e, - (fr.inria.corese.core.kgram.api.core.Node) g, - (fr.inria.corese.core.kgram.core.Mappings) m1, - (fr.inria.corese.core.kgram.core.Mappings) m2 - ); - } - } catch (Exception ex) { - // Silently ignore errors as the visit() method is optional/informative - } - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - - if (obj instanceof BindingAdapter) { - return delegate.equals(((BindingAdapter) obj).delegate); - } - - return false; - } - -} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapter.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapter.java deleted file mode 100644 index cc09fc27b..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapter.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.inria.corese.core.next.query.impl.kgram.adapter; - -import fr.inria.corese.core.next.query.impl.kgram.api.core.DatatypeValue; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException; - -/** - * Adapter to use an IDatatype via the DatatypeValue interface. - */ -public record DatatypeAdapter(IDatatype delegate) implements DatatypeValue { - - /** - * Constructs an adapter for the given IDatatype delegate. - * - * @param delegate the IDatatype instance to wrap - * @throws IllegalArgumentException if delegate is null - */ - public DatatypeAdapter { - if (delegate == null) { - throw new IllegalArgumentException("delegate cannot be null"); - } - } - - - /** - * Unwraps a DatatypeValue to get the underlying IDatatype. - * - * @param value the value to unwrap - * @return the IDatatype, or null if value is null or not an adapter - */ - public static IDatatype unwrap(DatatypeValue value) { - if (value instanceof DatatypeAdapter(IDatatype delegate1)) { - return delegate1; - } - return null; - } - - @Override - public String getLabel() { - return delegate.getLabel(); - } - - @Override - public Object getValue() { - return delegate.getValue(); - } - - @Override - public String getDatatypeURI() { - return delegate.getDatatypeURI(); - } - - @Override - public boolean isTrue() { - // VERSION 1: Use isTrueTest() if that's the method name - return delegate.isTrueTest(); - } - - @Override - public boolean equalsWE(DatatypeValue other) throws CoreseDatatypeException { - if (other instanceof DatatypeAdapter(IDatatype delegate1)) { - return delegate.equalsWE(delegate1); - } - return false; - } - - @Override - public int compare(DatatypeValue other) throws CoreseDatatypeException { - if (other instanceof DatatypeAdapter(IDatatype delegate1)) { - return delegate.compare(delegate1); - } - throw new IllegalArgumentException("Cannot compare with non-IDatatype value"); - } - - @Override - public int intValue() { - return delegate.intValue(); - } - - @Override - public double doubleValue() { - return delegate.doubleValue(); - } - - @Override - public boolean isNumber() { - return delegate.isNumber(); - } - - @Override - public boolean isLiteral() { - return delegate.isLiteral(); - } - - @Override - public boolean isURI() { - return delegate.isURI(); - } - - @Override - public boolean isBlank() { - return delegate.isBlank(); - } - -} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/TripleParserEvalSupport.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/TripleParserEvalSupport.java deleted file mode 100644 index de1138b76..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/adapter/TripleParserEvalSupport.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.inria.corese.core.next.query.impl.kgram.adapter; - -import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; -import fr.inria.corese.core.next.query.impl.kgram.api.query.Environment; -import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; -import fr.inria.corese.core.sparql.api.Computer; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.exceptions.EngineException; -import fr.inria.corese.core.sparql.triple.function.term.Binding; -import fr.inria.corese.core.sparql.triple.parser.Expression; - -/** - * Invokes {@link Expression#evalWE} for Corese-next {@link fr.inria.corese.core.next.query.impl.kgram.api.core.Expr} - */ -public final class TripleParserEvalSupport { - - private TripleParserEvalSupport() { - } - - public static IDatatype evalWE( - Expression expr, Computer eval, Binding binding, Environment env, Producer producer) { - if (!(env instanceof fr.inria.corese.core.kgram.api.query.Environment kgramEnv)) { - throw new QueryEvaluationException( - "Environment must implement fr.inria.corese.core.kgram.api.query.Environment" - + " (required by sparql.triple.parser.Expression#evalWE)"); - } - if (!(producer instanceof fr.inria.corese.core.kgram.api.query.Producer kgramProducer)) { - throw new QueryEvaluationException( - "Producer must implement fr.inria.corese.core.kgram.api.query.Producer" - + " (required by sparql.triple.parser.Expression#evalWE)"); - } - try { - return expr.evalWE(eval, binding, kgramEnv, kgramProducer); - } catch (EngineException e) { - throw new QueryEvaluationException(e.getMessage(), e); - } - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/BindingContext.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/BindingContext.java index cc6c36ac5..e0f63983b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/BindingContext.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/BindingContext.java @@ -1,5 +1,7 @@ package fr.inria.corese.core.next.query.impl.kgram.api.core; +import fr.inria.corese.core.next.query.impl.kgram.api.query.ProcessVisitor; + import java.util.Map; /** @@ -94,7 +96,7 @@ default Object get(Expr varExpr) { * * @return the ProcessVisitor or null */ - default Object getVisitor() { + default ProcessVisitor getVisitor() { return null; } @@ -103,7 +105,7 @@ default Object getVisitor() { * * @param visitor the ProcessVisitor to associate */ - default void setVisitor(Object visitor) { + default void setVisitor(ProcessVisitor visitor) { // By default, do nothing } @@ -119,4 +121,4 @@ default void setVisitor(Object visitor) { default void visit(Object e, Object g, Object m1, Object m2) { // By default, do nothing } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/DatatypeValue.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/DatatypeValue.java deleted file mode 100644 index 1d3756f24..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/DatatypeValue.java +++ /dev/null @@ -1,98 +0,0 @@ -package fr.inria.corese.core.next.query.impl.kgram.api.core; - -import fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException; - -/** - * Datatype value interface for KGRAM. - */ -public interface DatatypeValue { - - /** - * Returns the string label of this datatype value. - * - * @return the label - */ - String getLabel(); - - /** - * Returns the underlying Java object value. - * - * @return the value object - */ - Object getValue(); - - /** - * Returns the datatype URI. - * - * @return the datatype URI as a string - */ - String getDatatypeURI(); - - /** - * Tests if this value represents a boolean true. - * Used for filter evaluation. - * - * @return true if this value is truthy - */ - boolean isTrue(); - - /** - * Tests if this value is equivalent to another for comparison purposes. - * - * @param other the other value - * @return true if equivalent - */ - boolean equalsWE(DatatypeValue other) throws CoreseDatatypeException; - - /** - * Compares this value to another. - * - * @param other the other value - * @return negative if less, 0 if equal, positive if greater - */ - int compare(DatatypeValue other) throws CoreseDatatypeException; - - /** - * Returns the integer value if this is a numeric type. - * - * @return the integer value - * @throws NumberFormatException if not numeric - */ - int intValue(); - - /** - * Returns the double value if this is a numeric type. - * - * @return the double value - * @throws NumberFormatException if not numeric - */ - double doubleValue(); - - /** - * Tests if this is a number type. - * - * @return true if numeric - */ - boolean isNumber(); - - /** - * Tests if this is a literal (not a URI or blank node). - * - * @return true if literal - */ - boolean isLiteral(); - - /** - * Tests if this is a URI. - * - * @return true if URI - */ - boolean isURI(); - - /** - * Tests if this is a blank node. - * - * @return true if blank node - */ - boolean isBlank(); -} \ No newline at end of file diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Edge.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Edge.java index f06b832eb..69a4e71b9 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Edge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Edge.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.kgram.api.core; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; /** * Interface for Producer iterator that encapsulate Edge or Node with its Graph @@ -54,8 +54,8 @@ default boolean contains(Node node) { default int getEdgeIndex() { return -1; } - @SuppressWarnings("unused") default void setEdgeIndex(int n) { + // Most immutable edge implementations do not expose a mutable index. } Node getGraph(); @@ -69,6 +69,7 @@ default Object getProvenance() { } default void setProvenance(Object obj) { + // Provenance is an optional capability of concrete edge implementations. } default boolean isMatchArity() { @@ -79,11 +80,11 @@ default boolean isMatchArity() { default boolean isNested() { return false; } - @SuppressWarnings("unused") default void setNested(boolean b) { + // RDF-star nesting is an optional capability of concrete edge implementations. } - default IDatatype getGraphValue() { + default DatatypeValue getGraphValue() { Node node = getGraph(); if (node == null) { return null; @@ -91,19 +92,19 @@ default IDatatype getGraphValue() { return node.getDatatypeValue(); } - default IDatatype getSubjectValue() { + default DatatypeValue getSubjectValue() { return getNode(0).getDatatypeValue(); } - default IDatatype getPredicateValue() { + default DatatypeValue getPredicateValue() { if (getProperty() == null) { return null; } return getProperty().getDatatypeValue(); } - default IDatatype getObjectValue() { + default DatatypeValue getObjectValue() { return getNode(1).getDatatypeValue(); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Expr.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Expr.java index 9d868b573..f6f6ed044 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Expr.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Expr.java @@ -3,7 +3,7 @@ import fr.inria.corese.core.next.query.impl.kgram.api.query.Environment; import fr.inria.corese.core.next.query.impl.kgram.api.query.Evaluator; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.List; @@ -118,7 +118,7 @@ public interface Expr { * @param p the producer * @return the result of the evaluation */ - IDatatype evalWE(Evaluator eval, BindingContext b, Environment env, Producer p); + DatatypeValue evalWE(Evaluator eval, BindingContext b, Environment env, Producer p); /** * Tests if this expression evaluates to true. @@ -130,11 +130,11 @@ public interface Expr { * @return true if the expression is truthy */ default boolean test(Evaluator eval, BindingContext b, Environment env, Producer p) { - IDatatype dt = evalWE(eval, b, env, p); + DatatypeValue dt = evalWE(eval, b, env, p); if (dt == null) { return false; } - return dt.isTrueTest(); + return dt.isTrue(); } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Filter.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Filter.java index 29014bd62..bd5500a56 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Filter.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Filter.java @@ -1,23 +1,13 @@ package fr.inria.corese.core.next.query.impl.kgram.api.core; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; -import fr.inria.corese.core.sparql.triple.parser.Expression; import java.util.List; import java.util.Optional; /** - * Interface of Filter that contains an evaluable expression - * Filter (and Expr) api refer to sparql.triple.parser.Expression - * - *

Migration: filters built from the Corese-next SPARQL AST should use - * {@link fr.inria.corese.core.next.query.impl.sparql.bridge.SparqlAstToExpression} - * and {@link fr.inria.corese.core.next.query.impl.sparql.bridge.AstBackedExpr} / - * {@link fr.inria.corese.core.next.query.impl.sparql.bridge.NextFilterFromAst}. - * {@link #getFilterExpression()} still exposes the SPARQL {@link Expression} tree for the - * interpreter; {@link #getExp()} and {@link #coreseNextSource()} carry the - * {@link fr.inria.corese.core.next.query.impl.kgram.api.core.Expr} / Corese-next AST view. + * Native filter contract backed by the Corese-next SPARQL AST. * * @author Olivier Corby, Edelweiss, INRIA 2010 */ @@ -39,14 +29,10 @@ default Optional coreseNextSource() { List getVariables(boolean excludeLocal); - /** - * Evaluable expression processed by KGRAM generic Interpreter - * Expr api refer also to sparql.triple.parser.Expression - * - */ + /** Evaluable expression processed by the native KGRAM evaluator. */ Expr getExp(); - Expression getFilterExpression(); + TermAst getFilterExpression(); /** * Does filter contain a bound() function diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Node.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Node.java index d63484ece..940b4938e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Node.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Node.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.kgram.api.core; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.kgram.path.Path; -import fr.inria.corese.core.sparql.api.IDatatype; /** @@ -46,7 +46,7 @@ public interface Node extends Pointerable, Comparable { int compare(Node node); default int compareTo(Node node) { - return getDatatypeValue().compareTo(node.getDatatypeValue()); + return compare(node); } String getLabel(); @@ -57,8 +57,6 @@ default int compareTo(Node node) { boolean isBlank(); - boolean isFuture(); - default boolean isMatchNodeList() { return false; } @@ -68,12 +66,11 @@ default boolean isMatchCardinality() { } // the target value for Matcher and Evaluator - // for KGRAM query it returns IDatatype - IDatatype getValue(); + DatatypeValue getValue(); - IDatatype getDatatypeValue(); + DatatypeValue getDatatypeValue(); - default void setDatatypeValue(IDatatype dt) { + default void setDatatypeValue(DatatypeValue dt) { } Node getGraph(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/PointerType.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/PointerType.java index d9798a507..dca3e09e4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/PointerType.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/PointerType.java @@ -1,7 +1,5 @@ package fr.inria.corese.core.next.query.impl.kgram.api.core; -import static fr.inria.corese.core.kgram.api.core.ExpType.DT; - /** * Pointer type for object that can be object of CoresePointer * @@ -27,10 +25,12 @@ public enum PointerType { VISITOR("visitor") ; + private static final String DATATYPE_NAMESPACE = "http://ns.inria.fr/corese/datatype/"; + final String name; PointerType(String n) { - name = DT + n; + name = DATATYPE_NAMESPACE + n; } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Regex.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Regex.java index 04be68b12..3fbbe66e4 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Regex.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/Regex.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.kgram.api.core; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; /** * Interface of Property Path Regex @@ -26,7 +26,7 @@ public interface Regex { String getLongName(); - IDatatype getDatatypeValue(); + DatatypeValue getDatatypeValue(); int retype(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/TripleStore.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/TripleStore.java index e6106f371..5517574ac 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/TripleStore.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/TripleStore.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.kgram.api.core; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; /** * @author corby @@ -9,7 +9,7 @@ public interface TripleStore { Node getNode(int n); - IDatatype set(IDatatype key, IDatatype value); + DatatypeValue set(DatatypeValue key, DatatypeValue value); int size(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Environment.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Environment.java index 7d4a4ae5d..b3d716a9a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Environment.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Environment.java @@ -8,8 +8,7 @@ import fr.inria.corese.core.next.query.impl.kgram.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.kgram.path.Path; import fr.inria.corese.core.next.query.impl.kgram.tool.ApproximateSearchEnv; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.parser.ASTExtension; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.Map; @@ -106,7 +105,7 @@ default void setGraphNode(Node n) { void setExp(Exp exp); // id -> bnode - Map getMap(); + Map getMap(); Edge[] getEdges(); @@ -124,8 +123,6 @@ default void setGraphNode(Node n) { Node get(Expr varExpr); - ASTExtension getExtension(); - ApproximateSearchEnv getAppxSearchEnv(); Eval getEval(); @@ -134,9 +131,9 @@ default void setGraphNode(Node n) { ProcessVisitor getVisitor(); - IDatatype getReport(); + DatatypeValue getReport(); - void setReport(IDatatype dt); + void setReport(DatatypeValue dt); int size(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Evaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Evaluator.java index 99e858e40..38fb833e6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Evaluator.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Evaluator.java @@ -2,6 +2,8 @@ import fr.inria.corese.core.next.query.impl.kgram.core.Eval; +import java.time.OffsetDateTime; + /** * Interface for the connector that evaluates filters * @@ -9,6 +11,16 @@ */ public interface Evaluator { + /** + * Returns the stable timestamp associated with the current query evaluation. + * + *

SPARQL requires every invocation of {@code NOW()} in one query to + * produce the same value.

+ * + * @return query-scoped evaluation timestamp + */ + OffsetDateTime getQueryEvaluationTime(); + Mode getMode(); void setMode(Mode mode); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/ProcessVisitor.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/ProcessVisitor.java index b4eb30c8a..905f0bf32 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/ProcessVisitor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/ProcessVisitor.java @@ -6,13 +6,12 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.Pointerable; import fr.inria.corese.core.next.query.impl.kgram.core.*; import fr.inria.corese.core.next.query.impl.kgram.path.Path; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; /** * @author Olivier Corby, Wimmics INRIA I3S, 2018 */ -@SuppressWarnings("unused") public interface ProcessVisitor extends Pointerable { int SLICE_DEFAULT = 20; @@ -21,31 +20,31 @@ default boolean isShareable() { return false; } - default IDatatype defaultValue() { + default DatatypeValue defaultValue() { return null; } - default IDatatype init(Query q) { + default DatatypeValue init(Query q) { return defaultValue(); } - default IDatatype before(Query q) { + default DatatypeValue before(Query q) { return defaultValue(); } - default IDatatype after(Mappings map) { + default DatatypeValue after(Mappings map) { return defaultValue(); } - default IDatatype start(Query q) { + default DatatypeValue start(Query q) { return defaultValue(); } - default IDatatype finish(Mappings map) { + default DatatypeValue finish(Mappings map) { return defaultValue(); } - default IDatatype orderby(Mappings map) { + default DatatypeValue orderby(Mappings map) { return defaultValue(); } @@ -61,15 +60,15 @@ default int slice() { return SLICE_DEFAULT; } - default IDatatype produce(Eval eval, Node g, Edge edge) { + default DatatypeValue produce(Eval eval, Node g, Edge edge) { return defaultValue(); } - default IDatatype candidate(Eval eval, Node g, Edge q, Edge e) { + default DatatypeValue candidate(Eval eval, Node g, Edge q, Edge e) { return defaultValue(); } - default IDatatype path(Eval eval, Node g, Edge q, Path p, Node s, Node o) { + default DatatypeValue path(Eval eval, Node g, Edge q, Path p, Node s, Node o) { return defaultValue(); } @@ -81,44 +80,44 @@ default boolean result(Eval eval, Mappings map, Mapping m) { return true; } - default IDatatype statement(Eval eval, Node g, Exp e) { + default DatatypeValue statement(Eval eval, Node g, Exp e) { return defaultValue(); } - default IDatatype bgp(Eval eval, Node g, Exp e, Mappings m) { + default DatatypeValue bgp(Eval eval, Node g, Exp e, Mappings m) { return defaultValue(); } - default IDatatype join(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + default DatatypeValue join(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { return defaultValue(); } - default IDatatype optional(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + default DatatypeValue optional(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { return defaultValue(); } - default IDatatype minus(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + default DatatypeValue minus(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { return defaultValue(); } - default IDatatype union(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + default DatatypeValue union(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { return defaultValue(); } - default IDatatype graph(Eval eval, Node g, Exp e, Mappings m) { + default DatatypeValue graph(Eval eval, Node g, Exp e, Mappings m) { return defaultValue(); } - default IDatatype query(Eval eval, Node g, Exp e, Mappings m) { + default DatatypeValue query(Eval eval, Node g, Exp e, Mappings m) { return defaultValue(); } - default IDatatype service(Eval eval, Node s, Exp e, Mappings m) { + default DatatypeValue service(Eval eval, Node s, Exp e, Mappings m) { return defaultValue(); } - default IDatatype values(Eval eval, Node g, Exp e, Mappings m) { + default DatatypeValue values(Eval eval, Node g, Exp e, Mappings m) { return defaultValue(); } @@ -130,15 +129,15 @@ default boolean having(Eval eval, Expr e, boolean b) { return b; } - default IDatatype bind(Eval eval, Node g, Exp e, IDatatype val) { + default DatatypeValue bind(Eval eval, Node g, Exp e, DatatypeValue val) { return val; } - default IDatatype select(Eval eval, Expr e, IDatatype val) { + default DatatypeValue select(Eval eval, Expr e, DatatypeValue val) { return val; } - default IDatatype aggregate(Eval eval, Expr e, IDatatype val) { + default DatatypeValue aggregate(Eval eval, Expr e, DatatypeValue val) { return val; } @@ -158,7 +157,7 @@ default boolean filter() { return false; } - default int compare(Eval eval, int res, IDatatype dt1, IDatatype dt2) { + default int compare(Eval eval, int res, DatatypeValue dt1, DatatypeValue dt2) { return res; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Producer.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Producer.java index cc40a0d18..b644f654e 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Producer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/Producer.java @@ -1,11 +1,11 @@ package fr.inria.corese.core.next.query.impl.kgram.api.query; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.kgram.api.core.*; import fr.inria.corese.core.next.query.impl.kgram.core.Exp; import fr.inria.corese.core.next.query.impl.kgram.core.Mappings; import fr.inria.corese.core.next.query.impl.kgram.core.Query; import fr.inria.corese.core.next.query.impl.kgram.core.SparqlException; -import fr.inria.corese.core.sparql.api.IDatatype; import java.util.List; @@ -24,8 +24,8 @@ public interface Producer { * KGRAM calls this method before executing a query. It enables to * initialize the Producer */ - @SuppressWarnings("unused") default void init(Query q) { + // Optional lifecycle hook for producer implementations. } default void start(Query q) { @@ -125,6 +125,7 @@ Iterable getNodes(Node gNode, List from, Edge edge, Environment env, * exist) * @return Iterable of start nodes for exp */ + @SuppressWarnings("java:S107") // Core KGRAM Producer interface method requires 8 execution parameters Iterable getEdges(Node gNode, List from, Edge qEdge, Environment env, Regex exp, Node src, Node start, int index); @@ -140,11 +141,11 @@ Iterable getEdges(Node gNode, List from, Edge qEdge, Environment env */ Node getNode(Object value); - // cast java value into IDatatype value - IDatatype getValue(Object value); + // Cast a Java or RDF value into the native datatype contract. + DatatypeValue getValue(Object value); - // DatatypeValue from IDatatype or from Java value - IDatatype getDatatypeValue(Object value); + // Native datatype value from an RDF or Java value. + DatatypeValue getDatatypeValue(Object value); /** * use case: filter (?x = ?y) filter(?x = 'cst') is it possible to bind ?x @@ -164,9 +165,9 @@ Iterable getEdges(Node gNode, List from, Edge qEdge, Environment env * @param qNodes the query nodes to bind with values of object * @return Mappings */ - Mappings map(List qNodes, IDatatype value); + Mappings map(List qNodes, DatatypeValue value); - Mappings map(List qNodes, IDatatype value, int n); + Mappings map(List qNodes, DatatypeValue value, int n); /** * graph node { } Node node represents (contains) a graph diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Checker.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Checker.java index 680cbb099..0a01da6a0 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Checker.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Checker.java @@ -21,7 +21,7 @@ */ public class Checker { - static Logger logger = LoggerFactory.getLogger(Checker.class); + private static final Logger LOGGER = LoggerFactory.getLogger(Checker.class); Eval eval; Producer producer; @@ -95,8 +95,9 @@ void edge(Node gNode, Exp exp, Environment env) { map = ee.query(q); define = !map.isEmpty(); report(edge, exist, match, define); - } catch (SparqlException e) { - throw new RuntimeException(e); + } catch (SparqlException failure) { + throw new IllegalStateException( + "Failed to evaluate the edge definition query", failure); } } else { @@ -108,12 +109,12 @@ void edge(Node gNode, Exp exp, Environment env) { void report(Edge edge, boolean exist, boolean match, boolean define) { query.addInfo(edge.toString(), " defined:" + define + " exist: " + exist + " match: " + match); - logger.info("Edge: {}: {} {} {}", edge, exist, match, define); + LOGGER.info("Edge: {}: {} {} {}", edge, exist, match, define); } void report(Edge edge, boolean exist, boolean match) { query.addInfo(edge.toString(), " exist: " + exist + " match: " + match); - logger.info("Edge: {}: {} {}", edge, exist, match); + LOGGER.info("Edge: {}: {} {}", edge, exist, match); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/CompleteSPARQL.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/CompleteSPARQL.java index 835361afe..af8a69777 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/CompleteSPARQL.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/CompleteSPARQL.java @@ -8,7 +8,7 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.Filter; import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +39,7 @@ public class CompleteSPARQL { * group by, order by * */ - void complete(Producer p, Mappings map) throws SparqlException { + void complete(Producer p, Mappings map) { selectExpression(query, p, map); distinct(query, map); orderGroup(query, p, map); @@ -55,10 +55,9 @@ void distinct(Query q, Mappings map) { map.submit(m); }); } - @SuppressWarnings("UnusedReturnValue") - Mappings selectExpression(Query q, Producer p, Mappings map) throws SparqlException { + void selectExpression(Query q, Producer p, Mappings map) { if (query.isSelectExpression()) { - HashMap bnode = new HashMap<>(); + HashMap bnode = new HashMap<>(); for (Mapping m : map) { bnode.clear(); m.setMap(bnode); @@ -69,39 +68,15 @@ Mappings selectExpression(Query q, Producer p, Mappings map) throws SparqlExcept } } } - return map; } - Mapping selectExpression(Query q, Producer p, Mapping m) throws SparqlException { + Mapping selectExpression(Query q, Producer p, Mapping m) { ArrayList ql = new ArrayList<>(); ArrayList tl = new ArrayList<>(); for (Exp e : q.getSelectFun()) { - Filter f = e.getFilter(); - if (f != null) { - // select (exp as ?y) - if (e.isAggregate()) { - // processed later, need place holder - if (m.getNodeValue(e.getNode()) == null) { - ql.add(e.getNode()); - tl.add(null); - } - } else { - Node qnode = e.getNode(); - Node tnode = eval.eval(null, f, m, p); - if (tnode != null) { - Node val = m.getNodeValue(qnode); - if (val == null) { - // bind e.getNode() = node - ql.add(qnode); - tl.add(tnode); - m.setNodeValue(qnode, tnode); - } else if (!val.equals(tnode)) { - // error: select var != bgp var - return null; - } - } - } + if (!completeProjection(e, p, m, ql, tl)) { + return null; } } @@ -112,7 +87,36 @@ Mapping selectExpression(Query q, Producer p, Mapping m) throws SparqlException return m; } - void orderGroup(Query q, Producer p, Mappings map) throws SparqlException { + private boolean completeProjection(Exp expression, Producer producer, Mapping mapping, + List queryNodes, List targetNodes) { + Filter filter = expression.getFilter(); + if (filter == null) { + return true; + } + Node queryNode = expression.getNode(); + Node current = mapping.getNodeValue(queryNode); + if (expression.isAggregate()) { + // Aggregates are evaluated later; reserve their output slot now. + if (current == null) { + queryNodes.add(queryNode); + targetNodes.add(null); + } + return true; + } + Node result = eval.eval(null, filter, mapping, producer); + if (result == null) { + return true; + } + if (current != null) { + return current.equals(result); + } + queryNodes.add(queryNode); + targetNodes.add(result); + mapping.setNodeValue(queryNode, result); + return true; + } + + void orderGroup(Query q, Producer p, Mappings map) { for (Mapping m : map) { Node[] snode = new Node[q.getOrderBy().size()]; Node[] gnode = new Node[q.getGroupBy().size()]; @@ -123,7 +127,7 @@ void orderGroup(Query q, Producer p, Mappings map) throws SparqlException { } } - void orderGroup(List lExp, Node[] nodes, Producer p, Mapping m) throws SparqlException { + void orderGroup(List lExp, Node[] nodes, Producer p, Mapping m) { int n = 0; for (Exp e : lExp) { Node qNode = e.getNode(); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Eval.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Eval.java index 91d627d1c..82ccc6103 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Eval.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Eval.java @@ -1,6 +1,6 @@ package fr.inria.corese.core.next.query.impl.kgram.core; -import fr.inria.corese.core.next.query.impl.kgram.adapter.DatatypeAdapter; +import fr.inria.corese.core.next.data.Values; import fr.inria.corese.core.next.query.impl.kgram.api.core.*; import fr.inria.corese.core.next.query.impl.kgram.api.query.*; import fr.inria.corese.core.next.query.impl.kgram.event.Event; @@ -8,8 +8,8 @@ import fr.inria.corese.core.next.query.impl.kgram.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.kgram.event.ResultListener; import fr.inria.corese.core.next.query.impl.kgram.path.PathFinder; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.datatype.DatatypeMap; +import fr.inria.corese.core.next.query.impl.kgram.tool.NodeImpl; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -29,28 +29,21 @@ * Node (a property variable) *

*

- * TODO: optimize: query ordering, search by dichotomy (in a cache) + * Note: future optimization: query ordering, search by dichotomy (in a cache) * * @author Olivier Corby, Edelweiss, INRIA 2010 */ -public class Eval implements ExpType, Plugin { +public final class Eval implements ExpType, Plugin { static final int STOP = -2; - // true = new processing of named graph - public static boolean JOIN_MAPPINGS = true; - public static int DISPLAY_RESULT_MAX = 10; - public static int count = 0; - static Logger logger = LoggerFactory.getLogger(Eval.class); + private static final int DISPLAY_RESULT_LIMIT = 10; + static final Logger logger = LoggerFactory.getLogger(Eval.class); // draft test: when edge() has Mappings map parameter, push clause values(map) - private static boolean pushEdgeMappings = true; + private boolean pushEdgeMappings = true; // draft test: graph() has Mappings map parameter and eval body with map parameter - private static boolean parameterGraphMappings = true; + private boolean parameterGraphMappings = true; // draft test: union() has Mappings map parameter and eval branch with map parameter - private static boolean parameterUnionMappings = true; - - static { - setNewMappingsVersion(true); - } + private boolean parameterUnionMappings = true; KgramEventDispatcher manager; boolean hasEvent = false; @@ -67,9 +60,9 @@ public class Eval implements ExpType, Plugin { Memory memory; Query query; - Mappings results, + Mappings results; // initial results to be completed - initialResults; + Mappings initialResults; EvalSPARQL evalSparql; CompleteSPARQL completeSparql; List empty = new ArrayList<>(0); @@ -95,13 +88,13 @@ public class Eval implements ExpType, Plugin { // Edge and Node producer private Producer producer; private Stack current; - private final boolean hasListener = false; + private static final boolean HAS_LISTENER = false; private int nbResult; private boolean hasCandidate = false; private boolean hasStatement = false; private boolean hasProduce = false; - private boolean stop = false; - private final boolean joinMappings = JOIN_MAPPINGS; + private boolean stopped = false; + private boolean joinMappings = true; public Eval() { } @@ -130,31 +123,31 @@ public static Eval create(Producer p, Evaluator e, Matcher m) { return new Eval(p, e, m); } - public static boolean isPushEdgeMappings() { + public boolean isPushEdgeMappings() { return pushEdgeMappings; } - public static void setPushEdgeMappings(boolean aPushEdgeMappings) { + public void setPushEdgeMappings(boolean aPushEdgeMappings) { pushEdgeMappings = aPushEdgeMappings; } - public static boolean isParameterGraphMappings() { + public boolean isParameterGraphMappings() { return parameterGraphMappings; } - public static void setParameterGraphMappings(boolean aParameterGraphMappings) { + public void setParameterGraphMappings(boolean aParameterGraphMappings) { parameterGraphMappings = aParameterGraphMappings; } - public static boolean isParameterUnionMappings() { + public boolean isParameterUnionMappings() { return parameterUnionMappings; } - public static void setParameterUnionMappings(boolean aParameterUnionMappings) { + public void setParameterUnionMappings(boolean aParameterUnionMappings) { parameterUnionMappings = aParameterUnionMappings; } - public static void setNewMappingsVersion(boolean b) { + public void setNewMappingsVersion(boolean b) { setPushEdgeMappings(b); setParameterGraphMappings(b); setParameterUnionMappings(b); @@ -205,7 +198,6 @@ Mappings queryBasic(Node graphNode, Query q, Mapping m) throws SparqlException { if (hasEvent) { send(Event.END, q, map); } - map.setBindingContext(getBind()); clean(); return map; } @@ -219,7 +211,7 @@ void share(Mapping m) { if (m.getBind().getVisitor() != null) { // use case: let (?g = construct where) // see Interpreter exist() getMapping() - setVisitor((ProcessVisitor) m.getBind().getVisitor()); + setVisitor(m.getBind().getVisitor()); } } } @@ -229,7 +221,7 @@ void share(Mapping m) { // use case: metadata @share void share(ProcessVisitor vis) { if (vis.isShareable() && getBind().getVisitor() == null) { - getBind().setVisitor((fr.inria.corese.core.kgram.api.query.ProcessVisitor) vis); + getBind().setVisitor(vis); } } @@ -280,7 +272,7 @@ void queryWE(Node gNode, Query q, Mapping m, Mappings map) throws SparqlExceptio query(gNode, q, m, map); } catch (SparqlException ex) { if (ex.isStop()) { - // LDScriptException stop means stop query processing + // LDScriptException stopped means stopped query processing return; } // exception means this is an error @@ -295,6 +287,7 @@ void queryWE(Node gNode, Query q, Mapping m, Mappings map) throws SparqlExceptio * Mappings map is results or previous statement, possibly null * use case: optional(A, B) map = relevant subset of results of A */ + @SuppressWarnings("java:S1845") // Query execution method named query by convention void query(Node gNode, Query q, Mapping m, Mappings map) throws SparqlException { if (m != null) { // bind mapping variables into memory @@ -320,12 +313,12 @@ void queryWithValues(Node gNode, Query q, Mappings map) if (!values.isPostpone() && !q.isAlgebra()) { for (Mapping m : values.getMappings()) { - if (stop) { + if (stopped) { return; } if (valuesBinding(values.getNodeList(), m, -1)) { eval(gNode, q, map); - free(values.getNodeList(), m); + free(values.getNodeList()); } } return; @@ -334,10 +327,9 @@ void queryWithValues(Node gNode, Query q, Mappings map) } void eval(Node gNode, Query q, Mappings map) throws SparqlException { - evalExp(gNode, q, q.getBody(), map); + evalExp(gNode, q.getBody(), map); } - @SuppressWarnings("unused") - void evalExp(Node gNode, Query q, Exp exp, Mappings map) + void evalExp(Node gNode, Exp exp, Mappings map) throws SparqlException { Stack stack = Stack.create(exp); set(stack); @@ -348,7 +340,7 @@ void evalExp(Node gNode, Query q, Exp exp, Mappings map) * We just counted number of results: nbResult Just build a Mapping */ void countProfile() { - Node n = (Node) DatatypeMap.newInstance(nbResult); + Node n = NodeImpl.forValue(Values.factory().createLiteral(nbResult)); Mapping m = Mapping.create(getQuery().getSelectFun().getFirst().getNode(), n); getResults().add(m); } @@ -389,6 +381,24 @@ public Mappings subEval(Producer p, Node gNode, Node queryNode, Exp exp, Exp mai return subEval(p, gNode, queryNode, exp, main, map, null, false, false); } + /** + * Evaluates an EXISTS graph pattern against the bindings of the current solution. + * The nested evaluation receives a fresh memory while copying every currently + * bound query node, so correlated variables retain SPARQL EXISTS semantics. + */ + public boolean exists(Producer producer, Node graphNode, Exp pattern) throws SparqlException { + Memory nestedMemory = new Memory(match, evaluator); + evaluator.init(nestedMemory); + nestedMemory.init(getQuery()); + nestedMemory.setAppxSearchEnv(getMemory().getAppxSearchEnv()); + getMemory().copyInto(nestedMemory, pattern); + + Eval nested = copy(nestedMemory, producer); + Mappings mappings = nested.subEval( + getQuery(), graphNode, Stack.create(pattern), null, 0); + return !mappings.isEmpty(); + } + /** * external = false : graphNode is named graph URI or null, queryGraphNode is meaningless * external = true : graphNode is external graph, @@ -400,6 +410,7 @@ public Mappings subEval(Producer p, Node gNode, Node queryNode, Exp exp, Exp mai * main is embedding statement of exp (main = A optional B, exp = A | exp = B) * map and m are possible bindings stemming from previous statement evaluation */ + @SuppressWarnings("java:S107") // Internal KGRAM sub-evaluation method requires 9 execution parameters Mappings subEval(Producer p, Node graphNode, Node queryGraphNode, Exp exp, Exp main, Mappings map, Mapping m, boolean bind, boolean external) throws SparqlException { Memory mem = new Memory(match, getEvaluator()); @@ -422,7 +433,7 @@ Mappings subEval(Producer p, Node graphNode, Node queryGraphNode, Exp exp, Exp m // Producer p is bound to external named graph graphNode = null; } - bind(mem, exp, main, map, m, bind); + bind(mem, exp, main, m, bind); return eval.subEval(getQuery(), graphNode, Stack.create(exp), map, 0); } @@ -430,7 +441,7 @@ Mappings subEval(Producer p, Node graphNode, Node queryGraphNode, Exp exp, Exp m * subEval with bind parameters * freshMemory inherits data to evaluate exp */ - void bind(Memory freshMemory, Exp exp, Exp main, Mappings map, Mapping m, boolean bind) { + void bind(Memory freshMemory, Exp exp, Exp main, Mapping m, boolean bind) { if (m != null) { freshMemory.push(m, -1); } @@ -520,6 +531,10 @@ public Eval copy(Memory m, Producer p, boolean extern) { // q may be the subQuery Eval copy(Memory m, Producer p, Evaluator e, Query q, boolean extern) { Eval ev = create(p, e, getMatcher()); + ev.pushEdgeMappings = pushEdgeMappings; + ev.parameterGraphMappings = parameterGraphMappings; + ev.parameterUnionMappings = parameterUnionMappings; + ev.joinMappings = joinMappings; if (q != null) { ev.complete(q); } @@ -646,7 +661,7 @@ void complete(Query q) { } void profile(Query q) { - // select (count(*) as ?c) where {} + // select count(*) as ?c where pattern // do not built Mapping, just count them if (q.getQueryProfile() == Query.COUNT_PROFILE) { storeResult = false; @@ -692,11 +707,11 @@ private void complete() { results.complete(this); } - private void aggregate() throws SparqlException { + private void aggregate() { results.aggregate(evaluator, memory, getProducer()); } - private void template() throws SparqlException { + private void template() { results.template(evaluator, memory, getProducer()); } @@ -731,20 +746,17 @@ private PathFinder getPathFinder(Exp exp, Producer p) { } PathFinder pathFinder = PathFinder.create(this, p, query); - if (hasEvent) { - pathFinder.set(manager); - } pathFinder.set(getListener()); pathFinder.setList(query.getGlobalQuery().isListPath()); // rdf:type/rdfs:subClassOf* generated system path does not store the list of edges // to be optimized pathFinder.setStorePath(query.getGlobalQuery().isStorePath() && !exp.isSystem()); pathFinder.setCache(query.getGlobalQuery().isCachePath()); - // TODO: subQuery + // Note: subQuery pathFinder.setCheckLoop(query.isCheckLoop()); pathFinder.setCountPath(query.isCountPath()); - pathFinder.init(exp.getRegex(), exp.getObject(), exp.getMin(), exp.getMax()); - // TODO: check this with clean() + pathFinder.init(exp.getRegex(), exp.getMin(), exp.getMax()); + // Note: check this with clean() if (p.getMode() != Producer.EXTENSION && p.getQuery() == memory.getQuery()) { // do nothing } else { @@ -763,7 +775,7 @@ public void clean() { } } - private int solution(Producer p, Mapping m, int n) throws SparqlException { + private int solution(Producer p, Mapping m, int n) { int backtrack = n - 1; int status = store(p, m); if (status == STOP) { @@ -815,6 +827,7 @@ int eval(Producer p, Node gNode, Stack stack, int n) throws SparqlException { * It can be passed recursively through several statements: join(A, optional(union(B, C), D)) * Eventually, and() edge() path() transform Mappings map into values clause */ + @SuppressWarnings("java:S3776") // Core KGRAM evaluation dispatch loop coordinating stack traversal and backtracking int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws SparqlException { int backtrack = n - 1; boolean isEvent = hasEvent; @@ -832,7 +845,7 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws Sp } Exp exp = stack.get(n); - if (hasListener) { + if (HAS_LISTENER) { // rule engine may have a ResultWatcher listener exp = getListener().listen(exp, n); } @@ -889,7 +902,7 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws Sp // @note: map processing is not optimal for service with union // we pass mappings only for variables that are in-scope in // both branches of the union - // it can be bypassed with values var {undef} + // it can be bypassed with values var (undef) backtrack = service(p, graphNode, exp, map, stack, n); break; @@ -899,7 +912,7 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws Sp break; case UNION: - backtrack = union(p, graphNode, exp, map, stack, n); + backtrack = union(p, graphNode, exp, map, n); break; case OPTIONAL: @@ -959,6 +972,9 @@ int eval(Producer p, Node graphNode, Stack stack, Mappings map, int n) throws Sp backtrack = eval(p, graphNode, stack, n + 1); } break; + + default: + break; } } } @@ -975,7 +991,7 @@ private int minus(Producer p, Node graphNode, Exp exp, Mappings data, Stack stac Memory env = getMemory(); Mappings map1 = subEval(p, graphNode, null, exp.first(), exp, data); - if (stop) { + if (stopped) { return STOP; } if (map1.isEmpty()) { @@ -994,7 +1010,7 @@ private int minus(Producer p, Node graphNode, Exp exp, Mappings data, Stack stac set.start(); for (Mapping map : map1) { - if (stop) { + if (stopped) { return STOP; } boolean ok = !set.minusCompatible(map); @@ -1016,12 +1032,12 @@ boolean isFederate(Exp exp) { return exp.isRecFederate(); } - private int union(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { + private int union(Producer p, Node graphNode, Exp exp, Mappings data, int n) throws SparqlException { int backtrack = n - 1; // join(A, union(B, C)) ; map = eval(A).distinct(inscopenodes()) Mappings map1 = unionBranch(p, graphNode, exp.first(), exp, data); - if (stop) { + if (stopped) { return STOP; } Mappings map2 = unionBranch(p, graphNode, exp.rest(), exp, data); @@ -1094,7 +1110,7 @@ private int bgp(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws Mappings map = p.getMappings(graphNode, from, exp, getMemory()); for (Mapping m : map) { - if (stop) { + if (stopped) { return STOP; } m.fixQueryNodes(getQuery()); @@ -1125,7 +1141,7 @@ private int service(Producer p, Node graphNode, Exp exp, Mappings data, Stack st Mappings map = getProvider().service(node, exp, selectQueryMappings(data), this); for (Mapping m : map) { - if (stop) { + if (stopped) { return STOP; } // push each Mapping in memory and continue @@ -1217,7 +1233,7 @@ private int extBind(Producer p, Node graphNode, Exp exp, Stack stack, int n) thr if (map != null) { HashMap tab = toMap(exp.getNodeList()); for (Mapping m : map) { - if (stop) { + if (stopped) { return STOP; } if (env.push(tab, m, n)) { @@ -1268,45 +1284,43 @@ private int filter(Producer p, Node graphNode, Exp exp, Stack stack, int n) thro return backtrack; } - boolean test(Node graphNode, Filter f, Environment env, Producer p) throws SparqlException { + boolean test(Node graphNode, Filter f, Environment env, Producer p) { try { env.setGraphNode(graphNode); - IDatatype dt = eval(f, env, p); + DatatypeValue dt = eval(f, env, p); return isTrue(dt); } finally { env.setGraphNode(null); } } - boolean isTrue(IDatatype dt) { + boolean isTrue(DatatypeValue dt) { if (dt == null) { return false; } - return dt.isTrueTest(); + return dt.isTrue(); } - Node eval(Node graphNode, Filter f, Environment env, Producer p) throws SparqlException { + Node eval(Node graphNode, Filter f, Environment env, Producer p) { try { env.setGraphNode(graphNode); - return (Node) eval(f.getExp(), env, p); + return p.getNode(eval(f.getExp(), env, p)); } finally { env.setGraphNode(null); } } - IDatatype eval(Filter f, Environment env, Producer p) { - DatatypeValue result = (DatatypeValue) f.getExp().evalWE(getEvaluator(), env.getBind(), env, p); - return DatatypeAdapter.unwrap(result); + DatatypeValue eval(Filter f, Environment env, Producer p) { + return f.getExp().evalWE(getEvaluator(), env.getBind(), env, p); } - IDatatype eval(Expr e, Environment env, Producer p) { - DatatypeValue result = (DatatypeValue) e.evalWE(getEvaluator(), env.getBind(), env, p); - return DatatypeAdapter.unwrap(result); + DatatypeValue eval(Expr e, Environment env, Producer p) { + return e.evalWE(getEvaluator(), env.getBind(), env, p); } - // values var { unnext(exp) } - // @todo Producer is not current producer but global producer + // values var ( unnext(exp) ) + // Note: Producer is not current producer but global producer Mappings eval(Filter f, Environment env, List nodes) { return eval(f, env, getProducer(), nodes); } @@ -1316,12 +1330,12 @@ Mappings eval(Filter f, Environment env, Producer p, List nodes) { int n = 1; Expr exp = f.getExp(); if (exp.oper() == UNNEST) { - if (hasListener) { + if (HAS_LISTENER) { listener.listen(exp); } if (exp.arity() == 2) { // unnest(exp, 2) - IDatatype dt = eval(exp.getExp(1), env, p); + DatatypeValue dt = eval(exp.getExp(1), env, p); if (dt == null) { return new Mappings(); } @@ -1329,7 +1343,7 @@ Mappings eval(Filter f, Environment env, Producer p, List nodes) { } exp = exp.getExp(0); } - IDatatype res = eval(exp, env, p); + DatatypeValue res = eval(exp, env, p); if (res == null) { return new Mappings(); } @@ -1349,42 +1363,29 @@ Mappings eval(Filter f, Environment env, Producer p, List nodes) { * data may be null */ private int path(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { - int backtrack = n - 1, evENUM = Event.ENUM; + int backtrack = n - 1; + int evENUM = Event.ENUM; PathFinder path = getPathFinder(exp, p); - Filter f = null; Memory env = getMemory(); Query qq = getQuery(); boolean isEvent = hasEvent; if (data != null && data.getNodeList() != null && isPushEdgeMappings()) { // push values(data) before edge in stack - logger.info("Push path mappings:\nvalue {}\n{}", data.getNodeList(), data.toString(false, false, DISPLAY_RESULT_MAX)); + logger.info("Push path mappings:\nvalue {}\n{}", + data.getNodeList(), data.toString(false, false, DISPLAY_RESULT_LIMIT)); return eval(p, graphNode, stack.addCopy(n, exp.getValues(data)), n); } - if (stack.size() > n + 1) { - if (stack.get(n + 1).isFilter()) { - f = stack.get(n + 1).getFilter(); - } - } - + Filter f = getFilterForPath(stack, n); path.start(exp.getEdge(), qq.getPathNode(), env, f); boolean isSuccess = false; - List list = qq.getFrom(graphNode); - Node backtrackNode = graphNode; - - if (p.getMode() == Producer.EXTENSION) { - if (p.getQuery() == env.getQuery()) { - list = empty; - backtrackNode = p.getGraphNode(); - } else { - backtrackNode = null; - } - } + List list = getGraphNodeList(p, graphNode, env, qq); + Node backtrackNode = getBacktrackNode(p, graphNode, env); for (Mapping map : path.candidate(graphNode, list, env)) { - if (stop) { + if (stopped) { path.stop(); return STOP; } @@ -1416,17 +1417,41 @@ private int path(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack return backtrack; } + private Filter getFilterForPath(Stack stack, int n) { + if (stack.size() > n + 1 && stack.get(n + 1).isFilter()) { + return stack.get(n + 1).getFilter(); + } + return null; + } + + private Node getBacktrackNode(Producer p, Node graphNode, Memory env) { + if (p.getMode() == Producer.EXTENSION) { + if (p.getQuery() == env.getQuery()) { + return p.getGraphNode(); + } + return null; + } + return graphNode; + } + + private List getGraphNodeList(Producer p, Node graphNode, Memory env, Query qq) { + if (p.getMode() == Producer.EXTENSION && p.getQuery() == env.getQuery()) { + return empty; + } + return qq.getFrom(graphNode); + } + private int values(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws SparqlException { int backtrack = n - 1; getVisitor().values(this, getGraphNode(graphNode), exp, exp.getMappings()); for (Mapping map : exp.getMappings()) { - if (stop) { + if (stopped) { return STOP; } if (valuesBinding(exp.getNodeList(), map, n)) { backtrack = eval(p, graphNode, stack, n + 1); - free(exp.getNodeList(), map); + free(exp.getNodeList()); if (backtrack < n) { return backtrack; @@ -1470,7 +1495,7 @@ void popBinding(List varList, Mapping map, int i) { } } - void free(List varList, Mapping map) { + void free(List varList) { for (Node qNode : varList) { getMemory().pop(qNode); } @@ -1487,6 +1512,7 @@ void free(List varList, Mapping map) { * data is possible relevant bindings coming from preceding statement evaluation * data may be null */ + @SuppressWarnings("java:S3776") // Core KGRAM edge evaluation algorithm managing graph matching and backjump optimization private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { int backtrack = n - 1; int evENUM = Event.ENUM; @@ -1506,11 +1532,9 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack Node backtrackGraphNode = graphNode; boolean matchNBNode = qEdge.isMatchArity(); - if (data != null && data.getNodeList() != null) { - if (isPushEdgeMappings()) { - // push values(data) before edge in stack - return eval(p, graphNode, stack.addCopy(n, exp.getValues(data)), n); - } + if (data != null && data.getNodeList() != null && isPushEdgeMappings()) { + // push values(data) before edge in stack + return eval(p, graphNode, stack.addCopy(n, exp.getValues(data)), n); } if (p.getMode() == Producer.EXTENSION) { @@ -1529,7 +1553,7 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack Iterable entities; if (hasProduce) { // draft not used - entities = produce(p, graphNode, graphNodeList, qEdge); + entities = produce(graphNode, qEdge); if (entities == null) { entities = p.getEdges(graphNode, graphNodeList, qEdge, env); } @@ -1539,13 +1563,13 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack for (Edge entity : entities) { - if (stop) { + if (stopped) { return STOP; } if (entity != null) { nbEdge++; - if (hasListener && !listener.listen(exp, qEdge, entity)) { + if (HAS_LISTENER && !listener.listen(exp, qEdge, entity)) { continue; } @@ -1558,13 +1582,13 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack if (bmatch) { if (hasCandidate) { - IDatatype dt = getVisitor().candidate(this, getGraphNode(graphNode), qEdge, entity); + DatatypeValue dt = getVisitor().candidate(this, getGraphNode(graphNode), qEdge, entity); if (dt != null) { - bmatch = dt.booleanValue(); + bmatch = dt.isTrue(); } } - bmatch &= push(p, qEdge, entity, graphNode, graph, n); + bmatch &= push(p, qEdge, entity, n); } if (isEvent) { @@ -1575,7 +1599,7 @@ private int edge(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack isSuccess = true; backtrack = eval(p, graphNode, stack, n + 1); - env.pop(qEdge, entity); + env.pop(qEdge); if (hasGraphNode) { env.pop(graphNode); } @@ -1621,7 +1645,7 @@ private int query(Producer p, Node graphNode, Exp exp, Mappings data, Stack stac // enumerate the result of the sub query // bind the select nodes into the stack for (Mapping map : lMap) { - if (stop) { + if (stopped) { return STOP; } boolean bmatch = push(subQuery, map, n); @@ -1681,19 +1705,17 @@ private boolean push(Query subQuery, Mapping res, int n) { outNode = exp.get(0).getNode(); } - if (node != null) { - // a value may be null because of an option {} - if (!(mm.match(outNode, node, env) && env.push(outNode, node, n))) { - for (int i = 0; i < k; i++) { - subNode = subQuery.getSelect().get(i); - outNode = qq.getOuterNodeSelf(subNode); - Node value = res.getNode(subNode); - if (value != null) { - env.pop(outNode); - } + if (node != null && !(mm.match(outNode, node, env) && env.push(outNode, node, n))) { + // a value may be null because of an optional pattern + for (int i = 0; i < k; i++) { + subNode = subQuery.getSelect().get(i); + outNode = qq.getOuterNodeSelf(subNode); + Node value = res.getNode(subNode); + if (value != null) { + env.pop(outNode); } - return false; } + return false; } k++; } @@ -1719,7 +1741,7 @@ private void pop(Query subQuery, Mapping ans) { /** * Store a new result */ - private int store(Producer p, Mapping m) throws SparqlException { + private int store(Producer p, Mapping m) { boolean store = true; if (getListener() != null) { store = getListener().process(getMemory()); @@ -1774,7 +1796,7 @@ private boolean match(Edge qEdge, Edge edge, Node gNode, Node graphNode, Memory return getMatcher().match(gNode, graphNode, memory); } - private boolean push(Producer p, Edge qEdge, Edge ent, Node gNode, Node node, int n) { + private boolean push(Producer p, Edge qEdge, Edge ent, int n) { Memory env = getMemory(); return env.push(p, qEdge, ent, n); } @@ -1857,11 +1879,11 @@ public void setVisitor(ProcessVisitor visitor) { } public boolean isStop() { - return stop; + return stopped; } - public void setStop(boolean stop) { - this.stop = stop; + public void setStop(boolean stopped) { + this.stopped = stopped; } @@ -1886,7 +1908,7 @@ public void finish() { * List from = query.getFrom(gNode); Mappings map = * p.getMappings(gNode, from, exp, memory); */ - Mappings exec(Node gNode, Producer p, Exp exp, Mapping m) throws SparqlException { + Mappings exec(Node gNode, Producer p, Exp exp) throws SparqlException { List from = query.getFrom(gNode); return p.getMappings(gNode, from, exp, memory); } @@ -1915,7 +1937,8 @@ private int optBind(Producer p, Node gNode, Exp exp, Stack stack, int n) throws } else { // ?x = ?y - int i = 0, j = 1; + int i = 0; + int j = 1; Node node = env.getNode(exp.get(i).getNode()); if (node == null) { i = 1; @@ -1949,54 +1972,49 @@ private int optBind(Producer p, Node gNode, Exp exp, Stack stack, int n) throws * exp : BIND{?x = cst1 || ?x = cst2} Bind ?x with all its values */ private int cbind(Producer p, Node gNode, Exp exp, Stack stack, int n) throws SparqlException { - int backtrack = n - 1; - Memory env = memory; - Producer prod = getProducer(); - Node qNode = exp.get(0).getNode(); - if (!exp.status() || env.isBound(qNode)) { + if (!exp.status() || memory.isBound(qNode)) { return eval(p, gNode, stack, n + 1); } if (exp.getNodeList() == null) { - // Constant are not yet transformed into Node - for (Object value : exp.getObjectValues()) { - // get constant Node - Expr cst = (Expr) value; - Node node = prod.getNode(cst.getValue()); - if (node != null && prod.isBindable(node)) { - // store constant Node into Bind expression - // TODO: - // if there are several producers, it is considered - // bindable for all producers. This may be a problem. - exp.addNode(node); - } else { - // Constant fails being a Node: stop binding - exp.setNodeList(null); - exp.status(false); - break; - } - } + initBindNodes(exp, getProducer()); } if (exp.getNodeList() != null) { - // get variable Node - for (Node node : exp.getNodeList()) { - // Enumerate constant Node - env.push(qNode, node, n); - if (hasEvent) { - send(Event.BIND, exp, qNode, node); - } - backtrack = eval(p, gNode, stack, n + 1); - env.pop(qNode); - if (backtrack < n) { - return backtrack; - } + return evalBindNodes(p, gNode, exp, stack, n, qNode); + } + return eval(p, gNode, stack, n + 1); + } + + private void initBindNodes(Exp exp, Producer prod) { + for (Object value : exp.getObjectValues()) { + Expr cst = (Expr) value; + Node node = prod.getNode(cst.getValue()); + if (node != null && prod.isBindable(node)) { + exp.addNode(node); + } else { + exp.setNodeList(null); + exp.status(false); + break; } - } else { - backtrack = eval(p, gNode, stack, n + 1); } - return backtrack; + } + + private int evalBindNodes(Producer p, Node gNode, Exp exp, Stack stack, int n, Node qNode) throws SparqlException { + Memory env = memory; + for (Node node : exp.getNodeList()) { + env.push(qNode, node, n); + if (hasEvent) { + send(Event.BIND, exp, qNode, node); + } + int backtrack = eval(p, gNode, stack, n + 1); + env.pop(qNode); + if (backtrack < n) { + return backtrack; + } + } + return n - 1; } /** @@ -2005,11 +2023,13 @@ private int cbind(Producer p, Node gNode, Exp exp, Stack stack, int n) throws Sp * * @deprecated */ + @Deprecated(since = "4.0.0") + @SuppressWarnings("java:S1133") private int bgpAble(Producer p, Node graphNode, Exp exp, Stack stack, int n) throws SparqlException { int backtrack = n - 1; Mappings map = getMappings(p, graphNode, exp); for (Mapping m : map) { - if (stop) { + if (stopped) { return STOP; } m.fixQueryNodes(getQuery()); @@ -2033,6 +2053,8 @@ private int bgpAble(Producer p, Node graphNode, Exp exp, Stack stack, int n) thr * * @deprecated */ + @Deprecated(since = "4.0.0") + @SuppressWarnings("java:S1133") Mappings getMappings(Producer p, Node graphNode, Exp exp) throws SparqlException { if (exp.hasCache()) { // @deprecated @@ -2052,15 +2074,13 @@ Mappings getMappings(Producer p, Node graphNode, Exp exp) throws SparqlException /** * Draf extension where a Visitor provides Edge iterator */ - Iterable produce(Producer p, Node gNode, List from, Edge edge) { - IDatatype res = getVisitor().produce(this, gNode, edge); + Iterable produce(Node gNode, Edge edge) { + DatatypeValue res = getVisitor().produce(this, gNode, edge); if (res == null) { return null; } - if (res.getNodeObject() != null && (res.getNodeObject() instanceof Iterable)) { - return new IterableEntity((Iterable) res.getNodeObject()); - } else if (res instanceof Loopable) { - Iterable loop = ((Loopable) res).getLoop(); + if (res instanceof Loopable loopable) { + Iterable loop = loopable.getLoop(); if (loop != null) { return new IterableEntity(loop); } @@ -2068,13 +2088,15 @@ Iterable produce(Producer p, Node gNode, List from, Edge edge) { return null; } + /** + * @deprecated Legacy debugging hook + */ @Override - @Deprecated + @Deprecated(since = "4.0.0") + @SuppressWarnings("java:S1133") public void exec(Exp exp, Environment env, int n) { - if (exp.getObject() instanceof String label) { - if (env.getNode(label) != null) { - logger.debug("{}: {} {}", n, label, env.getNode(label).getLabel()); - } + if (exp.getObject() instanceof String label && env.getNode(label) != null) { + logger.debug("{}: {} {}", n, label, env.getNode(label).getLabel()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalGraph.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalGraph.java index 9d90ce93f..6b2a07c05 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalGraph.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalGraph.java @@ -11,11 +11,11 @@ */ public class EvalGraph { - Eval eval; + Eval engine; boolean stop = false; EvalGraph(Eval e) { - eval = e; + engine = e; } void setStop() { @@ -29,44 +29,31 @@ void setStop() { int eval(Producer p, Node gNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { int backtrack = n - 1; Node graphNode = exp.getGraphName(); - Node graph = eval.getNode(p, graphNode); + Node graph = engine.getNode(p, graphNode); Mappings res; if (graph == null) { - res = graphNodes(p, exp, data, n); + res = graphNodes(p, exp, data); } else { - res = graph(p, graph, exp, data, n); + res = graph(p, graph, exp, data); } if (res == null) { return backtrack; } - Memory env = eval.getMemory(); + Memory env = engine.getMemory(); for (Mapping m : res) { if (stop) { return Eval.STOP; } - Node namedGraph; - if (graphNode.isVariable()) { - namedGraph = m.getNode(graphNode); - if (namedGraph != null && !namedGraph.equals(m.getNamedGraph())) { - continue; - } - } - - if (!env.push(m, n)) { + if (!pushGraphMapping(env, graphNode, m, n)) { continue; } - if (!env.push(graphNode, m.getNamedGraph())) { - env.pop(m); - continue; - } - - backtrack = eval.eval(p, gNode, stack, n + 1); + backtrack = engine.eval(p, gNode, stack, n + 1); env.pop(graphNode); env.pop(m); @@ -78,15 +65,30 @@ int eval(Producer p, Node gNode, Exp exp, Mappings data, Stack stack, int n) thr return backtrack; } + private boolean pushGraphMapping(Memory environment, Node graphNode, Mapping mapping, int index) { + Node boundGraph = graphNode.isVariable() ? mapping.getNode(graphNode) : null; + if (boundGraph != null && !boundGraph.equals(mapping.getNamedGraph())) { + return false; + } + if (!environment.push(mapping, index)) { + return false; + } + if (environment.push(graphNode, mapping.getNamedGraph())) { + return true; + } + environment.pop(mapping); + return false; + } + /** * Iterate named graph pattern evaluation on named graph list * named graph list may come from Mappings map from previous statement * OR from the "from named" clause OR from dataset named graph list */ - private Mappings graphNodes(Producer p, Exp exp, Mappings map, int n) throws SparqlException { - Memory env = eval.getMemory(); - Query qq = eval.getQuery(); - Matcher mm = eval.getMatcher(); + private Mappings graphNodes(Producer p, Exp exp, Mappings map) throws SparqlException { + Memory env = engine.getMemory(); + Query qq = engine.getQuery(); + Matcher mm = engine.getMatcher(); Node name = exp.getGraphName(); Mappings res = null; Iterable graphNodes = null; @@ -105,7 +107,7 @@ private Mappings graphNodes(Producer p, Exp exp, Mappings map, int n) throws Spa for (Node graph : graphNodes) { if (mm.match(name, graph, env)) { - Mappings m = graph(p, graph, exp, map, n); + Mappings m = graph(p, graph, exp, map); if (res == null) { res = m; } else { @@ -121,15 +123,12 @@ private Mappings graphNodes(Producer p, Exp exp, Mappings map, int n) throws Spa * Node graph: graph URI or Node graph pointer or Node path pointer * Exp exp: graph name { BGP } */ - @SuppressWarnings("unused") - private Mappings graph(Producer p, Node graph, Exp exp, Mappings map, int n) throws SparqlException { + private Mappings graph(Producer p, Node graph, Exp exp, Mappings map) throws SparqlException { boolean external = false; - Node graphNode = exp.getGraphName(); Producer np = p; if (graph != null && p.isProducer(graph)) { - // graph ? g { } - // named graph in GraphStore - np = p.getProducer(graph, eval.getMemory()); + // Named graph pattern in GraphStore + np = p.getProducer(graph, engine.getMemory()); np.setGraphNode(graph); // the new gNode external = true; } @@ -139,38 +138,33 @@ private Mappings graph(Producer p, Node graph, Exp exp, Mappings map, int n) thr Node varNode = null; Node target = null; - if (external) { - if (graphNode.isVariable() && graph.getDatatypeValue().isExtension()) { - varNode = graphNode; - target = graph; - } - } else { + if (!external) { target = graph; } - if (eval.isFederate(exp)) { - res = eval.subEval(np, target, varNode, body, exp, map, null, false, external); + if (engine.isFederate(exp)) { + res = engine.subEval(np, target, varNode, body, exp, map, null, false, external); } else { Exp ee = body; Mappings data = null; if (graph != null && graph.getPath() == null) { // not a path pointer - if (Eval.isParameterGraphMappings()) { - // eval graph body with parameter map - // pro: if body is optional, eval it with parameter map + if (engine.isParameterGraphMappings()) { + // engine graph body with parameter map + // pro: if body is optional, engine it with parameter map data = map; } else { - // eval graph body with values(map) + // engine graph body with values(map) ee = body.complete(map); } } - res = eval.subEval(np, target, varNode, ee, exp, data, null, false, external); + res = engine.subEval(np, target, varNode, ee, exp, data, null, false, external); } res.setNamedGraph(graph); - eval.getVisitor().graph(eval, graph, exp, res); + engine.getVisitor().graph(engine, graph, exp, res); return res; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalJoin.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalJoin.java index 0934be634..bad15ac5a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalJoin.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalJoin.java @@ -8,14 +8,14 @@ /** * @author corby */ +@SuppressWarnings("java:S3776") // Legacy KGRAM join execution algorithms public class EvalJoin { - public static boolean SORT_OVERLOAD = true; - Eval eval; + Eval engine; boolean stop = false; - EvalJoin(Eval eval) { - this.eval = eval; + EvalJoin(Eval engine) { + this.engine = engine; } void setStop() { @@ -23,19 +23,19 @@ void setStop() { } Query getQuery() { - return eval.getMemory().getQuery(); + return engine.getMemory().getQuery(); } /** - * JOIN(e1, e2) Eval e1, eval e2, generate all joins that are compatible in + * JOIN(e1, e2) Eval e1, engine e2, generate all joins that are compatible in * cartesian product */ int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) throws SparqlException { int backtrack = n - 1; - Memory env = eval.getMemory(); - Mappings map1 = eval.subEval(p, graphNode, graphNode, exp.first(), exp, data); + Memory env = engine.getMemory(); + Mappings map1 = engine.subEval(p, graphNode, graphNode, exp.first(), exp, data); if (map1.isEmpty()) { - eval.getVisitor().join(eval, eval.getGraphNode(graphNode), exp, map1, map1); + engine.getVisitor().join(engine, engine.getGraphNode(graphNode), exp, map1, map1); return backtrack; } Mappings map1Extended = map1; @@ -54,12 +54,12 @@ int eval(Producer p, Node graphNode, Exp exp, Mappings data, Stack stack, int n) MappingSet set1 = new MappingSet(getQuery(), map1Extended); Mappings joinMappings = null; - if (eval.isJoinMappings()) { + if (engine.isJoinMappings()) { joinMappings = set1.prepareMappingsRest(exp.rest()); } - Mappings map2 = eval.subEval(p, graphNode, graphNode, exp.rest(), exp, joinMappings); + Mappings map2 = engine.subEval(p, graphNode, graphNode, exp.rest(), exp, joinMappings); - eval.getVisitor().join(eval, eval.getGraphNode(graphNode), exp, map1, map2); + engine.getVisitor().join(engine, engine.getGraphNode(graphNode), exp, map1, map2); if (map2.isEmpty()) { return backtrack; @@ -87,6 +87,7 @@ int join(Producer p, Node graphNode, Stack stack, Memory env, Mappings map1, Map * enumerate map1 * retrieve the index of value of commonVariable in map2 by dichotomy */ + @SuppressWarnings("java:S107") // Internal KGRAM join execution method requires 8 execution parameters int joinWithCommonVariable(Node commonVariable, Producer p, Node graphNode, Stack stack, Memory env, Mappings map1, Mappings map2, int n) throws SparqlException { int backtrack = n - 1; if (map1.size() > map2.size()) { @@ -94,10 +95,8 @@ int joinWithCommonVariable(Node commonVariable, Producer p, Node graphNode, Stac map1 = map2; map2 = tmp; } - if (SORT_OVERLOAD) { - // setEval enable node comparison overload by Visitor compare() for extended datatypes - map2.setEval(eval); - } + // Enable comparison customization through the query-scoped visitor. + map2.setEval(engine); map2.sort(commonVariable); for (Mapping m1 : map1) { @@ -115,7 +114,7 @@ int joinWithCommonVariable(Node commonVariable, Producer p, Node graphNode, Stac return STOP; } if (env.push(m2, n)) { - backtrack = eval.eval(p, graphNode, stack, n + 1); + backtrack = engine.eval(p, graphNode, stack, n + 1); env.pop(m2); if (backtrack < n) { return backtrack; @@ -133,7 +132,7 @@ int joinWithCommonVariable(Node commonVariable, Producer p, Node graphNode, Stac break; } if (env.push(m2, n)) { - backtrack = eval.eval(p, graphNode, stack, n + 1); + backtrack = engine.eval(p, graphNode, stack, n + 1); env.pop(m2); if (backtrack < n) { return backtrack; @@ -154,7 +153,7 @@ int joinWithCommonVariable(Node commonVariable, Producer p, Node graphNode, Stac // map2 is sorted, if n1 != n2 we can exit the loop break; } else if (env.push(m2, n)) { - backtrack = eval.eval(p, graphNode, stack, n + 1); + backtrack = engine.eval(p, graphNode, stack, n + 1); env.pop(m2); if (backtrack < n) { return backtrack; @@ -185,7 +184,7 @@ int joinWithoutCommonVariable(Producer p, Node graphNode, Stack stack, Memory en return STOP; } if (env.push(m2, n)) { - backtrack = eval.eval(p, graphNode, stack, n + 1); + backtrack = engine.eval(p, graphNode, stack, n + 1); env.pop(m2); if (backtrack < n) { return backtrack; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalSPARQL.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalSPARQL.java index 1e339144d..75169b7df 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalSPARQL.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalSPARQL.java @@ -1,8 +1,9 @@ package fr.inria.corese.core.next.query.impl.kgram.core; import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; +import fr.inria.corese.core.next.query.impl.kgram.api.core.ExpType.Type; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,11 +19,11 @@ public class EvalSPARQL { private static final Logger logger = LoggerFactory.getLogger(EvalSPARQL.class); - Eval eval; + Eval engine; Query query; EvalSPARQL(Query q, Eval e) { - eval = e; + engine = e; query = q; } @@ -38,7 +39,7 @@ Mappings eval(Node graph, Producer p, Exp exp, Mapping m) { case UNION -> union(graph, p, exp, m); case MINUS -> minus(graph, p, exp, m); case OPTIONAL -> optional(graph, p, exp, m); - case GRAPH -> graph(graph, p, exp, m); + case GRAPH -> graph(p, exp, m); default -> Mappings.create(query); }; } @@ -73,12 +74,11 @@ Mappings join(Mappings map1, Mappings map2) { } // sort map2 according to common variable, null value first - map2.sort(eval, cmn); + map2.sort(engine, cmn); return map1.joiner(map2, cmn); } - @SuppressWarnings("unused") - Mappings graph(Node graph, Producer p, Exp exp, Mapping m) { + Mappings graph(Producer p, Exp exp, Mapping m) { return bgp(exp.getGraphName(), p, exp.rest(), m); } @@ -86,27 +86,15 @@ Mappings optional(Node graph, Producer p, Exp exp, Mapping mm) { Mappings m1 = bgp(graph, p, exp.first(), mm); Mappings m2 = bgp(graph, p, exp.rest()); Mappings res = Mappings.create(query); - HashMap hm = new HashMap<>(); + HashMap hm = new HashMap<>(); for (Mapping ma : m1) { int nbsuc = 0; for (Mapping mb : m2) { - boolean success ; Mapping m = ma.merge(mb); - if (m != null) { - success = true; - if (exp.isPostpone()) { - m.setQuery(query); - m.setMap(hm); - hm.clear(); - if (!postpone(graph, exp, m, p)) { - success = false; - } - } - if (success) { - res.add(m); - nbsuc++; - } + if (acceptOptionalMapping(graph, p, exp, m, hm)) { + res.add(m); + nbsuc++; } } @@ -117,13 +105,23 @@ Mappings optional(Node graph, Producer p, Exp exp, Mapping mm) { return res; } + private boolean acceptOptionalMapping(Node graph, Producer producer, Exp expression, + Mapping mapping, HashMap blankNodes) { + if (mapping == null) { + return false; + } + if (!expression.isPostpone()) { + return true; + } + mapping.setQuery(query); + mapping.setMap(blankNodes); + blankNodes.clear(); + return postpone(graph, expression, mapping, producer); + } + boolean postpone(Node gNode, Exp exp, Mapping m, Producer p) { for (Exp e : exp.getPostpone()) { - try { - if (!eval.test(gNode, e.getFilter(), m, p)) { - return false; - } - } catch (SparqlException ex) { + if (!engine.test(gNode, e.getFilter(), m, p)) { return false; } } @@ -175,24 +173,24 @@ Mappings bgp(Node graph, Producer p, Exp exp, Mapping m) { return eval(graph, p, body, m); } } - return basic(graph, p, exp, m); + return basic(graph, p, exp); } - Mappings basic(Node graph, Producer p, Exp exp, Mapping m) { - exp.setType(Exp.Type.AND); + Mappings basic(Node graph, Producer p, Exp exp) { + exp.setType(Type.AND); try { - return eval.exec(graph, p, exp, m); + return engine.exec(graph, p, exp); } catch (SparqlException ex) { logger.error("Error executing basic pattern: {}", ex.getMessage(), ex); return null; } finally { - exp.setType(Exp.Type.BGP); + exp.setType(Type.BGP); } } private Mappings filter(Producer p, Exp exp, Mappings map) { Mappings res = Mappings.create(map.getQuery()); - HashMap bnode = new HashMap<>(); + HashMap bnode = new HashMap<>(); for (Mapping m : map) { m.setMap(bnode); bnode.clear(); @@ -206,11 +204,7 @@ private Mappings filter(Producer p, Exp exp, Mappings map) { private boolean test(Producer p, Exp exp, Mapping m) { for (Exp f : exp) { - try { - if (!eval.test(null, f.getFilter(), m, p)) { - return false; - } - } catch (SparqlException ex) { + if (!engine.test(null, f.getFilter(), m, p)) { return false; } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Exp.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Exp.java index 3f846a59b..fb2cb4777 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Exp.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Exp.java @@ -10,7 +10,7 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.PointerType; import fr.inria.corese.core.next.query.impl.kgram.api.core.Regex; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; -import fr.inria.corese.core.sparql.triple.parser.Expression; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import java.util.ArrayList; import java.util.HashMap; @@ -31,9 +31,8 @@ public class Exp extends PointerObject public static final int OBJECT = 1; public static final int PREDICATE = 2; - static final String NL = "\n"; + static final String NEWLINE = "\n"; static final String SP = " "; - static Exp empty = new Exp(Type.EMPTY); Type type; int index = -1; // optional success @@ -55,15 +54,15 @@ public class Exp extends PointerObject // for UNION Stack stack; // for EXTERN - Object object; + Object payload; Producer producer; Regex regex; Mappings map; - HashMap cache; + HashMap mappingCache; int min = -1; int max = -1; private boolean isPostpone = false; - private boolean BGPAble = false; + private boolean bgpAble = false; private boolean isFunctional = false; private boolean generated = false; private Node arg; @@ -185,11 +184,11 @@ public boolean isBGPAnd() { } public boolean isBGPAble() { - return BGPAble; + return bgpAble; } - public void setBGPAble(boolean BGPAble) { - this.BGPAble = BGPAble; + public void setBGPAble(boolean bgpAble) { + this.bgpAble = bgpAble; } public Node getCacheNode() { @@ -279,12 +278,27 @@ StringBuilder toString(StringBuilder sb, int n) { sb.append(title()).append(SP); if (type() == Type.VALUES) { - sb.append(getNodeList()); - sb.append(SP); + sb.append(getNodeList()).append(SP); } sb.append("{"); + appendEdgeNodeFilter(sb); + + if (type() == Type.VALUES) { + nl(sb, 0); + sb.append(getMappings().toString(true)); + indent(sb, n); + } else if (type() != Type.WATCH && type() != Type.CONTINUE && type() != Type.BACKJUMP) { + // Process normal types (skip WATCH, CONTINUE, BACKJUMP because of loop) + appendChildren(sb, n); + } + + sb.append("}"); + return sb; + } + + private void appendEdgeNodeFilter(StringBuilder sb) { if (edge != null) { sb.append(edge); if (size() > 0) { @@ -306,32 +320,22 @@ StringBuilder toString(StringBuilder sb, int n) { sb.append(SP); } } + } - if (type() == Type.VALUES) { - nl(sb, 0); - sb.append(getMappings().toString(true)); - } else if (type() != Type.WATCH && type() != Type.CONTINUE && type() != Type.BACKJUMP) { - // Process normal types (skip WATCH, CONTINUE, BACKJUMP because of loop) - if (isOptional() && isPostpone()) { - sb.append("POSTPONE "); - getPostpone().toString(sb); - nl(sb, n); - } - for (Exp e : this) { - nl(sb, n); - e.toString(sb, n + 1).append(SP); - } + private void appendChildren(StringBuilder sb, int n) { + if (isOptional() && isPostpone()) { + sb.append("POSTPONE "); + getPostpone().toString(sb); + nl(sb, n); } - - if (type() == Type.VALUES) { - indent(sb, n); + for (Exp e : this) { + nl(sb, n); + e.toString(sb, n + 1).append(SP); } - sb.append("}"); - return sb; } void nl(StringBuilder sb, int n) { - sb.append(NL); + sb.append(NEWLINE); indent(sb, n); } @@ -479,7 +483,7 @@ public Exp first() { if (!args.isEmpty()) { return args.getFirst(); } else { - return empty; + return new Exp(Type.EMPTY); } } @@ -492,7 +496,6 @@ public Exp rest() { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return args.iterator(); } @@ -541,7 +544,7 @@ public void setFilter(Filter f) { } } - public Expression getFilterExpression() { + public TermAst getFilterExpression() { return getFilter().getFilterExpression(); } @@ -553,6 +556,7 @@ public List getFilters() { return lFilter; } + @SuppressWarnings("java:S1172") // Subclasses like ExpEdge override this method with specific filtering public List getFilters(int n, int t) { return new ArrayList<>(0); } @@ -646,11 +650,11 @@ public void addNode(Node n) { } public Object getObject() { - return object; + return payload; } public void setObject(Object o) { - object = o; + payload = o; } public Producer getProducer() { @@ -661,13 +665,11 @@ public void setProducer(Producer p) { producer = p; } - @SuppressWarnings("unchecked") public List getObjectValues() { - if (object instanceof List) { - return (List) object; - } else { - return new ArrayList<>(); + if (payload instanceof List list) { + return new ArrayList<>(list); } + return new ArrayList<>(); } public int getMin() { @@ -706,10 +708,7 @@ boolean distinct(Node qNode) { for (int i = 0; i < size(); i++) { Exp exp = get(i); switch (exp.type()) { - case EDGE: - case PATH: - case XPATH: - case EVAL: + case EDGE, PATH, XPATH, EVAL: if (exp.contains(qNode)) { add(i + 1, Exp.create(Type.ACCEPT, qNode)); return true; @@ -721,6 +720,9 @@ boolean distinct(Node qNode) { return true; } break; + + default: + break; } } } @@ -864,9 +866,7 @@ void addBind(Node node, List lVar) { */ int nbNode() { switch (type) { - case EDGE: - case PATH: - case EVAL: + case EDGE, PATH, EVAL: if (edge.getEdgeVariable() == null) { return edge.nbNode(); } else { @@ -875,9 +875,10 @@ int nbNode() { case OPT_BIND: return size(); - } - return 0; + default: + return 0; + } } /** @@ -885,9 +886,7 @@ int nbNode() { */ Node getNode(int n) { switch (type) { - case EDGE: - case PATH: - case EVAL: + case EDGE, PATH, EVAL: if (n < edge.nbNode()) { return edge.getNode(n); } else { @@ -896,8 +895,10 @@ Node getNode(int n) { case OPT_BIND: return get(n).getNode(); + + default: + return null; } - return null; } /** @@ -922,38 +923,19 @@ public boolean contains(Node node) { */ public void share(List filterVar, List expVar) { switch (type()) { - case FILTER: - case OPT_BIND, OPTION: + case FILTER, OPT_BIND, OPTION: break; - case OPTIONAL: - case MINUS: + case OPTIONAL, MINUS: first().share(filterVar, expVar); break; case UNION: - // must be bound in both branches - ArrayList lVar1 = new ArrayList<>(); - ArrayList lVar2 = new ArrayList<>(); - first().share(filterVar, lVar1); - rest().share(filterVar, lVar2); - for (String varString : lVar1) { - if (lVar2.contains(varString) && !expVar.contains(varString)) { - expVar.add(varString); - } - } + shareUnion(filterVar, expVar); break; case QUERY: - ArrayList lVar = new ArrayList<>(); - getQuery().getBody().share(filterVar, lVar); - - for (Exp exp : getQuery().getSelectFun()) { - String name = exp.getNode().getLabel(); - if ((lVar.contains(name) || exp.getFilter() != null) && !expVar.contains(name)) { - expVar.add(name); - } - } + shareQuery(filterVar, expVar); break; case BIND: @@ -961,8 +943,7 @@ public void share(List filterVar, List expVar) { // hence variable cannot be considered as bound for filter break; - case EDGE: - case PATH: + case EDGE, PATH: for (int i = 0; i < nbNode(); i++) { Node nodePath = getNode(i); share(nodePath, filterVar, expVar); @@ -988,6 +969,30 @@ void share(Node node, List fVar, List eVar) { } } + private void shareUnion(List filterVar, List expVar) { + ArrayList lVar1 = new ArrayList<>(); + ArrayList lVar2 = new ArrayList<>(); + first().share(filterVar, lVar1); + rest().share(filterVar, lVar2); + for (String varString : lVar1) { + if (lVar2.contains(varString) && !expVar.contains(varString)) { + expVar.add(varString); + } + } + } + + private void shareQuery(List filterVar, List expVar) { + ArrayList lVar = new ArrayList<>(); + getQuery().getBody().share(filterVar, lVar); + + for (Exp exp : getQuery().getSelectFun()) { + String name = exp.getNode().getLabel(); + if ((lVar.contains(name) || exp.getFilter() != null) && !expVar.contains(name)) { + expVar.add(name); + } + } + } + public boolean bound(List fvec, List evec) { for (String varString : fvec) { if (!evec.contains(varString)) { @@ -1007,34 +1012,22 @@ public boolean bound(List fvec, List evec) { void getNodes(ExpNodeCollector h) { switch (type()) { case FILTER: - // get exists {} nodes + // get exists () nodes if (h.isExist()) { getExistNodes(getFilter().getExp(), h, h.getExistNodeList()); } break; - case NODE: + case NODE, ACCEPT: //use case: join() check connection, need all variables - case ACCEPT: h.add(getNode()); break; - case EDGE: - case PATH: - Edge pathEdge = getEdge(); - h.add(pathEdge.getNode(0)); - if (pathEdge.getEdgeVariable() != null) { - h.add(pathEdge.getEdgeVariable()); - } - h.add(pathEdge.getNode(1)); - - for (int i = 2; i < pathEdge.nbNode(); i++) { - h.add(pathEdge.getNode(i)); - } + case EDGE, PATH: + collectEdgeNodes(h); break; - case XPATH: - case EVAL: + case XPATH, EVAL: for (int i = 0; i < nbNode(); i++) { Node nodeEval = getNode(i); h.add(nodeEval); @@ -1055,12 +1048,7 @@ void getNodes(ExpNodeCollector h) { break; case OPTIONAL: - boolean b = h.isOptional(); - first().getNodes(h.setOptional(true)); - if (!h.isInSubScope()) { - rest().getNodes(h); - } - h.setOptional(b); + collectOptionalNodes(h); break; case GRAPH: @@ -1071,32 +1059,11 @@ void getNodes(ExpNodeCollector h) { break; case UNION: - if (h.isInSubScope()) { - // in-subscope record nodes that are bound in both branches of union - List left = first().getTheNodes(h.copy()); - List right = rest().getTheNodes(h.copy()); - for (Node nodeLeft : left) { - if (right.contains(nodeLeft)) { - h.add(nodeLeft); - } - } - } else { - for (Exp ee : this) { - ee.getNodes(h); - } - } + collectUnionNodes(h); break; case BIND: - if (h.isBind()) { - if (getNodeList() == null) { - h.add(getNode()); - } else { - for (Node nodeBind : getNodeList()) { - h.add(nodeBind); - } - } - } + collectBindNodes(h); break; case QUERY: @@ -1104,14 +1071,66 @@ void getNodes(ExpNodeCollector h) { break; default: - // BGP, service, union, named graph pattern - for (Exp ee : this) { - ee.getNodes(h); - if (h.isInSubScopeSample() && ee.isSkipStatement()) { - // skip statements after optional/minus/union/.. for in-subscope nodes - break; - } + collectDefaultNodes(h); + break; + } + } + + private void collectEdgeNodes(ExpNodeCollector h) { + Edge pathEdge = getEdge(); + h.add(pathEdge.getNode(0)); + if (pathEdge.getEdgeVariable() != null) { + h.add(pathEdge.getEdgeVariable()); + } + h.add(pathEdge.getNode(1)); + for (int i = 2; i < pathEdge.nbNode(); i++) { + h.add(pathEdge.getNode(i)); + } + } + + private void collectOptionalNodes(ExpNodeCollector h) { + boolean b = h.isOptional(); + first().getNodes(h.setOptional(true)); + if (!h.isInSubScope()) { + rest().getNodes(h); + } + h.setOptional(b); + } + + private void collectUnionNodes(ExpNodeCollector h) { + if (h.isInSubScope()) { + List left = first().getTheNodes(h.copy()); + List right = rest().getTheNodes(h.copy()); + for (Node nodeLeft : left) { + if (right.contains(nodeLeft)) { + h.add(nodeLeft); } + } + } else { + for (Exp ee : this) { + ee.getNodes(h); + } + } + } + + private void collectBindNodes(ExpNodeCollector h) { + if (h.isBind()) { + if (getNodeList() == null) { + h.add(getNode()); + } else { + for (Node nodeBind : getNodeList()) { + h.add(nodeBind); + } + } + } + } + + private void collectDefaultNodes(ExpNodeCollector h) { + for (Exp ee : this) { + ee.getNodes(h); + if (h.isInSubScopeSample() && ee.isSkipStatement()) { + break; + } } } @@ -1300,26 +1319,28 @@ void setBind() { Exp f = get(i); if (f.isFilter() && f.size() > 0) { Exp bindExp = f.first(); - if (bindExp.type() == Type.OPT_BIND - // no bind (?x = ?y) in case of JOIN - && (!Query.testJoin || bindExp.isBindCst())) { - int j = i - 1; - while (j > 0 && get(j).isFilter()) { - j--; - } - if (j >= 0) { - Exp g = get(j); - if ((g.isEdge() || g.isPath()) - && (!bindExp.isBindCst() || g.bind(bindExp.first().getNode()))) { - bindExp.status(true); - g.setBind(bindExp); - } - } + if (bindExp.type() == Type.OPT_BIND) { + processBind(i, bindExp); } } } } + private void processBind(int index, Exp bindExp) { + int j = index - 1; + while (j > 0 && get(j).isFilter()) { + j--; + } + if (j >= 0) { + Exp g = get(j); + if ((g.isEdge() || g.isPath()) + && (!bindExp.isBindCst() || g.bind(bindExp.first().getNode()))) { + bindExp.status(true); + g.setBind(bindExp); + } + } + } + /** * Edge bind node */ @@ -1333,15 +1354,15 @@ boolean bind(Node node) { } boolean hasCache() { - return cache != null; + return mappingCache != null; } void cache(Node n, Mappings m) { - cache.put(n, m); + mappingCache.put(n, m); } Mappings getMappings(Node n) { - return cache.get(n); + return mappingCache.get(n); } /** @@ -1374,9 +1395,9 @@ public void optional() { } /** - * BGP1 optional { filter(exp) BGP2 } + * BGP1 optional ( filter(exp) BGP2 ) * var(exp) memberOf inscope(BGP1, BGP2) - * TODO: + * Note: * for safety we skip bind because bind may fail * and variable may not be bound whereas we need them to be bound * to test in-scope filter @@ -1436,8 +1457,7 @@ List getNodeVariables() { List list = new ArrayList<>(); for (Exp exp : this) { switch (exp.type()) { - case EDGE: - case PATH: + case EDGE, PATH: exp.getEdgeVariables(list); break; case BIND: @@ -1446,6 +1466,8 @@ List getNodeVariables() { case VALUES: exp.getValuesVariables(list); break; + default: + break; } } return list; @@ -1632,6 +1654,8 @@ void bindNodes() { } } - static class VExp extends ArrayList { + static final class VExp extends ArrayList { + + private static final long serialVersionUID = 1L; } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/IterableEntity.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/IterableEntity.java index a5cfbf062..a8e6f0f43 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/IterableEntity.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/IterableEntity.java @@ -8,41 +8,37 @@ /** * @author Olivier Corby, Wimmics INRIA I3S, 2015 */ -class IterableEntity implements Iterable, Iterator { +final class IterableEntity implements Iterable { - Iterable loop; - Iterator it; + private final Iterable values; - IterableEntity(Iterable loop) { - this.loop = loop; - it = loop.iterator(); + IterableEntity(Iterable loop) { + values = loop; } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { - return this; + Iterator iterator = values.iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Edge next() { + return asEdge(iterator.next()); + } + }; } - @Override - public boolean hasNext() { - return it.hasNext(); - } - - @Override - public Edge next() { - Edge obj = it.next(); - if (obj instanceof Node n) { - return (Edge) n.getNodeObject(); + private static Edge asEdge(Object value) { + if (value instanceof Edge edge) { + return edge; } - - return obj; - - - } - - @Override - public void remove() { + if (value instanceof Node node && node.getNodeObject() instanceof Edge edge) { + return edge; + } + throw new IllegalStateException("Loop item cannot be converted to a KGRAM edge: " + value); } - } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mapping.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mapping.java index 43dfa084e..3a16c4599 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mapping.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mapping.java @@ -4,7 +4,7 @@ import fr.inria.corese.core.next.query.impl.kgram.api.query.Result; import fr.inria.corese.core.next.query.impl.kgram.path.Path; import fr.inria.corese.core.next.query.impl.kgram.tool.EnvironmentImpl; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.*; @@ -21,9 +21,9 @@ * * @author Olivier Corby, Edelweiss, INRIA 2009 */ -public class Mapping +public final class Mapping extends EnvironmentImpl - implements Result, Pointerable> { + implements Result, Pointerable> { static final Edge[] emptyEdge = new Edge[0]; @@ -33,7 +33,7 @@ public class Mapping // var -> Node HashMap values; // aggregate may need to share bnode map - Map bnode; + Map bnode; private Edge[] queryEdges; private Edge[] targetEdges; private Node[] queryNodes; @@ -49,7 +49,6 @@ public class Mapping private Node targetGraphNode; private BindingContext bindingContext; private Eval eval; - private IDatatype report; public Mapping() { init(emptyEdge, emptyEdge); @@ -197,11 +196,11 @@ void setQuery(Query q) { } @Override - public Map getMap() { + public Map getMap() { return bnode; } - void setMap(Map m) { + void setMap(Map m) { bnode = m; } @@ -287,6 +286,7 @@ public List getNodes(String varString, boolean distinct) { } void init() { + // No-op by default in base Mapping } /** @@ -353,7 +353,7 @@ public Mapping project(Node q) { return create(q, value); } - // TODO: manage Node isPath + // Note: manage Node isPath public void fixQueryNodes(Query q) { for (int i = 0; i < getQueryNodes().length; i++) { Node node = getQueryNodes()[i]; @@ -426,7 +426,7 @@ public Set getVariableNames() { return values.keySet(); } - public IDatatype getValue(String name) { + public DatatypeValue getValue(String name) { Node n = getNode(name); if (n == null) { return null; @@ -434,7 +434,7 @@ public IDatatype getValue(String name) { return n.getDatatypeValue(); } - public IDatatype getValue(Node qn) { + public DatatypeValue getValue(Node qn) { Node n = getNode(qn); if (n == null) { return null; @@ -484,8 +484,9 @@ public Object getValue(String varString, int n) { return getValue(varString); } - List getBinding(int n) { - List> l = getList(); + @SuppressWarnings("java:S1168") // Null signals an unbound index in internal evaluation + List getBinding(int n) { + List> l = getList(); if (n < l.size()) { return l.get(n); } @@ -497,17 +498,17 @@ List getBinding(int n) { * */ @Override - public Iterable> getLoop() { + public Iterable> getLoop() { return getList(); } - public List> getList() { - ArrayList> list = new ArrayList<>(); + public List> getList() { + ArrayList> list = new ArrayList<>(); int i = 0; for (Node n : getQueryNodes()) { Node val = getNode(i++); if (val != null) { - ArrayList l = new ArrayList<>(2); + ArrayList l = new ArrayList<>(2); l.add(n.getDatatypeValue()); l.add(val.getDatatypeValue()); list.add(l); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mappings.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mappings.java index 392e20058..730732b83 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mappings.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Mappings.java @@ -7,13 +7,13 @@ import fr.inria.corese.core.next.query.impl.kgram.event.Event; import fr.inria.corese.core.next.query.impl.kgram.event.EventImpl; import fr.inria.corese.core.next.query.impl.kgram.event.KgramEventDispatcher; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.*; import static fr.inria.corese.core.next.query.impl.kgram.api.core.PointerType.MAPPINGS; -/* +/** * Manage list of Mapping, result of a query * * process select distinct @@ -21,15 +21,17 @@ * * @author Olivier Corby, Edelweiss, INRIA 2009 */ -public class Mappings extends PointerObject +public final class Mappings extends PointerObject implements Comparator, Iterable { + private static final String NL = "\n"; private static final int SELECT = -1; private static final int HAVING = -2; // SPARQL: -1 (unbound first) // Corese order: 1 (unbound last) - public static int unbound = -1; + private static final int UNBOUND_ORDER = -1; + @SuppressWarnings("java:S1845") // select field holds projection nodes, distinct from SELECT constant List select; boolean isDistinct = false; // statisfy having(test) @@ -56,27 +58,20 @@ public class Mappings extends PointerObject private Eval eval; // service report if Mappings from service // json object - private IDatatype detail; // construct where result graph private TripleStore graph; - private int nbsolutions = 0; - private int nbDelete = 0; - private int nbInsert = 0; // result of query as a template - private Node templateResult; // fake result in case of aggregate without result private boolean isFake = false; // parse error in service result private boolean error = false; // return Binding stack as part of result to share it - private BindingContext bindingContext; // Federate Service manage provenance private Object provenance; // Linked Result URL List private final List link; // service result log private int length = 0; - private int queryLength = 0; // limit number of results to be displayed private int display = Integer.MAX_VALUE; @@ -107,8 +102,9 @@ void setEventManager(KgramEventDispatcher man) { } @Override - public Iterable getLoop() { - return this; + @SuppressWarnings("unchecked") // Every Mapping is an Object; preserve the historical self view. + public Iterable getLoop() { + return (Iterable) (Iterable) this; } @Override @@ -202,7 +198,6 @@ public void add(Mappings lm) { } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return getMappingList().iterator(); } @@ -255,7 +250,6 @@ public String toString(boolean all, boolean ptr, int max) { StringBuilder sb = new StringBuilder(); int i = 1; boolean isSelect = select != null && !all; - ArrayList alist = new ArrayList<>(); for (Mapping map : this) { if (i > max) { @@ -268,12 +262,12 @@ public String toString(boolean all, boolean ptr, int max) { if (isSelect) { for (Node qNode : select) { - print(map, qNode, sb, alist, ptr); + print(map, qNode, sb, ptr); } } else { for (Node qNode : map.getQueryNodes()) { - print(map, qNode, sb, alist, ptr); + print(map, qNode, sb, ptr); } } @@ -283,7 +277,7 @@ public String toString(boolean all, boolean ptr, int max) { return sb.toString(); } - void print(Mapping map, Node qNode, StringBuilder sb, List list, boolean ptr) { + void print(Mapping map, Node qNode, StringBuilder sb, boolean ptr) { Node node = map.getNode(qNode); if (node != null) { sb.append(qNode).append(" = ").append(node); @@ -318,7 +312,7 @@ public Node getNode(String varString) { return map.getNode(varString); } - public IDatatype getValue(String varString) { + public DatatypeValue getValue(String varString) { Node node = getNode(varString); if (node == null) { return null; @@ -372,7 +366,7 @@ boolean accept(Node node) { return getDistinct() == null || getDistinct().accept(node); } - // TODO: check select == null + // Note: check select == null public boolean accept(Mapping r) { if (select == null || select.isEmpty()) { return true; @@ -487,7 +481,7 @@ int find(Node node, Node qnode) { /** * comparator of Node for standard sort - * use IDatatype compareTo() + * use DatatypeValue compareTo() * compare with sameTerm semantics: order 1 and 01 in deterministic way * authorize overload of comparator for specific datatypes using a Visitor */ @@ -514,10 +508,10 @@ else if (order1[i] == null) { // unbound var if (order2[i] == null) { res = 0; } else { - res = unbound; + res = UNBOUND_ORDER; } } else if (order2[i] == null) { - res = -unbound; + res = -UNBOUND_ORDER; } else { res = 0; } @@ -565,10 +559,10 @@ else if (n1 == null) { // unbound var if (n2 == null) { res = 0; } else { - res = unbound; + res = UNBOUND_ORDER; } } else { - res = -unbound; + res = -UNBOUND_ORDER; } return res; } @@ -596,7 +590,7 @@ public void complete(Eval eval) { void limitOffset() { if (getQuery().getOffset() > 0) { // skip offset - // TODO: optimize this + // Note: optimize this for (int i = 0; i < getQuery().getOffset() && size() > 0; i++) { remove(0); } @@ -620,11 +614,11 @@ void limitOffset() { * */ - public void aggregate(Evaluator evaluator, Memory memory, Producer p) throws SparqlException { + public void aggregate(Evaluator evaluator, Memory memory, Producer p) { aggregate(getQuery(), evaluator, memory, p); } - public void aggregate(Query q, Evaluator evaluator, Memory memory, Producer p) throws SparqlException { + public void aggregate(Query q, Evaluator evaluator, Memory memory, Producer p) { if (size() == 0) { if (q.isAggregate()) { // SPARQL semantics requires that aggregate empty result set return one empty result @@ -657,7 +651,7 @@ public void aggregate(Query q, Evaluator evaluator, Memory memory, Producer p) t * select (aggregate() as ?c) * order by aggregate() */ - void aggregateExpList(Query q, Evaluator evaluator, Memory memory, Producer p, List list, boolean isSelect) throws SparqlException { + void aggregateExpList(Query q, Evaluator evaluator, Memory memory, Producer p, List list, boolean isSelect) { int n = 0; for (Exp exp : list) { if (exp.isAggregate()) { @@ -676,15 +670,15 @@ void aggregateExpList(Query q, Evaluator evaluator, Memory memory, Producer p, L /** * select count(?doc) as ?count group by ?person ?date order by ?count */ - private void aggregateSwitch(Query q, Evaluator eval, Exp exp, Memory mem, Producer p, int n) throws SparqlException { + private void aggregateSwitch(Query q, Evaluator eval, Exp exp, Memory mem, Producer p, int n) { if (exp.isExpGroupBy()) { // min(?l, groupBy(?x, ?y)) as ?min - evalGroupByExp(q, eval, exp, mem, p, n); + evalGroupByExp(eval, exp, mem, p, n); } else if (q.hasGroupBy()) { // perform group by and then aggregate - aggregateGroupMembers(q, getCreateGroup(), eval, exp, mem, p, n); + aggregateGroupMembers(getCreateGroup(), eval, exp, mem, p, n); } else { - aggregate(q, eval, exp, mem, p, n); + aggregate(eval, exp, mem, p, n); } } @@ -692,64 +686,71 @@ private void aggregateSwitch(Query q, Evaluator eval, Exp exp, Memory mem, Produ * Compute select aggregate, order by aggregate and having on one group or on * whole result (in both case: this Mappings) */ - private void aggregate(Query q, Evaluator eval, Exp exp, Memory memory, Producer p, int n) throws SparqlException { + private void aggregate(Evaluator eval, Exp exp, Memory memory, Producer p, int n) { // get first Mapping in current group Mapping firstMap = get(0); // bind the Mapping in memory to retrieve group by variables memory.aggregate(firstMap); - boolean res = true; - Eval ev = memory.getEval(); if (n == HAVING) { - res = exp.getFilter().getExp().test(eval, memory.getBind(), memory, p); - if (ev != null) { - ev.getVisitor().having(ev, exp.getFilter().getExp(), res); - } - if (hasEvent) { - manager.send(EventImpl.create(Event.FILTER, exp, res)); - } - setValid(res); + aggregateHaving(eval, exp, memory, p); } else { - Node aggregateValue; - if (exp.getFilter() == null) { - // use case: order by var - aggregateValue = memory.getNode(exp.getNode()); - } else { - // call fr.inria.corese.core.sparql.triple.function.aggregate.${AggregateFunction} - aggregateValue = eval(exp.getFilter(), eval, memory, p); - if (ev != null) { - ev.getVisitor().aggregate(ev, exp.getFilter().getExp(), - (aggregateValue == null) ? null : aggregateValue.getDatatypeValue()); - } - } + aggregateSelectOrOrder(eval, exp, memory, p, n); + } - if (hasEvent) { - manager.send(EventImpl.create(Event.FILTER, exp, aggregateValue)); - } + memory.pop(firstMap); + } - for (Mapping map : this) { + private void aggregateHaving(Evaluator eval, Exp exp, Memory memory, Producer p) { + boolean res = exp.getFilter().getExp().test(eval, memory.getBind(), memory, p); + Eval ev = memory.getEval(); + if (ev != null) { + ev.getVisitor().having(ev, exp.getFilter().getExp(), res); + } + if (hasEvent) { + manager.send(EventImpl.create(Event.FILTER, exp, res)); + } + setValid(res); + } - if (n == SELECT) { - // select (count(?x) as ?c) - map.setNode(exp.getNode(), aggregateValue); - } else { - // order by count(?x) - map.setOrderBy(n, aggregateValue); - } + private void aggregateSelectOrOrder(Evaluator eval, Exp exp, Memory memory, Producer p, int n) { + Eval ev = memory.getEval(); + Node aggregateValue; + if (exp.getFilter() == null) { + // use case: order by var + aggregateValue = memory.getNode(exp.getNode()); + } else { + // Delegate the aggregate expression to the configured evaluator. + aggregateValue = eval(exp.getFilter(), eval, memory, p); + if (ev != null) { + ev.getVisitor().aggregate(ev, exp.getFilter().getExp(), + (aggregateValue == null) ? null : aggregateValue.getDatatypeValue()); } } - memory.pop(firstMap); + if (hasEvent) { + manager.send(EventImpl.create(Event.FILTER, exp, aggregateValue)); + } + + for (Mapping map : this) { + if (n == SELECT) { + // select (count(?x) as ?c) + map.setNode(exp.getNode(), aggregateValue); + } else { + // order by count(?x) + map.setOrderBy(n, aggregateValue); + } + } } - Node eval(Filter f, Evaluator eval, Environment env, Producer p) throws SparqlException { - return (Node) f.getExp().evalWE(eval, env.getBind(), env, p); + Node eval(Filter f, Evaluator eval, Environment env, Producer p) { + return p.getNode(f.getExp().evalWE(eval, env.getBind(), env, p)); } /** * Process aggregate for each group select, order by, having */ - private void aggregateGroupMembers(Query q, Group group, Evaluator eval, Exp exp, Memory mem, Producer p, int n) throws SparqlException { + private void aggregateGroupMembers(Group group, Evaluator eval, Exp exp, Memory mem, Producer p, int n) { int mappingCount = 0; for (Mappings map : group.getValues()) { if (hasEvent) { @@ -757,21 +758,19 @@ private void aggregateGroupMembers(Query q, Group group, Evaluator eval, Exp exp } map.setCount(mappingCount++); mem.setGroup(map); - map.aggregate(q, eval, exp, mem, p, n); + map.aggregate(eval, exp, mem, p, n); mem.setGroup(null); } } void finish(Query qq) { - setNbsolutions(size()); if (qq.hasGroupBy() && !qq.isConstruct()) { // after group by (and aggregate), leave one Mapping for each group // with result of the group groupBy(); } else if (qq.getHaving() != null) { // clause 'having' with no group by - // select (max(?x) as ?max) where {} - // having(?max > 100) + // e.g. select (max(?x) as ?max) having(?max > 100) having(); } else if (qq.isAggregate() && !qq.isConstruct()) { clean(); @@ -794,7 +793,7 @@ void clean() { } } - public void prepareAggregate(Mapping map, Query q, Map bn, int n) { + public void prepareAggregate(Mapping map, Query q, Map bn, int n) { setCount(n); // in case there is a nested aggregate, map will be an Environment // it must implement aggregate() and hence must know current Mappings group @@ -805,9 +804,9 @@ public void prepareAggregate(Mapping map, Query q, Map bn, in } // min(?l, groupBy(?x, ?y)) as ?min - void evalGroupByExp(Query q, Evaluator eval, Exp exp, Memory mem, Producer p, int n) throws SparqlException { + void evalGroupByExp(Evaluator eval, Exp exp, Memory mem, Producer p, int n) { Group g = createGroup(exp); - aggregateGroupMembers(q, g, eval, exp, mem, p, n); + aggregateGroupMembers(g, eval, exp, mem, p, n); if (exp.isHaving()) { // min(?l, groupBy(?x, ?y), (?l = ?min)) as ?min having(eval, exp, mem, p, g); @@ -838,29 +837,29 @@ void having(Evaluator eval, Exp exp, Memory mem, Producer p, Group g) { /** * Template perform additionnal group_concat(?out) */ - void template(Evaluator eval, Memory mem, Producer p) throws SparqlException { + void template(Evaluator eval, Memory mem, Producer p) { template(eval, getQuery(), mem, p); } - void template(Evaluator eval, Query q, Memory mem, Producer p) throws SparqlException { + void template(Evaluator eval, Query q, Memory mem, Producer p) { if (q.isTemplate() && size() > 0 && !(isFake() && q.isTransformationTemplate())) { // fake in transformation template -> fail // fake in query template -> not fail - setTemplateResult(apply(eval, q.getTemplateGroup(), mem, p)); + apply(eval, q.getTemplateGroup(), mem, p); } } /** * Template perform additionnal group_concat(?out) */ - public Node apply(Evaluator eval, Exp exp, Memory memory, Producer p) throws SparqlException { + public Node apply(Evaluator eval, Exp exp, Memory memory, Producer p) { Mapping firstMap = get(0); // bind the Mapping in memory to retrieve group by variables memory.aggregate(firstMap); if (size() == 1) { Node node = eval(exp.getFilter().getExp().getExp(0).getFilter(), eval, memory, p); - if (node != null && !node.isFuture()) { - // if (node == null) go to aggregate below because we want it to be uniform + if (node != null) { + // When node is null, go to aggregate below because we want it to be uniform // whether there is one or several results return node; } @@ -1079,49 +1078,60 @@ public Mappings joiner(Mappings map2, Node cmn) { Node val = m1.getNodeValue(cmn); if (val == null) { // common unbound in m1 - for (Mapping m2 : map2) { - Mapping m = m1.merge(m2); - if (m != null) { - res.add(m); - } - } + joinCommonUnbound(res, m1, map2); } else { - for (Mapping m2 : map2) { - Node val2 = m2.getNodeValue(cmn); - if (val2 == null) { - // common unbound in m2 - Mapping m = m1.merge(m2); - if (m != null) { - res.add(m); - } - } else { - break; - } - } - - // index of common value in map2 - int index = map2.find(val, cmn); + joinCommonBound(res, m1, map2, cmn, val); + } + } + return res; + } - if (index >= 0 && index < map2.size()) { + private void joinCommonUnbound(Mappings res, Mapping m1, Mappings map2) { + for (Mapping m2 : map2) { + Mapping m = m1.merge(m2); + if (m != null) { + res.add(m); + } + } + } - for (int i = index; i < map2.size(); i++) { + private void joinCommonBound(Mappings res, Mapping m1, Mappings map2, Node cmn, Node val) { + joinPrefixUnbound(res, m1, map2, cmn); + joinMatchingRange(res, m1, map2, cmn, val); + } - // get value of common in map2 - Mapping m2 = map2.get(i); - Node n2 = m2.getNodeValue(cmn); + private void joinPrefixUnbound(Mappings res, Mapping m1, Mappings map2, Node cmn) { + for (Mapping m2 : map2) { + Node val2 = m2.getNodeValue(cmn); + if (val2 == null) { + // common unbound in m2 + Mapping m = m1.merge(m2); + if (m != null) { + res.add(m); + } + } else { + break; + } + } + } - if (n2 == null || !val.match(n2)) { // was equal - break; - } - Mapping m = m1.merge(m2); - if (m != null) { - res.add(m); - } - } + private void joinMatchingRange(Mappings res, Mapping m1, Mappings map2, Node cmn, Node val) { + // index of common value in map2 + int index = map2.find(val, cmn); + if (index >= 0 && index < map2.size()) { + for (int i = index; i < map2.size(); i++) { + // get value of common in map2 + Mapping m2 = map2.get(i); + Node n2 = m2.getNodeValue(cmn); + if (n2 == null || !val.match(n2)) { + break; + } + Mapping m = m1.merge(m2); + if (m != null) { + res.add(m); } } } - return res; } public Mappings minus(Mappings lm) { @@ -1172,16 +1182,14 @@ public void join(Node varNode, Node val) { if (!getSelect().contains(varNode)) { getSelect().add(varNode); } - for (int i = 0; i < size(); ) { - Mapping m = getMappingList().get(i); + Iterator it = getMappingList().iterator(); + while (it.hasNext()) { + Mapping m = it.next(); Node node = m.getNodeValue(varNode); if (node == null) { m.addNode(varNode, val); - i++; - } else if (node.equals(val)) { - i++; - } else { - getMappingList().remove(m); + } else if (!node.equals(val)) { + it.remove(); } } } @@ -1236,13 +1244,6 @@ public void setDelete(List lDelete) { } - void setNbsolutions(int nbsolutions) { - this.nbsolutions = nbsolutions; - } - - private void setTemplateResult(Node templateResult) { - this.templateResult = templateResult; - } @Override public PointerType pointerType() { @@ -1286,9 +1287,6 @@ public void setNodeList(List nodeList) { this.nodeList = nodeList; } - public void setBindingContext(BindingContext ctx) { - bindingContext = ctx; - } public boolean isError() { return error; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Memory.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Memory.java index e91205e34..767844731 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Memory.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Memory.java @@ -1,14 +1,11 @@ package fr.inria.corese.core.next.query.impl.kgram.core; -import fr.inria.corese.core.next.query.impl.kgram.adapter.BindingAdapter; import fr.inria.corese.core.next.query.impl.kgram.api.core.*; import fr.inria.corese.core.next.query.impl.kgram.api.query.*; import fr.inria.corese.core.next.query.impl.kgram.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.kgram.path.Path; import fr.inria.corese.core.next.query.impl.kgram.tool.ApproximateSearchEnv; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.function.term.Binding; -import fr.inria.corese.core.sparql.triple.parser.ASTExtension; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.ArrayList; import java.util.HashMap; @@ -22,15 +19,16 @@ */ public class Memory extends PointerObject implements Environment { + private static final String SERVICE_REPORT_ZERO = "?_service_report_0"; static final Edge[] emptyEdges = new Edge[0]; static final Edge[] emptyEntities = new Edge[0]; - public static boolean IS_EDGE = false; // number of times nodes are bound by Stack // decrease with backtrack - int[] nbNodes, nbEdges, + int[] nbNodes; + int[] nbEdges; // stackIndex[n] = index in Eval Exp stack where nth node is bound first // enable to compute where to backjump - stackIndex; + int[] stackIndex; Edge[] qEdges; Edge[] result; Node[] qNodes; @@ -43,7 +41,7 @@ public class Memory extends PointerObject implements Environment { Object object; // bnode(label) must return same bnode in same solution, different otherwise. // hence must clear bnodes after each solution - Map bnode; + Map bnode; // query or sub query Query query; Node gNode; @@ -58,9 +56,8 @@ public class Memory extends PointerObject implements Environment { int nbEdge = 0; int nbNode = 0; // service evaluation detail report - private IDatatype detail; - private boolean isFake = false; - private boolean isEdge = IS_EDGE; + private DatatypeValue detail; + private boolean isEdge; private BindingContext bindingContext; private ApproximateSearchEnv appxSearchEnv; @@ -233,15 +230,14 @@ Node getNode(String name, List list) { /** * mem is a fresh new Memory, init() has been done Copy this memory into mem - * Use case: exists {} , sub query Can bind all Memory nodes or bind only + * Use case: exists pattern , sub query Can bind all Memory nodes or bind only * subquery select nodes (2 different semantics) - * TODO: let ( .., exists {}), + * Note: let ( .., exists pattern), * MUST push BGP solution and then push Bind */ void copyInto(Query sub, Memory mem, Exp exp) { - int n = 0; if (sub == null) { - // exists {} + // exists pattern copyInto(mem, exp); } else if (eval.getMode() != Evaluator.Mode.SPARQL_MODE) { // bind subquery select nodes @@ -249,19 +245,18 @@ void copyInto(Query sub, Memory mem, Exp exp) { // that are select nodes of sub query // hence sub query memory share only select node bindings // with outer query memory - // use case: ?x :p ?z {select ?z where {?x :q ?z}} + // use case: ?x :p ?z (select ?z where (?x :q ?z)) // ?x in sub query is not the same as ?x in outer query (it is not bound here) // only ?z is the same for (Node subNode : sub.getSelect()) { - copyInto(subNode, mem, n); - n++; + copyInto(subNode, mem); } mem.share(this); } } /** - * exists { } + * exists pattern * PRAGMA: when exists is in function, this memory is empty */ void copyInto(Memory mem, Exp exp) { @@ -287,50 +282,29 @@ void share(Memory source) { share(getBind(), source.getBind()); } - /** - * Copy this Bind local variable stack into this memory - * Use case: function xt:foo(?x) { exists { ?x ex:pp ?y } } - */ - void copy(Binding bind, Exp exp) { - List list = exp.getNodes(); - for (fr.inria.corese.core.kgram.api.core.Expr var : bind.getVariables()) { - Node qn = getNode(var.getLabel(), list); - if (qn != null) { - push(qn, (Node) bind.get(var)); - } - } - } - /** * Copy this BindingContext local variable stack into this memory */ void copy(BindingContext bindCtx, Exp exp) { - if (bindCtx instanceof BindingAdapter) { - Binding binding = ((BindingAdapter) bindCtx).delegate(); - copy(binding, exp); - } else { - List list = exp.getNodes(); - for (Map.Entry entry : bindCtx.getBindings().entrySet()) { - String varLabel = entry.getKey(); - Node qn = getNode(varLabel, list); - if (qn != null) { - push(qn, entry.getValue()); - } + List list = exp.getNodes(); + for (Map.Entry entry : bindCtx.getBindings().entrySet()) { + String varLabel = entry.getKey(); + Node qn = getNode(varLabel, list); + if (qn != null) { + push(qn, entry.getValue()); } } } void copyInto(Memory mem) { - int n = 0; // bind all nodes // use case: inpath copy the memory for (Node qNode : qNodes) { - copyInto(qNode, mem, n); - n++; + copyInto(qNode, mem); } } - void copyInto(Node qNode, Memory mem, int n) { + void copyInto(Node qNode, Memory mem) { if (qNode != null) { Node tNode = getNode(qNode); if (tNode != null) { @@ -339,8 +313,8 @@ void copyInto(Node qNode, Memory mem, int n) { } } - Mapping store(Query q, Producer p, boolean subEval) throws SparqlException { - return store(query, p, subEval, false); + Mapping store(Query q, Producer p, boolean subEval) { + return store(q, p, subEval, false); } /** @@ -348,7 +322,8 @@ Mapping store(Query q, Producer p, boolean subEval) throws SparqlException { * in this case: no select exp, no order by, no group by, etc * subEval = false: main or nested select query. */ - Mapping store(Query q, Producer p, boolean subEval, boolean func) throws SparqlException { + @SuppressWarnings("java:S3776") // Legacy KGRAM solution construction algorithm assembling Mapping results from memory state + Mapping store(Query q, Producer p, boolean subEval, boolean func) { boolean complete = !q.getGlobalQuery().isAlgebra(); Node detailNode = null; @@ -357,7 +332,7 @@ Mapping store(Query q, Producer p, boolean subEval, boolean func) throws SparqlE // use case: xt:sparql() return map with report // PluginImpl sparql() record report in Environment // detailNode is defined by ASTParser with @report metadata - detailNode = getQuery().getSelectNode(Binding.SERVICE_REPORT_ZERO); + detailNode = getQuery().getSelectNode(SERVICE_REPORT_ZERO); if (detailNode != null) { push(detailNode, (Node) getReport()); } @@ -382,7 +357,8 @@ Mapping store(Query q, Producer p, boolean subEval, boolean func) throws SparqlE Node[] snode = new Node[q.getOrderBy().size()]; Node[] gnode = new Node[q.getGroupBy().size()]; - int n = 0, i = 0; + int n = 0; + int i = 0; if (isEdge) { qedge = new Edge[nbEdge]; tedge = new Edge[nbEdge]; @@ -521,15 +497,15 @@ public void clear() { } @Override - public Map getMap() { + public Map getMap() { return bnode; } - void setMap(Map m) { + void setMap(Map m) { bnode = m; } - void orderGroup(List lExp, Node[] nodes, Producer p) throws SparqlException { + void orderGroup(List lExp, Node[] nodes, Producer p) { int n = 0; for (Exp e : lExp) { Node qNode = e.getNode(); @@ -577,51 +553,53 @@ boolean push(Edge q, Edge ent, int n) { } boolean push(Producer p, Edge q, Edge ent, int n) { - boolean success = true; - int max = q.nbNode(); + if (!pushNodes(p, q, ent, n)) { + return false; + } + if (!pushEdgeVariable(q, ent, n)) { + return false; + } + if (isEdge) { + recordEdge(q, ent); + } + return true; + } + private boolean pushNodes(Producer p, Edge q, Edge ent, int n) { + int max = q.nbNode(); for (int i = 0; i < max; i++) { Node node = q.getNode(i); if (node != null) { - if (node.isMatchNodeList()) { - success = pushNodeList(p, node, ent, i); - } else { - success = push(node, ent.getNode(i), n); - } - + boolean success = node.isMatchNodeList() ? pushNodeList(p, node, ent, i) : push(node, ent.getNode(i), n); if (!success) { - // it fail: pop right now pop(q, i); - // stop pushing as ith node failed - break; + return false; } } } + return true; + } - if (success) { - // explicit edge node - // e.g. the node that represents the property/relation - Node pNode = q.getEdgeVariable(); - if (pNode != null) { - success = push(pNode, ent.getEdgeNode(), n); - - if (!success) { - // it fail: pop nodes - pop(q, q.nbNode()); - } + private boolean pushEdgeVariable(Edge q, Edge ent, int n) { + Node pNode = q.getEdgeVariable(); + if (pNode != null) { + boolean success = push(pNode, ent.getEdgeNode(), n); + if (!success) { + pop(q, q.nbNode()); + return false; } } + return true; + } - if (isEdge && success) { - int index = q.getEdgeIndex(); - if (nbEdges[index] == 0) { - nbEdge++; - } - nbEdges[index]++; - qEdges[index] = q; - result[index] = ent; + private void recordEdge(Edge q, Edge ent) { + int index = q.getEdgeIndex(); + if (nbEdges[index] == 0) { + nbEdge++; } - return success; + nbEdges[index]++; + qEdges[index] = q; + result[index] = ent; } void pop(Edge q, int length) { @@ -734,14 +712,14 @@ int getIndex(Node node) { return stackIndex[node.getIndex()]; } - void pop(Edge q, Edge r) { - popNode(q, r); + void pop(Edge q) { + popNode(q); if (isEdge) { - popEdge(q, r); + popEdge(q); } } - void popNode(Edge q, Edge r) { + void popNode(Edge q) { if (q != null) { int max = q.nbNode(); for (int i = 0; i < max; i++) { @@ -761,7 +739,7 @@ void popNode(Edge q, Edge r) { } } - void popEdge(Edge q, Edge r) { + void popEdge(Edge q) { int index = q.getEdgeIndex(); if (nbEdges[index] > 0) { nbEdges[index]--; @@ -787,7 +765,7 @@ boolean push(Mapping res, int n) { */ void aggregate(Mapping map) { push(map, -1); - Map bnodeMap = map.getMap(); + Map bnodeMap = map.getMap(); if (bnodeMap == null) { bnodeMap = new HashMap<>(); map.setMap(bnodeMap); @@ -800,38 +778,40 @@ boolean push(Mapping res, int n, boolean isEdge) { } boolean push(Mapping res, int n, boolean isEdge, boolean isBlank) { + if (!pushMappingNodes(res, n, isBlank)) { + return false; + } + return !isEdge || pushMappingEdges(res, n); + } + + private boolean pushMappingNodes(Mapping res, int n, boolean isBlank) { int k = 0; for (Node qNode : res.getQueryNodes()) { - if (qNode != null && qNode.getIndex() >= 0) { - // use case: skip select fun() as var - // when var has no index - if (!qNode.isBlank() || isBlank) { - // do not push service bnode - Node node = res.getNode(k); - if (!push(qNode, node, n)) { - for (int i = 0; i < k; i++) { - pop(res.getQueryNode(i)); - } - return false; + if (qNode != null && qNode.getIndex() >= 0 && (!qNode.isBlank() || isBlank)) { + Node node = res.getNode(k); + if (!push(qNode, node, n)) { + for (int i = 0; i < k; i++) { + pop(res.getQueryNode(i)); } + return false; } } k++; } + return true; + } - if (isEdge) { - k = 0; - for (Edge qEdge : res.getQueryEdges()) { - Edge edge = res.getEdge(k); - if (!push(qEdge, edge, n)) { - for (int i = 0; i < k; i++) { - pop(res.getQueryEdge(i), res.getEdge(i)); - } - // TODO: pop the nodes - return false; + private boolean pushMappingEdges(Mapping res, int n) { + int k = 0; + for (Edge qEdge : res.getQueryEdges()) { + Edge edge = res.getEdge(k); + if (!push(qEdge, edge, n)) { + for (int i = 0; i < k; i++) { + pop(res.getQueryEdge(i)); } - k++; + return false; } + k++; } return true; } @@ -847,16 +827,7 @@ boolean push(HashMap list, Mapping map, int n) { if (tNode != null) { Node node = map.getNodeProtect(k); if (!push(tNode, node, n)) { - // pop - for (int i = 0; i < k; i++) { - Node qq = map.getQueryNode(i); - if (qq != null) { - Node tt = list.get(qq.getLabel()); - if (tt != null) { - pop(tt); - } - } - } + popPreviousNodes(list, map, k); return false; } } @@ -866,8 +837,19 @@ boolean push(HashMap list, Mapping map, int n) { return true; } + private void popPreviousNodes(HashMap list, Mapping map, int limit) { + for (int i = 0; i < limit; i++) { + Node qq = map.getQueryNode(i); + if (qq != null) { + Node tt = list.get(qq.getLabel()); + if (tt != null) { + pop(tt); + } + } + } + } + void pop(HashMap list, Mapping map) { - int n = 0; for (Node qNode : map.getQueryNodes()) { if (qNode != null) { Node tNode = list.get(qNode.getLabel()); @@ -875,7 +857,6 @@ void pop(HashMap list, Mapping map) { pop(tNode); } } - n++; } } @@ -887,18 +868,15 @@ void pop(Mapping res) { } void pop(Mapping res, boolean isEdge) { - int n = 0; for (Node qNode : res.getQueryNodes()) { if (qNode != null && qNode.getIndex() >= 0) { pop(qNode); } - n++; } if (isEdge) { - n = 0; for (Edge qEdge : res.getQueryEdges()) { - pop(qEdge, res.getEdge(n++)); + pop(qEdge); } } } @@ -1008,7 +986,10 @@ public Node getNode(Expr varExpr) { if (index == ExprType.UNBOUND) { return null; } + break; + default: + break; } return getNode(index); } @@ -1094,14 +1075,6 @@ public void setObject(Object o) { object = o; } - public void setFake(boolean isFake) { - this.isFake = isFake; - } - - @Override - public ASTExtension getExtension() { - return query.getActualExtension(); - } @Override public Node get(Expr varExpr) { @@ -1146,13 +1119,13 @@ public Iterable getLoop() { return new ArrayList<>(0); } - List> getList() { - ArrayList> list = new ArrayList<>(); + List> getList() { + ArrayList> list = new ArrayList<>(); int i = 0; for (Node n : getQueryNodes()) { Node val = getNode(i++); if (n != null && val != null) { - ArrayList l = new ArrayList<>(2); + ArrayList l = new ArrayList<>(2); l.add(n.getDatatypeValue()); l.add(val.getDatatypeValue()); list.add(l); @@ -1183,21 +1156,22 @@ public Object getValue(String varString, int n) { return node.getDatatypeValue(); } - List getBinding(int n) { - List> l = getList(); + @SuppressWarnings("java:S1168") // Null signals an unbound index in internal evaluation + List getBinding(int n) { + List> l = getList(); if (n < l.size()) { return l.get(n); } return null; } - public IDatatype getReport() { + public DatatypeValue getReport() { return detail; } @Override - public void setReport(IDatatype detail) { + public void setReport(DatatypeValue detail) { this.detail = detail; } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/ProcessVisitorDefault.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/ProcessVisitorDefault.java index b6556b8fb..08ae00563 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/ProcessVisitorDefault.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/ProcessVisitorDefault.java @@ -2,79 +2,69 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; import fr.inria.corese.core.next.query.impl.kgram.api.query.ProcessVisitor; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.parser.Metadata; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; -/** - * - * @author Olivier Corby, Wimmics INRIA I3S, 2019 - * - */ -@SuppressWarnings("java:S4144") -public class ProcessVisitorDefault implements ProcessVisitor { - - public static int SLICE_DEFAULT_VALUE = ProcessVisitor.SLICE_DEFAULT; - - int slice = SLICE_DEFAULT_VALUE; - IDatatype defaultValue; +/** No-op process visitor that forwards reporting events to the query binding context. */ +@SuppressWarnings("java:S4144") // Distinct visitor callbacks intentionally share no-op behavior. +public final class ProcessVisitorDefault implements ProcessVisitor { @Override public int slice() { - return slice; + return ProcessVisitor.SLICE_DEFAULT; } @Override - public IDatatype defaultValue() { - return defaultValue; + public DatatypeValue defaultValue() { + return null; } void visit(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { - if (eval.getQuery().getGlobalAST() != null && eval.getQuery().getGlobalAST().hasMetadata(Metadata.Type.REPORT)) { + if (eval.getQuery().isReportEnabled()) { eval.getBind().visit(e, g, m1, m2); } } @Override - public IDatatype graph(Eval eval, Node g, Exp e, Mappings m1) { + public DatatypeValue graph(Eval eval, Node g, Exp e, Mappings m1) { visit(eval, g, e, m1, null); return defaultValue(); } @Override - public IDatatype query(Eval eval, Node g, Exp e, Mappings m1) { + public DatatypeValue query(Eval eval, Node g, Exp e, Mappings m1) { visit(eval, g, e, m1, null); return defaultValue(); } @Override - public IDatatype service(Eval eval, Node g, Exp e, Mappings m1) { + public DatatypeValue service(Eval eval, Node g, Exp e, Mappings m1) { visit(eval, g, e, m1, null); return defaultValue(); } @Override - public IDatatype optional(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + public DatatypeValue optional(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { visit(eval, g, e, m1, m2); return defaultValue(); } @Override - public IDatatype minus(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + public DatatypeValue minus(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { visit(eval, g, e, m1, m2); return defaultValue(); } @Override - public IDatatype union(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + public DatatypeValue union(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { visit(eval, g, e, m1, m2); return defaultValue(); } @Override - public IDatatype join(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { + public DatatypeValue join(Eval eval, Node g, Exp e, Mappings m1, Mappings m2) { visit(eval, g, e, m1, m2); return defaultValue(); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Query.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Query.java index 9e5fa114a..f4e3f3150 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Query.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/Query.java @@ -2,12 +2,11 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.*; import fr.inria.corese.core.next.query.impl.kgram.api.core.Filter; -import fr.inria.corese.core.next.query.impl.kgram.api.query.DistributedQueryPlanFactory; import fr.inria.corese.core.next.query.impl.kgram.api.query.Matcher; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; import fr.inria.corese.core.next.query.impl.kgram.filter.Compile; import fr.inria.corese.core.next.query.impl.kgram.tool.Message; -import fr.inria.corese.core.sparql.triple.parser.*; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,7 +21,8 @@ * @author Olivier Corby, Edelweiss, INRIA 2009 * */ -public class Query extends Exp { +@SuppressWarnings({"java:S1845", "java:S1700", "java:S2387"}) // Legacy KGRAM AST structure +public final class Query extends Exp { public static final int QP_T0 = 0; //No QP settings @@ -30,9 +30,6 @@ public class Query extends Exp { public static final int QP_HEURISTICS_BASED = 2;//Heuristics based QP public static final int QP_BGP = 3;//BGP based QP - //used to set the default query plan method - public static int STD_PLAN = QP_DEFAULT; - public static final int STD_PROFILE = -1; public static final int COUNT_PROFILE = 1; @@ -40,28 +37,18 @@ public class Query extends Exp { public static final String PATHNODE = "pathNode"; - public static boolean test = true; - public static boolean testJoin = false; - public static boolean isOptional = true; - - private static DistributedQueryPlanFactory factory; - - /** - * @return the factory - */ - public static DistributedQueryPlanFactory getFactory() { - return factory; - } - - int limit = Integer.MAX_VALUE, offset = 0, - // if slice > 0 : service gets mappings from previous pattern by slices - slice = 20; + int limit = Integer.MAX_VALUE; + int offset = 0; + // if slice > 0 : service gets mappings from previous pattern by slices + int slice = 20; private int number = 0; boolean distinct = false; - int iNode = 0, iEdge = 0; - private int edgeIndex = -1; - List from, named, selectNode; + int iNode = 0; + int iEdge = 0; + List from; + List named; + List selectNode; // all nodes (on demand) // std pattern but minus/exists (without select) @@ -76,30 +63,29 @@ public static DistributedQueryPlanFactory getFactory() { List bindingNodes; private List constructNodes; List relaxEdges; - List selectExp, - //selectWithExp, - orderBy, groupBy; - List failure, pathFilter, funList; - - List errors, info; - Exp having, construct, delete; + List selectExp; + List orderBy; + List groupBy; + List failure; + List pathFilter; + List funList; + + List errors; + List info; + Exp having; + Exp construct; + Exp delete; // gNode is a local graph node when subquery has no ?g in its select - // use case: graph ?g {{select where {}}} - Node gNode, pathNode; + // use case: graph ?g (select where ...) + Node gNode; + Node pathNode; // outer main query that contains this (when subquery) - private Node provenance; // SPIN graph - private Object graph; - Query query, outerQuery; - private ArrayList subQueryList; - ASTQuery ast; + Query query; + Query outerQuery; + QueryAst ast; Object object; - // Transformation profile template - private Query templateProfile; - //private Object templateVisitor; - // st:set/st:get Context - private Context context; // current transformer if any private Object transformer; // table: transformation -> Transformer @@ -120,11 +106,9 @@ public static DistributedQueryPlanFactory getFactory() { HashMap table; // Extended queries for additional group by List queries; - // implemented by ASTExtension - private ASTExtension extension; - private boolean isCompiled = false; private boolean datasetSpecified; + private boolean reportEnabled; boolean isCheck = false; private boolean isUseBind = true; @@ -148,15 +132,11 @@ public static DistributedQueryPlanFactory getFactory() { private boolean parallel = true; private boolean validate = false; private boolean federate = false; - private boolean serviceResult = false; - private boolean isFun = false; - private boolean isPathType = false; // store the list of edges of the path private boolean isStorePath = true; // cache PP result in PathFinder private boolean isCachePath = false; boolean isCountPath = false; - private boolean importFailure = false; boolean isCorrect = true; @@ -165,13 +145,9 @@ public static DistributedQueryPlanFactory getFactory() { boolean isRule = false; boolean isDetail = true; private boolean algebra = false; - private boolean isMatch = false; - private boolean initMode = false; - private int id = -1; - private int priority = 100; int mode = Matcher.UNDEF; - int planner = STD_PLAN; + int planner = QP_DEFAULT; private int queryProfile = STD_PROFILE; private boolean isService = false; @@ -183,24 +159,15 @@ public static DistributedQueryPlanFactory getFactory() { private boolean isTransformationTemplate = false; // member of a set of templates of a pprinter (not a single query that is a template) - private boolean isPrinterTemplate = false; - private Exp templateGroup, templateNL; + private Exp templateGroup; private final List argList; private Mapping mapping; - private List edgeList; - private String name; - private String uri; - private String profile; - private boolean isNumbering; private boolean isExtension = false; private BgpGenerator bgpGenerator; - private List queryEdgeList; - private Mappings selection; - private Mappings discorevy; private String service; @@ -232,12 +199,8 @@ public Query(){ bindingNodes = new ArrayList<>(); relaxEdges = new ArrayList<>(); argList = new ArrayList<>(); - queryEdgeList = new ArrayList<>(); querySorter = new QuerySorter(this); - if (getFactory() != null){ - setBgpGenerator(getFactory().instance()); - } } Query(Exp e) { @@ -249,6 +212,7 @@ public static Query create(Exp e) { return new Query(e); } + @SuppressWarnings("java:S1172") // Parameter preserved for legacy factory compatibility public static Query create(int type) { return new Query(); } @@ -318,15 +282,15 @@ public void setObject(Object o) { object = o; } - public ASTQuery getGlobalAST() { + public QueryAst getGlobalAST() { return getGlobalQuery().getAST(); } - public ASTQuery getAST() { + public QueryAst getAST() { return ast; } - public void setAST(ASTQuery o) { + public void setAST(QueryAst o) { ast = o; } @@ -548,7 +512,7 @@ public List selectNodesFromPattern() { /** - * select var node list, including (exp as var) + * select variable node list, including (exp as variable) * computed by compiler transformer */ public List getSelect() { @@ -560,7 +524,7 @@ public void setSelect(List list) { } /** - * select var node list as Exp(node, exp) where exp may be null + * select variable node list as Exp(node, exp) where exp may be null * computed by compiler transformer */ public List getSelectFun() { @@ -649,10 +613,12 @@ public boolean isCheck() { return isCheck; } + @Override public boolean isAggregate() { return isAggregate; } + @Override public void setAggregate(boolean b) { isAggregate = b; } @@ -736,10 +702,8 @@ public void setOptimize(boolean b) { public void setAggregate() { for (Exp exp : getSelectFun()) { - if (exp.getFilter() != null) { - if (exp.isAggregate() && !exp.isExpGroupBy()) { - setAggregate(true); - } + if (exp.getFilter() != null && exp.isAggregate() && !exp.isExpGroupBy()) { + setAggregate(true); } } for (Exp exp : getOrderBy()) { @@ -771,7 +735,7 @@ public void setOrderBy(List s) { } public boolean isOrderBy() { - return orderBy.size() > 0; + return !orderBy.isEmpty(); } public List getOrderBy() { @@ -787,7 +751,7 @@ public List getGroupBy() { } public boolean isGroupBy() { - return groupBy.size() > 0; + return !groupBy.isEmpty(); } public boolean hasGroupBy() { @@ -854,13 +818,14 @@ public Exp getDelete() { return delete; } + @Override boolean member(Node node, List lExp) { return member(node.getLabel(), lExp); } - boolean member(String var, List lExp) { + boolean member(String variable, List lExp) { for (Exp exp : lExp) { - if (var.equals(exp.getNode().getLabel())) { + if (variable.equals(exp.getNode().getLabel())) { return true; } } @@ -900,9 +865,7 @@ public int nbEdges() { */ public void complete(Producer prod) { synchronized (this) { - if (isCompiled()) { - return; - } else { + if (!isCompiled()) { basicComplete(prod); setCompiled(); } @@ -910,7 +873,7 @@ public void complete(Producer prod) { } void basicComplete(Producer prod) { - // sort edges according to var connexity, assign filters + // sort edges according to variable connexity, assign filters // recurse on subquery querySorter.compile(prod); setAggregate(); @@ -973,8 +936,8 @@ void complete2() { void index(List list) { for (Exp ee : list) { - // use case: group by (exists{?x :p ?y} as ?b) - // use case: order by exists{?x :p ?y} + // use case: group by (exists(?x :p ?y) as ?b) + // use case: order by exists(?x :p ?y) if (ee.getFilter() != null) { index(this, ee.getFilter()); } @@ -1094,13 +1057,14 @@ public void collect() { } } - // exist: inside exists { exp } - // or inside A minus { exp } + // exist: inside exists ( exp ) + // or inside A minus ( exp ) + @SuppressWarnings("java:S3776") void collect(Exp exp, boolean exist) { switch (exp.type()) { case FILTER: - // get exists {} nodes + // get exists () nodes // draft collectExist(exp.getFilter().getExp()); break; @@ -1115,8 +1079,7 @@ void collect(Exp exp, boolean exist) { } break; - case EDGE: - case PATH: + case EDGE, PATH: Edge edge = exp.getEdge(); store(edge.getNode(0), exist, false); if (edge.getEdgeVariable() != null) { @@ -1128,8 +1091,7 @@ void collect(Exp exp, boolean exist) { } break; - case XPATH: - case EVAL: + case XPATH, EVAL: for (int i = 0; i < exp.nbNode(); i++) { Node node = exp.getNode(i); store(node, exist, false); @@ -1190,103 +1152,84 @@ void collectExist(Expr exp) { * query is (sub)query this is global query */ int index(Query query, Exp exp, boolean isExist, int start) { - int min = Integer.MAX_VALUE, n; - Type type = exp.type(); - - switch (type) { - case EDGE: - case PATH: - case XPATH: - case EVAL: - Edge edge = exp.getEdge(); - edge.setEdgeIndex(iEdge++); - min = indexExpEdge(query, exp); - - if (exp.hasPath()) { - // x rdf:type t - // x rdf:type/rdfs:subClassOf* t - Exp ep = exp.getPath(); - ep.getEdge().setEdgeIndex(edge.getEdgeIndex()); - indexExpEdge(query, ep); - } - break; - - case VALUES: - for (Node node : exp.getNodeList()) { - n = qIndex(query, node); - min = Math.min(min, n); - } - break; - - case NODE: - Node node = exp.getNode(); - min = qIndex(query, node); - break; + int min = switch (exp.type()) { + case EDGE, PATH, XPATH, EVAL -> indexEdgeExp(query, exp); + case VALUES -> indexValuesExp(query, exp); + case NODE -> qIndex(query, exp.getNode()); + case BIND -> indexBindExp(query, exp, isExist); + case FILTER -> indexExpFilter(query, exp, isExist); + case QUERY -> indexExpQuery(query, exp, isExist); + case OPT_BIND, ACCEPT -> Integer.MAX_VALUE; + default -> indexDefaultExp(query, exp, isExist, start); + }; - case BIND: - Node qn = exp.getNode(); - min = qIndex(query, qn); - if (exp.getNodeList() != null){ - // values () {unnest(expr)} - for (Node bn : exp.getNodeList()){ - int ii = qIndex(query, bn); - min = Math.min(min, ii); - } - } - // continue on filter below: - - case FILTER: - min = indexExpFilter(query, exp, isExist); - break; + if (exp.getGraphNode() != null) { + index(exp.getGraphNode()); + } - case QUERY: - min = indexExpQuery(query, exp, isExist); - break; + return min; + } - case OPT_BIND: - case ACCEPT: - break; + private int indexEdgeExp(Query query, Exp exp) { + Edge edge = exp.getEdge(); + edge.setEdgeIndex(iEdge++); + int min = indexExpEdge(query, exp); + if (exp.hasPath()) { + Exp ep = exp.getPath(); + ep.getEdge().setEdgeIndex(edge.getEdgeIndex()); + indexExpEdge(query, ep); + } + return min; + } - default: - // AND UNION OPTION GRAPH BIND - int startIndex = globalNodeIndex(), - ind = -1; - if (start >= 0) { - startIndex = start; - } - if (exp.isUnion()) { - ind = startIndex; - } - for (Exp e : exp) { - n = index(query, e, isExist, ind); - min = Math.min(min, n); - } + private int indexValuesExp(Query query, Exp exp) { + int min = Integer.MAX_VALUE; + for (Node node : exp.getNodeList()) { + int n = qIndex(query, node); + min = Math.min(min, n); } + return min; + } - // index the fake graph node (select/minus) - if (exp.getGraphNode() != null) { - index(exp.getGraphNode()); + private int indexBindExp(Query query, Exp exp, boolean isExist) { + Node qn = exp.getNode(); + int min = qIndex(query, qn); + if (exp.getNodeList() != null) { + for (Node bn : exp.getNodeList()) { + int ii = qIndex(query, bn); + min = Math.min(min, ii); + } } + return indexExpFilter(query, exp, isExist); + } + private int indexDefaultExp(Query query, Exp exp, boolean isExist, int start) { + int min = Integer.MAX_VALUE; + int startIndex = (start >= 0) ? start : globalNodeIndex(); + int ind = exp.isUnion() ? startIndex : -1; + for (Exp e : exp) { + int n = index(query, e, isExist, ind); + min = Math.min(min, n); + } return min; } - // use case: index filter exists {?x ?p ?y} + // use case: index filter exists (?x ?p ?y) int indexExpFilter(Query query, Exp exp, boolean isExist) { int min = Integer.MAX_VALUE; boolean hasExist = index(query, exp.getFilter()); List lVar = exp.getFilter().getVariables(true); - for (String var : lVar) { - Node qNode = query.getProperAndSubSelectNode(var); + for (String variable : lVar) { + Node qNode = query.getProperAndSubSelectNode(variable); if (qNode == null) { - // TODO: does not work with filter in exists {} - // because getProperAndSubSelectNode does not go into exists {} - if (!isTriple(exp, var)) { + // Note: does not work with filter in exists pattern + // because getProperAndSubSelectNode does not go into exists pattern + if (!isTriple(exp, variable)) { // no error message for use case: - // var = ?_bn = <> - logger.warn(Message.Prefix.UNDEF_VAR.getString(), var); - addError(Message.Prefix.UNDEF_VAR.getString(), var); + // variable = ?_bn = <> + logger.warn(Message.Prefix.UNDEF_VAR.getString(), variable); + addError(Message.Prefix.UNDEF_VAR.getString(), variable); } } else if (!isExist && !hasExist) { int n = qIndex(query, qNode); @@ -1295,7 +1238,7 @@ int indexExpFilter(Query query, Exp exp, boolean isExist) { } if (hasExist) { // use case: - // exists {?x p ?y filter(?x != ?z)} + // exists (?x p ?y filter(?x != ?z)) min = -1; } return min; @@ -1340,15 +1283,11 @@ int indexExpEdge(Query query, Exp exp) { } + @SuppressWarnings("java:S3400") // Placeholder method for future RDF-star variable annotations boolean isTriple(Exp exp, String name) { - List varList = exp.getFilterExpression().getVariables(VariableScope.filterscopeNotLocal()); - for (Variable var : varList) { - if (var.getName().equals(name)) { - if (var.isTripleWithTriple()) { - return true; - } - } - } + // RDF-star variable annotations belonged to the legacy parser expression. + // The native AST represents triple terms explicitly, so a plain variable + // reference cannot carry that hidden flag. return false; } @@ -1357,6 +1296,7 @@ boolean isTriple(Exp exp, String name) { * If node is in a sub query, return the * index of the outer node corresponding to node and rec. */ + @SuppressWarnings("java:S1172") // Query parameter preserved for outer query scoping int qIndex(Query query, Node node) { return index(node); } @@ -1409,7 +1349,7 @@ public List getArgList() { /** * Compute node list for filter variables use case: Pattern compiler (?x = - * cst) TODO: does not dive into minus {PAT} + * cst) Note: does not dive into minus (PAT) */ public List getNodes(Exp exp) { return getNodes(exp.getFilter()); @@ -1418,8 +1358,8 @@ public List getNodes(Exp exp) { public List getNodes(Filter f) { List lVar = f.getVariables(); ArrayList lNode = new ArrayList<>(); - for (String var : lVar) { - Node node = getProperAndSubSelectNode(var); + for (String variable : lVar) { + Node node = getProperAndSubSelectNode(variable); if (node != null && !lNode.contains(node)) { lNode.add(node); } @@ -1578,10 +1518,12 @@ public boolean isTemplate() { return isTemplate; } + @Override public int getNumber() { return number; } + @Override public void setNumber(int number) { this.number = number; } @@ -1614,42 +1556,10 @@ public boolean isExtension() { return isExtension; } - public ASTExtension getExtension() { - return extension; - } - - public ASTExtension getActualExtension(){ - return getGlobalQuery().getExtension(); - } - - - public void setExtension(ASTExtension ext) { - this.extension = ext; - } - - - public Expr getLocalExpression(String name){ - if (getExtension() != null){ - Expr exp = (Expr) getExtension().get(name); - if (exp != null){ - return exp.getFunction(); - } - } - return null; - } - - // subquery inherit from global query - public Expr getGlobalExpression(String name) { - if (getGlobalQuery() != this) { - return getGlobalQuery().getLocalExpression(name); - } - return null; - } - @Override - public Iterable getLoop() { - return getEdges(); + public Iterable getLoop() { + return () -> getEdges().stream().map(Object.class::cast).iterator(); } public List getEdges(){ @@ -1678,21 +1588,12 @@ public void setBgpGenerator(BgpGenerator bgpGenerator) { } - public Context getContext() { - if (query == null){ - return context; - } - return query.getContext(); + public boolean isReportEnabled() { + return getGlobalQuery().reportEnabled; } - - public void setContext(Context context) { - if (query == null){ - this.context = context; - } - else { - query.setContext(context); - } + public void setReportEnabled(boolean reportEnabled) { + getGlobalQuery().reportEnabled = reportEnabled; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/EventImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/EventImpl.java index 0c1ba11f7..8d9aa6b1b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/EventImpl.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/EventImpl.java @@ -4,9 +4,6 @@ import fr.inria.corese.core.next.query.impl.kgram.core.Exp; import fr.inria.corese.core.next.query.impl.kgram.core.Stack; -import java.util.Hashtable; - - /** * Event to trace KGRAM execution * @author Olivier Corby, Edelweiss, INRIA 2010 @@ -15,11 +12,11 @@ public class EventImpl implements Event { - static Hashtable titles; - int type; boolean isSuccess = true; - Object object, arg, arg2; + Object object; + Object arg; + Object arg2; EventImpl(int n, Object o){ type = n; @@ -30,8 +27,8 @@ public class EventImpl implements Event { type = n; object = o; arg = o2; - if (o2 instanceof Boolean){ - isSuccess = (Boolean) o2; + if (o2 instanceof Boolean success){ + isSuccess = success; } } @@ -40,8 +37,8 @@ public class EventImpl implements Event { object = o; arg = o2; arg2 = o3; - if (o3 instanceof Boolean){ - isSuccess = (Boolean) o3; + if (o3 instanceof Boolean success){ + isSuccess = success; } } @@ -49,10 +46,8 @@ public String toString(){ String str = getTitle(); if (object != null){ str += " "; - if (object instanceof Exp exp){ - if (exp.isEdge()){ + if (object instanceof Exp exp && exp.isEdge()){ str += "("+ exp.getEdge().getEdgeIndex() + ") "; - } } str += object; } @@ -66,50 +61,33 @@ public String toString(){ } - static void deftitle(int type, String title){ - titles.put(type, title); - } - String getTitle(){ - if (titles == null){ - init(); - } - return titles.get(type); - } - - static void init(){ - titles = new Hashtable<>(); - deftitle(BEGIN, "begin"); - deftitle(START, "start"); - deftitle(ENUM, "enum"); - deftitle(FILTER, "filter"); - deftitle(BIND, "bind"); - deftitle(MATCH, "match"); - deftitle(GRAPH, "graph"); - deftitle(PATH, "path"); - deftitle(PATHSTEP, "step"); - deftitle(FINISH, "finish"); - - - deftitle(AGG, "aggregate"); - deftitle(DISTINCT, "distinct"); - deftitle(LIMIT, "limit"); - - deftitle(RESULT, "result"); - deftitle(END, "end"); - - // User Event - - deftitle(COMPLETE, "complete"); - deftitle(FORWARD, "forward"); - deftitle(MAP, "map"); - deftitle(NEXT, "next"); - deftitle(QUIT, "quit"); - deftitle(STEP, "step"); - deftitle(SUCCESS, "success"); - - deftitle(VERBOSE, "verbose"); - deftitle(HELP, "help"); + return switch (type) { + case BEGIN -> "begin"; + case START -> "start"; + case ENUM -> "enum"; + case FILTER -> "filter"; + case BIND -> "bind"; + case MATCH -> "match"; + case GRAPH -> "graph"; + case PATH -> "path"; + case PATHSTEP, STEP -> "step"; + case FINISH -> "finish"; + case AGG -> "aggregate"; + case DISTINCT -> "distinct"; + case LIMIT -> "limit"; + case RESULT -> "result"; + case END -> "end"; + case COMPLETE -> "complete"; + case FORWARD -> "forward"; + case MAP -> "map"; + case NEXT -> "next"; + case QUIT -> "quit"; + case SUCCESS -> "success"; + case VERBOSE -> "verbose"; + case HELP -> "help"; + default -> "event(" + type + ')'; + }; } @@ -144,8 +122,8 @@ public Object getArg(int n){ } public Exp getExp(){ - if (object instanceof Exp){ - return (Exp) object; + if (object instanceof Exp expression){ + return expression; } return null; } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/execution/SparqlKgramEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/execution/SparqlKgramEvaluator.java index ec8e30ec1..7858d23cd 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/execution/SparqlKgramEvaluator.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/execution/SparqlKgramEvaluator.java @@ -5,22 +5,27 @@ import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; import fr.inria.corese.core.next.query.impl.kgram.core.Eval; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + /** * KGRAM evaluator for SPARQL query execution. * *

KGRAM requires an {@link Evaluator} even for simple basic graph patterns. - * This implementation currently covers expression-free graph pattern execution: - * edge enumeration is delegated to the producer, and RDF term comparison is - * delegated to the matcher.

- * - *

SPARQL expression features such as FILTER, BIND, and function calls should - * be added here when they enter the supported execution scope, so expression - * evaluation remains part of the same KGRAM runtime path.

+ * This implementation owns the small amount of immutable, query-scoped state + * required by native expression evaluation. Edge enumeration remains delegated + * to the producer and RDF term comparison to the matcher.

*/ public final class SparqlKgramEvaluator implements Evaluator { + private final OffsetDateTime queryEvaluationTime = OffsetDateTime.now(ZoneOffset.UTC); private Mode mode = Mode.KGRAM_MODE; + @Override + public OffsetDateTime getQueryEvaluationTime() { + return queryEvaluationTime; + } + @Override public Mode getMode() { return mode; @@ -53,6 +58,6 @@ public void finish(Environment environment) { @Override public void init(Environment environment) { - // Expression evaluation state will be initialized here when supported. + // All native expression state is immutable and initialized at construction. } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Checker.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Checker.java index c9b424920..cbcff07ea 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Checker.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Checker.java @@ -2,7 +2,6 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.Expr; import fr.inria.corese.core.next.query.impl.kgram.api.core.ExprType; -import fr.inria.corese.core.next.query.impl.kgram.core.Query; import java.util.ArrayList; import java.util.List; @@ -18,30 +17,24 @@ * @author Olivier Corby, Edelweiss, INRIA 2010 * */ -public class Checker implements ExprType { - public static boolean verbose = true; - +public final class Checker implements ExprType { static final int BOOL = BOOLEAN; - static List alwaysFalse; - - - Matcher matcher; - Query query; + private final List alwaysFalse; + private final Matcher matcher; - public Checker(Query q){ + public Checker(){ matcher = new Matcher(); - query = q; + alwaysFalse = createAlwaysFalsePatterns(); } /** * Check one filter */ public boolean check(Expr ee){ - boolean b = match(ee); // match = true means that a false pattern matches // hence return false (check correctness is false) - return ! b; + return !match(ee); } @@ -52,21 +45,17 @@ public boolean check(Expr ee){ boolean match(Expr ee){ - return match(ee, alwaysFalse()); + return match(ee, alwaysFalse); } boolean match(Expr ee, List pat) { - boolean suc = false, b; - for (FilterPattern p : pat){ - b = matcher.match(p, ee); - if (b){ - suc = true; + if (matcher.match(p, ee)){ + return true; } } - - return suc; + return false; } @@ -76,25 +65,20 @@ boolean match(Expr ee, List pat) { * */ - List alwaysFalse(){ - if (alwaysFalse == null){ - List pat = new ArrayList<>(); - pat.add(neqSelf()); - pat.add(ltSelf()); - pat.add(notEqSelf()); - pat.add(notGeSelf()); - - pat.add(patNotPat()); - pat.add(notOr()); - pat.add(eqNeq()); - pat.add(eqGt()); - pat.add(ltGt()); - pat.add(gtNotGe()); - pat.add(eqNotGe()); - - alwaysFalse = pat; - } - return alwaysFalse; + List createAlwaysFalsePatterns(){ + List patterns = new ArrayList<>(); + patterns.add(neqSelf()); + patterns.add(ltSelf()); + patterns.add(notEqSelf()); + patterns.add(notGeSelf()); + patterns.add(patNotPat()); + patterns.add(notOr()); + patterns.add(eqNeq()); + patterns.add(eqGt()); + patterns.add(ltGt()); + patterns.add(gtNotGe()); + patterns.add(eqNotGe()); + return List.copyOf(patterns); } @@ -152,7 +136,7 @@ FilterPattern notOr(){ } - // TODO: + // Note: // we can have both with list of values // ?x = xpath() && ?x != xpath() FilterPattern eqNeq(){ diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Compile.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Compile.java index bbb8a579c..830ef5b7f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Compile.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Compile.java @@ -34,27 +34,27 @@ */ public class Compile implements ExprType { - Query query; + Query kgramQuery; Matcher matcher; Checker checker; public Compile(Query q){ - query = q; + kgramQuery = q; matcher = new Matcher(); - checker = new Checker(query); + checker = new Checker(); } /** * exp is a FILTER Exp * when !bound(?x) : get Node ?x for optimizing backjump with optional * when ?x = ?y : get list Node, for optimizing edge - * TODO: + * Note: * assign filter to edge and put edge first in its component: * x p t . y p z filter(y = x) * we can bind y = x before enumerate y p z */ - @SuppressWarnings("unused") public void process(Query q, Exp exp) { + this.kgramQuery = q; Filter ff = exp.getFilter(); Expr ee = ff.getExp(); @@ -67,7 +67,7 @@ public void process(Query q, Exp exp) { case EQ: eq(exp); break; - //todo: IN, VALUES + // Note: IN, VALUES case IN: in(exp); break; @@ -94,12 +94,12 @@ void eq(Exp exp){ FilterPattern pat = new FilterPattern(TERM, EQ, VARIABLE, VARIABLE); if (matcher.match(pat, ee)){ // compute Node list corresponding to variables - List lNode = query.getNodes(exp); + List lNode = kgramQuery.getNodes(exp); if (lNode.size()==2){ Exp bind = Exp.create(ExpType.Type.OPT_BIND); for (Node qNode : lNode){ - Exp var = Exp.create(ExpType.Type.NODE, qNode); - bind.add(var); + Exp variable = Exp.create(ExpType.Type.NODE, qNode); + bind.add(variable); } exp.add(bind); } @@ -118,7 +118,7 @@ else if (matcher.match(new FilterPattern(TERM, EQ, VARIABLE, CONSTANT), ee)){ Exp buildCst(Exp exp, ExpType.Type type){ Filter ff = exp.getFilter(); Expr ee = ff.getExp(); - Node node = query.getProperAndSubSelectNode(ff.getVariables().getFirst()); + Node node = kgramQuery.getProperAndSubSelectNode(ff.getVariables().getFirst()); if (node != null){ // variable ?x Exp bind = Exp.create(type, Exp.create(ExpType.Type.NODE, node)); @@ -131,7 +131,7 @@ Exp buildCst(Exp exp, ExpType.Type type){ } Exp buildVar(Exp exp){ - List lNode = query.getNodes(exp); + List lNode = kgramQuery.getNodes(exp); if (lNode.size()==2){ Exp bind = Exp.create(ExpType.Type.TEST); for (Node node : lNode){ @@ -177,7 +177,7 @@ void or(Exp exp){ pat.setRec(); pat.setMatchConstant(); if (matcher.match(pat, ee)) { - Node node = query.getProperAndSubSelectNode(ff.getVariables().getFirst()); + Node node = kgramQuery.getProperAndSubSelectNode(ff.getVariables().getFirst()); if (node != null){ List list = getConstants(ee); Exp bind = Exp.create(ExpType.Type.OPT_BIND, Exp.create(ExpType.Type.NODE, node)); @@ -196,8 +196,8 @@ void in(Exp exp) { Filter ff = exp.getFilter(); Expr expr = ff.getExp(); List lvar = ff.getVariables(); - Expr var = expr.getExp(0); - if (! var.isVariable()){ + Expr variable = expr.getExp(0); + if (! variable.isVariable()){ return; } List values = expr.getExp(1).getExpList(); @@ -210,7 +210,7 @@ void in(Exp exp) { list.add(e); } - Node node = query.getProperAndSubSelectNode(lvar.getFirst()); + Node node = kgramQuery.getProperAndSubSelectNode(lvar.getFirst()); if (node != null) { Exp bind = Exp.create(ExpType.Type.OPT_BIND, Exp.create(ExpType.Type.NODE, node)); bind.setObject(list); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Extension.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Extension.java index 343757ec2..76ddbaa4b 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Extension.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/Extension.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.kgram.filter; import fr.inria.corese.core.next.query.impl.kgram.api.core.Expr; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; /** * Manage extension functions @@ -25,7 +25,7 @@ public interface Extension { Expr getMetadata(String metadata, int n); - Expr getMethod(String label, IDatatype type, IDatatype[] param); + Expr getMethod(String label, DatatypeValue type, DatatypeValue[] param); void removeNamespace(String name); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/FilterPattern.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/FilterPattern.java index 383eaae0e..267c0413a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/FilterPattern.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/FilterPattern.java @@ -5,7 +5,7 @@ import fr.inria.corese.core.next.query.impl.kgram.api.query.Evaluator; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; import fr.inria.corese.core.next.query.impl.kgram.core.Exp; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.ArrayList; import java.util.List; @@ -17,8 +17,9 @@ * * @author Olivier Corby, Edelweiss, INRIA 2010 */ -public class FilterPattern implements ExprType, Expr { - int type, oper; +public final class FilterPattern implements ExprType, Expr { + int type; + int oper; String label; // recursive pattern, ako * boolean rec = false; @@ -130,6 +131,7 @@ public int oper() { } public void setIndex(int index) { + // FilterPattern indices are immutable } public int type() { @@ -167,6 +169,7 @@ public Expr getArg() { @Override public void setArg(Expr exp) { + // FilterPattern arguments are immutable } @Override @@ -191,10 +194,12 @@ public boolean isFuncall() { @Override public void setOper(int n) { + // FilterPattern operator is immutable } @Override public void setExp(int i, Expr e) { + // FilterPattern sub-expressions are immutable } @Override @@ -288,7 +293,7 @@ public boolean isDynamic() { } @Override - public IDatatype evalWE(Evaluator eval, BindingContext b, Environment env, Producer p) { + public DatatypeValue evalWE(Evaluator eval, BindingContext b, Environment env, Producer p) { throw new UnsupportedOperationException("Not supported yet."); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/PathFinder.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/PathFinder.java index 1cf526149..e7cab410a 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/PathFinder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/PathFinder.java @@ -8,8 +8,8 @@ import fr.inria.corese.core.next.query.impl.kgram.api.query.Evaluator; import fr.inria.corese.core.next.query.impl.kgram.api.query.Matcher; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; +import fr.inria.corese.core.next.query.api.exception.QueryException; import fr.inria.corese.core.next.query.impl.kgram.core.*; -import fr.inria.corese.core.next.query.impl.kgram.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.kgram.event.ResultListener; import fr.inria.corese.core.next.query.impl.kgram.tool.EdgeInv; import org.slf4j.Logger; @@ -32,14 +32,14 @@ * Path Variable: ?x exp :: $path ?y * Path Length: pathLength($path) * Path Enumeration: ?x exp :: $path ?y graph $path {?a ?p ?b} - * Shortest path: ?x distinct short exp ?y ; ?x short exp ?y -- TODO: complete + * Shortest path: ?x distinct short exp ?y ; ?x short exp ?y -- Note: complete * short to get only shortest * Path weight: ?x (rdf:first@2 / rdf:rest@1* / ^rdf:first@2) * ?y * Constaint: ?x exp * @ {?this a foaf:Person} ?y ?x exp * @ [a foaf:Person] ?y * Parallel Path: ?x (foaf:knows || ^rdfs:seeAlso) + ?y - * Check Loop: pf.setCheckLoop(true) => exp+ exp{n,m} without loop + * Check Loop: pf.setCheckLoop(true) => exp+ exp[n,m] without loop * exec.setPathLoop(false) pragma {kg:path kg:loop false} * * @author Olivier Corby, Edelweiss, INRIA 2010 @@ -50,7 +50,6 @@ public class PathFinder { private static final Logger logger = LoggerFactory.getLogger(PathFinder.class); - public static long cedge = 0, cresult = 0, ctest = 0; // synchronized buffer between this and projection private PathMappingBuffer mbuffer; @@ -66,7 +65,10 @@ public class PathFinder { private Filter filter; private Memory mem; private Edge edge; - private Node gNode, targetNode, regexNode, varNode; + private Node gNode; + private Node targetNode; + private Node regexNode; + private Node varNode; private List from; private Node[] qNodes; @@ -89,9 +91,9 @@ public class PathFinder { private boolean isCache = false; private boolean isStorePath = true; - private final int maxLength = Integer.MAX_VALUE; + private static final int MAX_LENGTH = Integer.MAX_VALUE; private int min = 0; - private int max = maxLength; + private int max = MAX_LENGTH; private Regex regexp; @@ -135,10 +137,6 @@ void setEval(Eval ev) { kgram = ev; } - @SuppressWarnings("unused") - public void set(KgramEventDispatcher man) { - } - public void set(ResultListener rl) { listener = rl; hasListener = rl != null; @@ -175,11 +173,9 @@ public void start(Edge edge, Node node, Memory env, Filter f) { index = n; targetNode = env.getNode(edge.getNode(other)); varNode = edge.getEdgeVariable(); - if (f != null) { - if (match(edge, lVar, index)) { - filter = f; - init(env); - } + if (f != null && match(edge, lVar, index)) { + filter = f; + init(env); } if (mem == null && node != null) { init(env); @@ -190,7 +186,6 @@ void init(Memory env) { mem = new Memory(matcher, evaluator); mem.init(env.getQuery()); mem.init(env); - mem.setFake(true); evaluator.init(mem); mem.share(mem.getBind(), env.getBind()); mem.setEval(kgram); @@ -209,34 +204,25 @@ boolean match(Edge edge, List lVar, int index) { * index 0 */ int index(Edge edge, Environment mem, List lVar) { - int n = -1; // which arg is bound if any ? for (int i = 0; i < 2; i++) { if (mem.isBound(edge.getNode(i))) { - n = i; - break; + return i; } } - if (n == -1) { - for (int i = 0; i < 2; i++) { - if (edge.getNode(i).isConstant()) { - n = i; - break; - } + for (int i = 0; i < 2; i++) { + if (edge.getNode(i).isConstant()) { + return i; } } - if (n == -1 && lVar != null) { + if (lVar != null) { for (int i = 0; i < 2; i++) { if (match(edge, lVar, i)) { - n = i; - break; + return i; } } } - if (n == -1) { - n = 0; - } - return n; + return 0; } /** @@ -261,7 +247,7 @@ public Iterable candidate2(Node gNode, List from, Environment mem } /** - * Retrieve solution in cache TODO: manage two tables for two possible index + * Retrieve solution in cache Note: manage two tables for two possible index */ Mappings getMappings(Node cstart) { if (isCache() && cstart != null) { @@ -317,13 +303,7 @@ public void stop() { * init at creation time, no need to change. pmax comes from pathLength() <= * pmax */ - @SuppressWarnings("unused") - public void init(Regex exp, Object smode, int pmin, int pmax) { - cedge = 0; - cresult = 0; - ctest = 0; - - Regex regexp1 = exp.transform(); + public void init(Regex exp, int pmin, int pmax) { regexp = exp; isReverse = false; @@ -416,7 +396,8 @@ void process(Node cstart, Environment memory) { private Mapping result(Path path, Node gNode, Node src, Node start, boolean isReverse) { Edge ee = edge; int length = 3; - int ip = 2, is = 3; + int ip = 2; + int is = 3; if (!isStorePath) { length = 2; @@ -426,7 +407,8 @@ private Mapping result(Path path, Node gNode, Node src, Node start, boolean isRe length += 1; } - Node n1, n2; + Node n1; + Node n2; if (path.size() == 0) { n1 = start; n2 = start; @@ -478,13 +460,12 @@ private Mapping result(Path path, Node gNode, Node src, Node start, boolean isRe */ Node getPathNode(Path p) { Filter f = query.getGlobalFilter(Query.PATHNODE); - Node node; - try { - node = (Node) f.getExp().evalWE(evaluator, memory.getBind(), memory, producer); - node.setObject(p); - } catch (Exception e) { - throw new RuntimeException(e); + Node node = producer.getNode( + f.getExp().evalWE(evaluator, memory.getBind(), memory, producer)); + if (node == null) { + throw new IllegalStateException("The path-node expression returned no RDF value"); } + node.setObject(p); return node; } @@ -550,10 +531,6 @@ public Node next() { } return null; } - - @Override - public void remove() { - } }; } @@ -570,7 +547,7 @@ boolean test(Filter filter, Path path, Node qNode, Node node) { boolean test; try { test = filter.getExp().test(evaluator, mem.getBind(), mem, producer); - } catch (Exception ex) { + } catch (QueryException ex) { test = false; } mem.pop(qNode); @@ -586,7 +563,7 @@ boolean test(Node node) { boolean test; try { test = filter.getExp().test(evaluator, mem.getBind(), mem, producer); - } catch (Exception ex) { + } catch (QueryException ex) { test = false; } mem.pop(qNode); @@ -641,280 +618,246 @@ void eval(Record stack, Path path, Node start, Node src) { Regex exp = stack.pop(); switch (exp.retype()) { - case Regex.TEST: { - // exp @[ ?this != ] - boolean b = true; + case Regex.TEST: + evalTest(exp, stack, path, start, src); + break; - if (start != null) { - b = test(exp.getExpr().getFilter(), path, regexNode, start); - } + case Regex.LABEL, Regex.NOT: + evalLabel(exp, stack, path, start, src); + break; - if (b) { - eval(stack, path, start, src); - } - stack.push(exp); - } - break; + case Regex.SEQ: + evalSeq(exp, stack, path, start, src); + break; + + case Regex.PARA: + evalPara(exp, stack, path, start, src); + break; + + case Regex.CHECK: + evalCheck(exp, stack, path, start, src); + break; - case Regex.LABEL: - case Regex.NOT: { - if (path.size() >= path.getMax()) { + case Regex.PLUS: + // exp+ + if (start == null && stack.getVisit().knows(exp)) { stack.push(exp); return; } + plus(exp, stack, path, start, src); + break; - boolean inverse = exp.isInverse() || exp.isReverse(); - Producer pp = producer; - List ff = from; - Edge ee = edge; - Environment env = memory; - int ii = index, oo = other; - int pweight = path.weight(), eweight = exp.getWeight(); - int size = path.size(); - - boolean hasFilter = filter != null, - isStart = start == null, - hasSource = size == 0 && src == null && gNode != null, - hasHandler = hasListener, - hasShort = isShort, - hasOne = isOne; - - Visit visit = stack.getVisit(); - Node gg = gNode, previous = null; - ResultListener handler = listener; - - for (Edge ent : pp.getEdges(gg, ff, ee, env, exp, src, start, ii)) { - if (isStop) { - stack.push(exp); - return; - } + case Regex.COUNT: + // regex count: exp[1, n] + count(exp, stack, path, start, src); + break; - if (stack.isSuccess()) { - // parallel path has succeeded: stop it - stack.push(exp); - return; - } + case Regex.STAR: + // exp* + if (start == null && stack.getVisit().knows(exp)) { + stack.push(exp); + return; + } + star(exp, stack, path, start, src); + break; - if (ent == null) { - continue; - } + case Regex.ALT: + evalAlt(exp, stack, path, start, src); + break; - Edge rel = ent; - Node node = rel.getNode(ii); + case Regex.OPTION: + option(exp, stack, path, start, src); + break; - if (inverse) { - EdgeInv ei = new EdgeInv(ent); - rel = ei; - ent = ei; - node = rel.getNode(ii); - } + default: + break; + } + } - if (hasFilter && isStart) { - // test a filter on the index node - boolean testResult = test(rel.getNode(ii)); - if (!testResult) { - continue; - } - } + private void evalTest(Regex exp, Record stack, Path path, Node start, Node src) { + boolean b = true; + if (start != null) { + b = test(exp.getExpr().getFilter(), path, regexNode, start); + } + if (b) { + eval(stack, path, start, src); + } + stack.push(exp); + } - if (hasSource) { - // first time: bind the common source of current path - src = ent.getGraph(); - } else if (src != null && !ent.getGraph().match(src)) { - // all relations need same source in one path - continue; - } + private void evalSeq(Regex exp, Record stack, Path path, Node start, Node src) { + int fst = 0; + int rst = 1; + if (isReverse) { + fst = 1; + rst = 0; + } - if (isStart) { - boolean isNew = previous == null || !previous.match(node); - previous = node; - - if (isNew) { - // clean the table of visited nodes as we have a new start node - visit.start(); - } - - // visit start node - visit.nstart(node); - // in case there is e1 || e2 - stack.pushStart(node); - - if (hasShort) { - // reset node length to zero when start changes - if (isNew) { - pp.initPath(ee, 0); - visit.initPath(); - } - } - } + stack.push(exp.getArg(rst)); + stack.push(exp.getArg(fst)); - if (hasShort) { - // shortest path - Node other = rel.getNode(oo); - Integer l = visit.getLength(other, exp); - int length = pweight + eweight; - - if (l == null) { - visit.setLength(other, exp, length); - } else if (length > l) { - continue; - } else if (hasOne && length == l) { - continue; - } else { - visit.setLength(other, exp, length); - } - } + eval(stack, path, start, src); - if (hasHandler) { - handler.enter(ent, exp, size); - } + stack.pop(); + stack.pop(); + stack.push(exp); + } - path.add(ent, eweight); + private void evalPara(Regex exp, Record stack, Path path, Node start, Node src) { + if (start != null) { + stack.pushStart(start); + } + stack.push(exp.getArg(2)); + stack.push(exp.getArg(0)); + eval(stack, path, start, src); + stack.pop(); + stack.pop(); - boolean suc = kgram.getVisitor().step(kgram, src, ee, path, path.firstNode(), path.lastNode()); + if (start != null) { + stack.popStart(); + } + stack.push(exp); + } - if (suc) { - eval(stack, path, rel.getNode(oo), src); - } + private void evalCheck(Regex exp, Record stack, Path path, Node start, Node src) { + Regex test = exp.getArg(0); - path.remove(eweight); + if (test.retype() == Regex.PARA) { + Record st = new Record(Visit.create(isReverse, isCountPath)); + st.push(test.getArg(1)); + st.setTarget(start); + Node prev = stack.getStart(); - if (hasHandler) { - handler.leave(ent, exp, size); - } + eval(st, path, prev, src); - if (isStart) { - visit.nleave(node); - stack.popStart(); - } - } + if (st.isSuccess()) { + eval(stack, path, start, src); + } - stack.push(exp); + stack.push(exp); + } else if (test.retype() == Regex.OPTION) { + if (!stack.getVisit().nloop(test, start)) { + eval(stack, path, start, src); } - break; - - case Regex.SEQ: { - int fst = 0, rst = 1; - if (isReverse) { - // path walk from right to left - // index = 1 - // hence sequence walk from right to left - // use case: ?x p/q - fst = 1; - rst = 0; - } - stack.push(exp.getArg(rst)); - stack.push(exp.getArg(fst)); + stack.push(exp); + } + } - eval(stack, path, start, src); + private void evalAlt(Regex exp, Record stack, Path path, Node start, Node src) { + stack.push(exp.getArg(0)); + eval(stack, path, start, src); + stack.pop(); - stack.pop(); - stack.pop(); + stack.push(exp.getArg(1)); + eval(stack, path, start, src); + stack.pop(); + + stack.push(exp); + } + + private void evalLabel(Regex exp, Record stack, Path path, Node start, Node src) { + if (path.size() >= path.getMax()) { + stack.push(exp); + return; + } + + Node previous = null; + for (Edge ent : producer.getEdges(gNode, from, edge, memory, exp, src, start, index)) { + if (isStop || stack.isSuccess()) { stack.push(exp); + return; } - break; + previous = processEdge(ent, exp, stack, path, start, src, previous); + } - case Regex.PARA: - // e1 || e2 - if (start != null) { - stack.pushStart(start); - } - // push check(e2) (para has a 3rd argument for check) - stack.push(exp.getArg(2)); - // push e1 - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - // pop e1 - stack.pop(); - // pop check(e2) - stack.pop(); + stack.push(exp); + } - if (start != null) { - stack.popStart(); - } - // push para - stack.push(exp); - break; + private Node processEdge(Edge ent, Regex exp, Record stack, Path path, Node start, Node src, Node previous) { + if (ent == null) { + return previous; + } - case Regex.CHECK: - // additional statement to perform checking after - // a standard operation occurs - Regex test = exp.getArg(0); - - switch (test.retype()) { - case Regex.PARA: - // check(e1 || e2) - // e1 has computed a path from former start to this start (which is now target of e2) - // check there is a parallel path e2 from start to target - // create new Record to check loop specific to path e2 - Record st = new Record(Visit.create(isReverse, isCountPath)); - // push e2 - st.push(test.getArg(1)); - st.setTarget(start); - // retrieve the common start of path e1 and e2 - Node prev = stack.getStart(); - - eval(st, path, prev, src); - - if (st.isSuccess()) { - eval(stack, path, start, src); - } - - stack.push(exp); - break; - - case Regex.OPTION: - // check that target has not already been reached by option - // because sparql 1.1 option is not counting - if (!stack.getVisit().nloop(test, start)) { - // target not yet reached: continue evaluation - eval(stack, path, start, src); - } - - stack.push(exp); - break; - } - break; + Edge rel = ent; + Node node = rel.getNode(index); + if (exp.isInverse() || exp.isReverse()) { + EdgeInv ei = new EdgeInv(ent); + rel = ei; + ent = ei; + node = rel.getNode(index); + } - case Regex.PLUS: - // exp+ - if (start == null && stack.getVisit().knows(exp)) { - stack.push(exp); - return; - } - plus(exp, stack, path, start, src); - break; + if (filter != null && start == null && !test(rel.getNode(index))) { + return previous; + } - case Regex.COUNT: - // exp{1,n} - count(exp, stack, path, start, src); - break; + Node currentSrc = src; + if (path.size() == 0 && src == null && gNode != null) { + currentSrc = ent.getGraph(); + } else if (currentSrc != null && !ent.getGraph().match(currentSrc)) { + return previous; + } - case Regex.STAR: - // exp* - if (start == null && stack.getVisit().knows(exp)) { - stack.push(exp); - return; - } - star(exp, stack, path, start, src); - break; + Visit visit = stack.getVisit(); + boolean isStart = start == null; + if (isStart) { + previous = recordStartNode(node, previous, visit, stack); + } - case Regex.ALT: - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - stack.pop(); + if (isShort && !checkShortestPath(visit, rel.getNode(other), exp, path.weight() + exp.getWeight())) { + return previous; + } - stack.push(exp.getArg(1)); - eval(stack, path, start, src); - stack.pop(); + stepPath(ent, rel, exp, stack, path, currentSrc); - stack.push(exp); - break; + if (isStart) { + visit.nleave(node); + stack.popStart(); + } - case Regex.OPTION: - option(exp, stack, path, start, src); - break; + return previous; + } + + private Node recordStartNode(Node node, Node previous, Visit visit, Record stack) { + boolean isNew = previous == null || !previous.match(node); + if (isNew) { + visit.start(); + } + visit.nstart(node); + stack.pushStart(node); + if (isShort && isNew) { + producer.initPath(edge, 0); + visit.initPath(); + } + return node; + } + + private boolean checkShortestPath(Visit visit, Node otherNode, Regex exp, int length) { + Integer l = visit.getLength(otherNode, exp); + if (l == null) { + visit.setLength(otherNode, exp, length); + return true; + } + if (length > l || (isOne && length == l)) { + return false; + } + visit.setLength(otherNode, exp, length); + return true; + } + + private void stepPath(Edge ent, Edge rel, Regex exp, Record stack, Path path, Node currentSrc) { + if (hasListener) { + listener.enter(ent, exp, path.size()); + } + path.add(ent, exp.getWeight()); + boolean suc = kgram.getVisitor().step(kgram, currentSrc, edge, path, path.firstNode(), path.lastNode()); + if (suc) { + eval(stack, path, rel.getNode(other), currentSrc); + } + path.remove(exp.getWeight()); + if (hasListener) { + listener.leave(ent, exp, path.size()); } } @@ -928,19 +871,17 @@ boolean isDistinct(Record stack, Node start, Node target) { void result(Record stack, Path path, Node start, Node src) { if (path.size() > 0) { - if (isDistinct) { + if (isDistinct && !isDistinct(stack, path.firstNode(), path.lastNode())) { // distinct (start,target) - if (!isDistinct(stack, path.firstNode(), path.lastNode())) { - return; - } + return; } - boolean store = true; + boolean shouldStore = true; if (hasListener) { - store = listener.process(path); + shouldStore = listener.process(path); } - if (store) { + if (shouldStore) { Mapping map = result(path, gNode, src, start, isReverse); if (map != null) { result(src, map); @@ -1118,65 +1059,68 @@ void plus(Regex exp, Record stack, Path path, Node start, Node src) { */ void count(Regex exp, Record stack, Path path, Node start, Node src) { if (stack.getVisit().count(exp) >= exp.getMin()) { - if (checkLoop(exp)) { - if (stack.getVisit().nloop(exp, start)) { - stack.push(exp); - return; - } - } - - // min length is reached, can leave - int save = stack.getVisit().count(exp); - stack.getVisit().set(exp, 0); - eval(stack, path, start, src); - stack.getVisit().set(exp, save); + countMinReached(exp, stack, path, start, src); + } else { + countMinNotReached(exp, stack, path, start, src); + } + } + private void countMinReached(Regex exp, Record stack, Path path, Node start, Node src) { + if (checkLoop(exp) && stack.getVisit().nloop(exp, start)) { stack.push(exp); + return; + } - if (stack.getVisit().count(exp) < exp.getMax()) { - // max length not reached, can continue - stack.getVisit().count(exp, +1); - stack.push(exp.getArg(0)); - eval(stack, path, start, src); - stack.pop(); - stack.getVisit().count(exp, -1); - } - - if (checkLoop(exp)) { - stack.getVisit().nremove(exp, start); - } - } else { - // count(exp) < exp.getMin() - if (isReverse) { - if (checkLoop(exp)) { - // use case: ?x exp{2,} - // path goes backward - stack.getVisit().ninsert(exp, start); - } - } else if (checkLoop) { - if (stack.getVisit().nloop(exp, start)) { - stack.push(exp); - return; - } - } + // min length is reached, can leave + int save = stack.getVisit().count(exp); + stack.getVisit().set(exp, 0); + eval(stack, path, start, src); + stack.getVisit().set(exp, save); - stack.push(exp); + stack.push(exp); + if (stack.getVisit().count(exp) < exp.getMax()) { + // max length not reached, can continue stack.getVisit().count(exp, +1); stack.push(exp.getArg(0)); eval(stack, path, start, src); stack.pop(); stack.getVisit().count(exp, -1); + } - if (isReverse) { - if (checkLoop(exp)) { - // use case: ?x exp{2,} - // path goes backward - stack.getVisit().nremove(exp, start); - } - } else if (checkLoop) { + if (checkLoop(exp)) { + stack.getVisit().nremove(exp, start); + } + } + + private void countMinNotReached(Regex exp, Record stack, Path path, Node start, Node src) { + if (isReverse) { + if (checkLoop(exp)) { + // use case: ?x exp[2,] + // path goes backward + stack.getVisit().ninsert(exp, start); + } + } else if (checkLoop && stack.getVisit().nloop(exp, start)) { + stack.push(exp); + return; + } + + stack.push(exp); + + stack.getVisit().count(exp, +1); + stack.push(exp.getArg(0)); + eval(stack, path, start, src); + stack.pop(); + stack.getVisit().count(exp, -1); + + if (isReverse) { + if (checkLoop(exp)) { + // use case: ?x exp[2,] + // path goes backward stack.getVisit().nremove(exp, start); } + } else if (checkLoop) { + stack.getVisit().nremove(exp, start); } } @@ -1184,8 +1128,8 @@ boolean hasMax(Regex exp) { return exp.getMax() != -1 && exp.getMax() != Integer.MAX_VALUE; } - // for count exp {n,m} + // for count exp [n,m] boolean checkLoop(Regex exp) { return checkLoop || !hasMax(exp); } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/BasicPatternGenerator.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/BasicPatternGenerator.java index 0872c3b6b..2e30f40fe 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/BasicPatternGenerator.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/BasicPatternGenerator.java @@ -10,12 +10,17 @@ * * @author Fuqi Song, Wimmics Inria I3S */ -public class BasicPatternGenerator { +public final class BasicPatternGenerator { - public static int[][] BASIC_PATTERN = null; - private final static int P = 1, S = 0, O = 2; - private final static int[] default_order = {P, S, O};//p> modelList = new ArrayList<>(); - //** 1 Group the patterns by their pattern - for (int i = 0; i < models.size(); i++) { - List l = new ArrayList<>(); - - QPGNodeCostModel model = models.get(i); - l.add(model); - for (int j = i + 1; j < models.size(); j++) { - if (QPGNodeCostModel.compareModel(model, models.get(j), basicPatterns, producer) == 0) { - l.add(models.get(j)); - i++; - } else { - break; - } - } - modelList.add(l); - } + List> modelList = groupModels(models, basicPatterns); //** 2 assign cost for (int i = 0; i < modelList.size(); i++) { for (AbstractCostModel model : modelList.get(i)) { - model.estimate(Arrays.asList(new Object[]{modelList.size(), i})); + model.estimate(List.of(modelList.size(), i)); + } + } + } + + private List> groupModels(List models, int[][] patterns) { + List> groups = new ArrayList<>(); + int index = 0; + while (index < models.size()) { + QPGNodeCostModel first = models.get(index++); + List group = new ArrayList<>(); + group.add(first); + while (index < models.size() + && QPGNodeCostModel.compareModel(first, models.get(index), patterns, producer) == 0) { + group.add(models.get(index++)); } + groups.add(group); } + return groups; } //assign weight/sel for edge between triple pattern diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ApproximateSearchEnv.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ApproximateSearchEnv.java index be45a87b3..b81273c7f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ApproximateSearchEnv.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ApproximateSearchEnv.java @@ -7,40 +7,32 @@ /** * Data structure: Key -> (node -> Value) - * Key: (?var, ) Value: (node, similarity, algs) + * Key: (?variable, ) Value: (node, SIMILARITY, algs) * * @author Fuqi Song, Wimmics Inria I3S */ public class ApproximateSearchEnv { - private static int code = 0; - private final int id; private final Map> all; public ApproximateSearchEnv() { - this.id = code++; this.all = new HashMap<>(); } - public Double getSimilarity(Expr var, Node node) { - Key key = new Key(var); + public Double getSimilarity(Expr variable, Node node) { + Key key = new Key(variable); Value r = this.get(key, node); return (r == null) ? null : r.getSimilarity(); } private Value get(Key key, Node node) { - if (this.all.containsKey(key)) { - if (this.all.get(key).containsKey(node)) { - return this.all.get(key).get(node); - } - } - - return null; + Map candidates = all.get(key); + return candidates == null ? null : candidates.get(node); } @Override public String toString() { - StringBuilder sb = new StringBuilder("Appx search [" + this.id + "]\n"); + StringBuilder sb = new StringBuilder("Approximate search\n"); for (Map.Entry> entrySet : all.entrySet()) { Key key = entrySet.getKey(); Map value = entrySet.getValue(); @@ -54,15 +46,15 @@ public String toString() { static class Key { - private final Expr var; + private final Expr variable; private Node uri; - public Key(Expr var) { - this.var = var; + public Key(Expr variable) { + this.variable = variable; } public Expr getVar() { - return var; + return variable; } @Override @@ -79,19 +71,19 @@ public boolean equals(Object obj) { return false; } Key other = (Key) obj; - return Objects.equals(this.var, other.var); + return Objects.equals(this.variable, other.variable); } @Override public String toString() { - return "Key{" + "var=" + var + ", uri=" + uri + '}'; + return "Key{" + "variable=" + variable + ", uri=" + uri + '}'; } } static class Value { private final Node node; - private final double similarity = -1; + private static final double SIMILARITY = -1; private final String algorithms; public Value(Node node, String algorithms) { @@ -100,12 +92,12 @@ public Value(Node node, String algorithms) { } public double getSimilarity() { - return similarity; + return SIMILARITY; } @Override public String toString() { - return "[" + node + ", " + similarity + ", " + algorithms + "]"; + return "[" + node + ", " + SIMILARITY + ", " + algorithms + "]"; } } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/EnvironmentImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/EnvironmentImpl.java index 5f626c186..cc38ea4b8 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/EnvironmentImpl.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/EnvironmentImpl.java @@ -9,8 +9,7 @@ import fr.inria.corese.core.next.query.impl.kgram.core.*; import fr.inria.corese.core.next.query.impl.kgram.event.KgramEventDispatcher; import fr.inria.corese.core.next.query.impl.kgram.path.Path; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.parser.ASTExtension; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import java.util.Map; @@ -18,6 +17,7 @@ public class EnvironmentImpl implements Environment { protected Query query; public EnvironmentImpl(){ + // The base environment starts without a query or bindings. } @Override @@ -26,7 +26,7 @@ public int count() { } @Override - public Node getNode(Expr var){ + public Node getNode(Expr variable){ return null; } @@ -84,7 +84,7 @@ public Node getGraphNode() { @Override public void setObject(Object o) { - + // The base environment does not retain an attached object. } @Override @@ -94,7 +94,7 @@ public Object getObject() { @Override public void setExp(Exp exp) { - + // Expression storage is supplied by concrete execution environments. } @Override @@ -102,8 +102,8 @@ public Exp getExp() { return null; } - public Map getMap() { - return null; + public Map getMap() { + return Map.of(); } @Override @@ -132,12 +132,7 @@ public Mappings getMappings() { } @Override - public Node get(Expr var) { - throw new UnsupportedOperationException("Not supported yet."); - } - - @Override - public ASTExtension getExtension() { + public Node get(Expr variable) { throw new UnsupportedOperationException("Not supported yet."); } @@ -193,12 +188,12 @@ public ProcessVisitor getVisitor() { } @Override - public void setReport(IDatatype dt) { + public void setReport(DatatypeValue dt) { throw new UnsupportedOperationException("Not supported yet."); } @Override - public IDatatype getReport() { + public DatatypeValue getReport() { throw new UnsupportedOperationException("Not supported yet."); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/KgramNodes.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/KgramNodes.java index f78c6d0e0..9578c78de 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/KgramNodes.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/KgramNodes.java @@ -1,9 +1,9 @@ package fr.inria.corese.core.next.query.impl.kgram.tool; +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; import fr.inria.corese.core.next.query.impl.kgram.path.Path; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.datatype.DatatypeMap; /** * Factory for Corese-specific KGRAM nodes used by the Corese-next runtime model. @@ -35,7 +35,7 @@ public static Node rootProperty() { private static final class RootPropertyNode implements Node { - private static final IDatatype VALUE = DatatypeMap.newResource(ROOT_PROPERTY_URI); + private static final DatatypeValue VALUE = Values.factory().createIRI(ROOT_PROPERTY_URI); private int index = -1; private String key = INITKEY; @@ -67,12 +67,12 @@ public boolean same(Node n) { @Override public boolean match(Node n) { - return n != null && !n.isVariable() && VALUE.match(n.getDatatypeValue()); + return n != null && !n.isVariable() && VALUE.sameTerm(n.getDatatypeValue()); } @Override public int compare(Node node) { - return VALUE.compareTo(node.getDatatypeValue()); + return VALUE.compare(node.getDatatypeValue()); } @Override @@ -96,17 +96,12 @@ public boolean isBlank() { } @Override - public boolean isFuture() { - return false; - } - - @Override - public IDatatype getValue() { + public DatatypeValue getValue() { return VALUE; } @Override - public IDatatype getDatatypeValue() { + public DatatypeValue getDatatypeValue() { return getValue(); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/NodeImpl.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/NodeImpl.java index a254d5dec..defdfe99f 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/NodeImpl.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/NodeImpl.java @@ -1,34 +1,41 @@ package fr.inria.corese.core.next.query.impl.kgram.tool; +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.kgram.path.Path; import fr.inria.corese.core.next.query.impl.kgram.api.core.Edge; import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; import fr.inria.corese.core.next.query.impl.kgram.api.core.TripleStore; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.datatype.DatatypeMap; -import fr.inria.corese.core.sparql.triple.parser.Atom; -import fr.inria.corese.core.sparql.triple.parser.Constant; -import fr.inria.corese.core.sparql.triple.parser.Variable; -public class NodeImpl implements Node { +import java.util.Objects; +/** Native KGRAM node backed exclusively by Corese-next value contracts. */ +public final class NodeImpl implements Node { + private DatatypeValue value; + private final String variableName; + private int index = -1; + private String key = INITKEY; + private Object payload; - Atom atom; - int index = -1; + private NodeImpl(DatatypeValue value, String variableName) { + this.value = value; + this.variableName = variableName; + } - public NodeImpl(Atom at) { - atom = at; + /** Creates a constant node carrying a Corese-next RDF value. */ + public static NodeImpl forValue(DatatypeValue value) { + return new NodeImpl(Objects.requireNonNull(value, "value"), null); } /** Creates a constant node for an IRI. */ public static NodeImpl forIRI(String iri) { - return new NodeImpl(Constant.create(DatatypeMap.newResource(iri))); + return forValue(Values.factory().createIRI(iri)); } /** Creates a constant node for a blank node. */ public static NodeImpl forBlank(String id) { - return new NodeImpl(Constant.create(DatatypeMap.createBlank(id))); + return forValue(Values.factory().createBNode(id)); } /** @@ -39,31 +46,37 @@ public static NodeImpl forBlank(String id) { * @param lang language tag, or {@code null} */ public static NodeImpl forLiteral(String label, String datatypeUri, String lang) { - return new NodeImpl(Constant.create(DatatypeMap.createLiteral(label, datatypeUri, lang))); + if (lang != null && !lang.isEmpty()) { + return forValue(Values.factory().createLiteral(label, lang)); + } + if (datatypeUri != null && !datatypeUri.isEmpty()) { + return forValue(Values.factory().createLiteral( + label, Values.factory().createIRI(datatypeUri))); + } + return forValue(Values.factory().createLiteral(label)); } /** Creates a variable node with the given name. */ public static NodeImpl forVariable(String name) { - return new NodeImpl(new Variable(name)); + return new NodeImpl(null, Objects.requireNonNull(name, "name")); } @Override - public IDatatype getValue() { - return atom.getDatatypeValue(); - } - - public IDatatype getValue(Node n) { - return n.getValue(); + public DatatypeValue getValue() { + return value; } @Override - public IDatatype getDatatypeValue() { - return atom.getDatatypeValue(); + public DatatypeValue getDatatypeValue() { + return getValue(); } @Override - public void setDatatypeValue(IDatatype dt) { - atom = Constant.create(dt); + public void setDatatypeValue(DatatypeValue datatypeValue) { + if (isVariable()) { + throw new IllegalStateException("A variable node cannot become a constant"); + } + value = Objects.requireNonNull(datatypeValue, "datatypeValue"); } @Override @@ -78,13 +91,17 @@ public Node getNode() { @Override public String toString() { - return atom.toSparql(); // + "[" + getIndex() +"]"; + if (isVariable()) { + return variableName.startsWith("?") ? variableName : "?" + variableName; + } + return value.toString(); } @Override public int compare(Node node) { - if (node.getValue() != null) { - return getValue().compareTo(getValue(node)); + Objects.requireNonNull(node, "node"); + if (value != null && node.getDatatypeValue() != null) { + return value.compare(node.getDatatypeValue()); } return getLabel().compareTo(node.getLabel()); } @@ -96,31 +113,23 @@ public int getIndex() { @Override public String getLabel() { - if (atom.isResource()) { - return atom.getLongName(); - } - return atom.getName(); + return isVariable() ? variableName : value.getLabel(); } @Override public boolean isConstant() { - return atom.isConstant(); + return value != null; } @Override public boolean isVariable() { - return atom.isVariable(); + return variableName != null; } - // Constant bnode or sparql variable as bnode + /** Returns whether this constant node represents an RDF blank node. */ @Override public boolean isBlank() { - return atom.isBlankOrBlankNode(); - } - - @Override - public boolean isFuture() { - return isConstant() && getDatatypeValue().isFuture(); + return value != null && value.isBNode(); } @Override @@ -128,34 +137,28 @@ public boolean same(Node n) { if (isVariable() || n.isVariable()) { return sameVariable(n); } - return getValue().sameTerm(getValue(n)); + return value.sameTerm(n.getDatatypeValue()); } - boolean sameVariable(Node n) { - return isVariable() && n.isVariable() && getLabel().equals(n.getLabel()); + private boolean sameVariable(Node node) { + return isVariable() && node.isVariable() && getLabel().equals(node.getLabel()); } @Override public boolean match(Node n) { - if (isVariable() || n.isVariable()) { - return sameVariable(n); - } - return getValue().match(getValue(n)); + return same(n); } @Override - public boolean equals(Object o) { - if (o instanceof Node) { - return equals((Node) o); // was same - } - return false; + public boolean equals(Object other) { + return other instanceof Node node && equals(node); } - public boolean equals(Node n) { - if (isVariable() || n.isVariable()) { - return sameVariable(n); + public boolean equals(Node node) { + if (isVariable() || node.isVariable()) { + return sameVariable(node); } - return getValue().equals(getValue(n)); + return value.equals(node.getDatatypeValue()); } @Override @@ -165,16 +168,17 @@ public void setIndex(int n) { @Override public Object getNodeObject() { - return null; + return payload; } - @Override + @Override public Edge getEdge() { - return (Edge) getDatatypeValue().getEdge(); + return payload instanceof Edge edge ? edge : null; } @Override public void setObject(Object o) { + payload = o; } @Override @@ -184,11 +188,12 @@ public Path getPath() { @Override public String getKey() { - return INITKEY; + return key; } @Override public void setKey(String str) { + key = Objects.requireNonNull(str, "str"); } @Override @@ -196,5 +201,8 @@ public TripleStore getTripleStore() { return null; } - + @Override + public int hashCode() { + return isVariable() ? variableName.hashCode() : value.hashCode(); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ProducerDefault.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ProducerDefault.java index aa3183c78..b4d8b4a48 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ProducerDefault.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/ProducerDefault.java @@ -1,12 +1,12 @@ package fr.inria.corese.core.next.query.impl.kgram.tool; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.kgram.api.core.*; import fr.inria.corese.core.next.query.impl.kgram.api.query.Environment; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; import fr.inria.corese.core.next.query.impl.kgram.core.Exp; import fr.inria.corese.core.next.query.impl.kgram.core.Mappings; import fr.inria.corese.core.next.query.impl.kgram.core.Query; -import fr.inria.corese.core.sparql.api.IDatatype; import java.util.ArrayList; import java.util.List; @@ -46,6 +46,7 @@ public Iterable getGraphNodes(Node node, List from, @Override public void initPath(Edge edge, int index) { + // No-op by default in ProducerDefault } @Override @@ -55,7 +56,7 @@ public Node getNode(Object value) { @Override - public Mappings map(List nodes, IDatatype object) { + public Mappings map(List nodes, DatatypeValue object) { return null; } @@ -111,12 +112,12 @@ public Mappings getMappings(Node gNode, List from, Exp exp, Environment en } @Override - public IDatatype getValue(Object value) { + public DatatypeValue getValue(Object value) { throw new UnsupportedOperationException("Not supported yet."); } @Override - public IDatatype getDatatypeValue(Object value) { + public DatatypeValue getDatatypeValue(Object value) { throw new UnsupportedOperationException("Not supported yet."); } @@ -127,6 +128,7 @@ public Edge copy(Edge ent) { @Override public void close() { + // No resources to release in default implementation } @Override @@ -145,7 +147,7 @@ public DatatypeNodeFactory getDatatypeNodeFactory() { } @Override - public Mappings map(List qNodes, IDatatype object, int n) { + public Mappings map(List qNodes, DatatypeValue object, int n) { throw new UnsupportedOperationException("Not supported yet."); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdge.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdge.java index b7247d406..49e6bad25 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdge.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdge.java @@ -23,10 +23,10 @@ public final class StorageManagerEdge implements Edge { public StorageManagerEdge(Statement statement) { this.statement = Objects.requireNonNull(statement, "statement"); - this.subject = StorageManagerKgramValues.node(statement.getSubject()); - this.predicate = StorageManagerKgramValues.node(statement.getPredicate()); - this.object = StorageManagerKgramValues.node(statement.getObject()); - this.graph = statement.getContext() == null ? null : StorageManagerKgramValues.node(statement.getContext()); + this.subject = NodeImpl.forValue(statement.getSubject()); + this.predicate = NodeImpl.forValue(statement.getPredicate()); + this.object = NodeImpl.forValue(statement.getObject()); + this.graph = statement.getContext() == null ? null : NodeImpl.forValue(statement.getContext()); } public Statement getSourceStatement() { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerKgramValues.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerKgramValues.java deleted file mode 100644 index 0f50695dc..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerKgramValues.java +++ /dev/null @@ -1,112 +0,0 @@ -package fr.inria.corese.core.next.query.impl.kgram.tool; - -import fr.inria.corese.core.next.data.api.term.BNode; -import fr.inria.corese.core.next.data.api.term.IRI; -import fr.inria.corese.core.next.data.api.term.Literal; -import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.datatype.DatatypeMap; -import fr.inria.corese.core.sparql.triple.parser.Constant; - -/** - * Conversion helpers for the StorageManager/KGRAM boundary. - * - *

The storage layer exposes RDF values through the Corese-next data model, while KGRAM - * nodes still carry Corese {@link IDatatype} values. This helper keeps that transitional - * conversion in one place and avoids routing storage results through the legacy KGRAM node API. - */ -final class StorageManagerKgramValues { - - private StorageManagerKgramValues() { - } - - /** - * Converts a storage RDF value to a KGRAM node. - * - * @param value storage value to wrap - * @return KGRAM node carrying the equivalent Corese datatype - */ - static Node node(Value value) { - return node(datatypeValue(value)); - } - - /** - * Wraps a Corese datatype as a KGRAM node. - * - * @param datatype Corese datatype to wrap - * @return KGRAM node backed by a parser constant - */ - static Node node(IDatatype datatype) { - return new NodeImpl(Constant.create(datatype)); - } - - /** - * Converts a KGRAM node to a storage RDF value. - * - * @param node KGRAM node to convert - * @param valueFactory factory used to create storage values - * @return equivalent storage value - */ - static Value rdfValue(Node node, ValueFactory valueFactory) { - return rdfValue(node.getDatatypeValue(), valueFactory); - } - - /** - * Converts a Corese datatype to the storage RDF value model. - * - *

Language-tagged literals are converted through the language branch, and therefore - * become RDF {@code langString} literals in the storage model. - * - * @param datatype Corese datatype to convert - * @param valueFactory factory used to create storage values - * @return equivalent storage value - * @throws IllegalArgumentException when the datatype cannot be represented as RDF - */ - static Value rdfValue(IDatatype datatype, ValueFactory valueFactory) { - if (datatype.isURI()) { - return valueFactory.createIRI(datatype.getLabel()); - } - if (datatype.isBlank()) { - return valueFactory.createBNode(datatype.getLabel()); - } - if (datatype.isLiteral()) { - String language = datatype.getLang(); - if (language != null && !language.isEmpty()) { - return valueFactory.createLiteral(datatype.getLabel(), language); - } - String datatypeUri = datatype.getDatatypeURI(); - if (datatypeUri != null && !datatypeUri.isEmpty()) { - return valueFactory.createLiteral(datatype.getLabel(), valueFactory.createIRI(datatypeUri)); - } - return valueFactory.createLiteral(datatype.getLabel()); - } - throw new IllegalArgumentException("Unsupported KGRAM datatype value: " + datatype); - } - - /** - * Converts a storage RDF value to the Corese datatype model used by KGRAM nodes. - * - * @param value storage value to convert - * @return equivalent Corese datatype - * @throws IllegalArgumentException when the value is not an RDF IRI, blank node or literal - */ - static IDatatype datatypeValue(Value value) { - if (value instanceof IRI iri) { - return DatatypeMap.newResource(iri.stringValue()); - } - if (value instanceof BNode bNode) { - return DatatypeMap.createBlank(bNode.getID()); - } - if (value instanceof Literal literal) { - return literal.getLanguage() - .map(language -> DatatypeMap.createLiteral(literal.getLabel(), null, language)) - .orElseGet(() -> DatatypeMap.createLiteral( - literal.getLabel(), - literal.getDatatype().stringValue(), - null)); - } - throw new IllegalArgumentException("Unsupported RDF value: " + value); - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducer.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducer.java index 4050252c4..56a6490bb 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducer.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducer.java @@ -3,9 +3,8 @@ import fr.inria.corese.core.next.data.api.term.IRI; import fr.inria.corese.core.next.data.api.term.Resource; import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.api.factory.ValueFactory; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.data.api.model.Statement; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory; import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; import fr.inria.corese.core.next.query.impl.kgram.api.core.BindingContext; import fr.inria.corese.core.next.query.impl.kgram.api.core.Edge; @@ -23,8 +22,6 @@ import fr.inria.corese.core.next.query.impl.kgram.path.Path; import fr.inria.corese.core.next.storage.api.StorageManager; import fr.inria.corese.core.next.storage.api.model.StatementPattern; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.parser.ASTExtension; import java.util.ArrayList; import java.util.List; @@ -42,7 +39,6 @@ public final class StorageManagerProducer extends ProducerDefault { private final StorageManager storage; - private final ValueFactory valueFactory; /** * Creates a KGRAM producer backed by the given storage manager. @@ -50,18 +46,7 @@ public final class StorageManagerProducer extends ProducerDefault { * @param storage storage manager queried by this producer */ public StorageManagerProducer(StorageManager storage) { - this(storage, new CoreseValueFactory()); - } - - /** - * Creates a producer with an explicit value factory. - * - * @param storage storage manager queried by this producer - * @param valueFactory factory used to convert KGRAM values to storage values - */ - StorageManagerProducer(StorageManager storage, ValueFactory valueFactory) { this.storage = Objects.requireNonNull(storage, "storage"); - this.valueFactory = Objects.requireNonNull(valueFactory, "valueFactory"); } @Override @@ -92,7 +77,7 @@ public Iterable getGraphNodes(Node graphNode, List from, Environment } List nodes = new ArrayList<>(); for (Resource context : storage.metadata().getContexts()) { - Node node = StorageManagerKgramValues.node(context); + Node node = NodeImpl.forValue(context); if (matchesFrom(node, namedGraphs, environment)) { nodes.add(node); } @@ -129,29 +114,36 @@ public Node getNode(Object value) { if (value instanceof Node node) { return node; } - if (value instanceof Value rdfValue) { - return StorageManagerKgramValues.node(rdfValue); - } - if (value instanceof IDatatype datatype) { - return StorageManagerKgramValues.node(datatype); + if (value instanceof DatatypeValue datatypeValue) { + return NodeImpl.forValue(datatypeValue); } return null; } @Override - public IDatatype getDatatypeValue(Object value) { + public DatatypeValue getDatatypeValue(Object value) { if (value instanceof Node node) { return node.getDatatypeValue(); } - if (value instanceof Value rdfValue) { - return StorageManagerKgramValues.datatypeValue(rdfValue); - } - if (value instanceof IDatatype datatype) { - return datatype; + if (value instanceof DatatypeValue datatypeValue) { + return datatypeValue; } return null; } + @Override + public DatatypeValue getValue(Object value) { + return getDatatypeValue(value); + } + + private static Value rdfValue(Node node) { + DatatypeValue value = Objects.requireNonNull(node, "node").getDatatypeValue(); + if (value instanceof Value rdfValue) { + return rdfValue; + } + throw new IllegalArgumentException("KGRAM node does not carry an RDF value: " + node); + } + @Override public boolean isBindable(Node node) { return node != null && node.isVariable(); @@ -214,21 +206,21 @@ private StorageQueryPattern queryPattern(Node graphNode, List from, Edge q // Subject and predicate have stricter RDF roles than object: subject must // be a resource, predicate must be an IRI, while object accepts any RDF value. if (subjectNode != null) { - Value value = StorageManagerKgramValues.rdfValue(subjectNode, valueFactory); + Value value = rdfValue(subjectNode); if (!(value instanceof Resource resource)) { return StorageQueryPattern.emptyResult(); } subject = resource; } if (predicateNode != null) { - Value value = StorageManagerKgramValues.rdfValue(predicateNode, valueFactory); + Value value = rdfValue(predicateNode); if (!(value instanceof IRI iri)) { return StorageQueryPattern.emptyResult(); } predicate = iri; } if (objectNode != null) { - object = StorageManagerKgramValues.rdfValue(objectNode, valueFactory); + object = rdfValue(objectNode); } // Graph and dataset clauses become the statement contexts passed to storage. @@ -292,7 +284,7 @@ private ContextSelection selectExplicitGraphContext( ? ContextSelection.emptyResult() : ContextSelection.allContexts(); } - Value value = StorageManagerKgramValues.rdfValue(resolvedGraphNode, valueFactory); + Value value = rdfValue(resolvedGraphNode); if (!(value instanceof Resource resource)) { return ContextSelection.emptyResult(); } @@ -315,7 +307,7 @@ private ContextSelection selectDatasetContexts(List activeGraphs, Environm if (resolvedNode == null) { continue; } - Value value = StorageManagerKgramValues.rdfValue(resolvedNode, valueFactory); + Value value = rdfValue(resolvedNode); if (!(value instanceof Resource resource)) { return ContextSelection.emptyResult(); } @@ -630,7 +622,7 @@ public void setExp(Exp exp) { } @Override - public java.util.Map getMap() { + public java.util.Map getMap() { return delegate == null ? java.util.Map.of() : delegate.getMap(); } @@ -676,11 +668,6 @@ public Node get(Expr varExpr) { return delegate == null ? null : delegate.get(varExpr); } - @Override - public ASTExtension getExtension() { - return delegate == null ? null : delegate.getExtension(); - } - @Override public ApproximateSearchEnv getAppxSearchEnv() { return delegate == null ? null : delegate.getAppxSearchEnv(); @@ -704,12 +691,12 @@ public ProcessVisitor getVisitor() { } @Override - public IDatatype getReport() { + public DatatypeValue getReport() { return delegate == null ? null : delegate.getReport(); } @Override - public void setReport(IDatatype datatype) { + public void setReport(DatatypeValue datatype) { if (delegate != null) { delegate.setReport(datatype); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java index 411572caa..992a103a2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/query/CoreseUpdate.java @@ -1,10 +1,11 @@ package fr.inria.corese.core.next.query.impl.query; +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.factory.ValueFactory; import fr.inria.corese.core.next.data.api.term.IRI; import fr.inria.corese.core.next.data.api.term.Resource; import fr.inria.corese.core.next.data.api.model.Statement; import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory; import fr.inria.corese.core.next.query.api.Update; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; @@ -56,7 +57,7 @@ public void execute() throws QueryEvaluationException { executionGuard.run(); UpdateRequestAst request = (UpdateRequestAst) parser.parse(updateString); MutationOperations mutations = storage.mutations(); - CoreseValueFactory factory = new CoreseValueFactory(); + ValueFactory factory = Values.factory(); for (UpdateRequestUnitAst operation : request.operations()) { switch (operation) { @@ -74,7 +75,7 @@ public void execute() throws QueryEvaluationException { // ------------------------------------------------------------------------- private void applyQuads(QuadsAst quads, MutationOperations mutations, - CoreseValueFactory factory, boolean insert) { + ValueFactory factory, boolean insert) { for (TriplePatternAst triple : quads.defaultTriples()) { Statement stmt = toStatement(triple, null, factory); if (insert) { @@ -96,7 +97,7 @@ private void applyQuads(QuadsAst quads, MutationOperations mutations, } } - private Statement toStatement(TriplePatternAst triple, Resource context, CoreseValueFactory factory) { + private Statement toStatement(TriplePatternAst triple, Resource context, ValueFactory factory) { Value subject = termToValue(triple.subject(), factory); Value object = termToValue(triple.object(), factory); @@ -119,10 +120,10 @@ private Statement toStatement(TriplePatternAst triple, Resource context, CoreseV return factory.createStatement(s, p, object); } - private Value termToValue(TermAst term, CoreseValueFactory factory) { + private Value termToValue(TermAst term, ValueFactory factory) { return switch (term) { case IriAst(String raw) -> factory.createIRI(RdfText.stripAngleBrackets(raw)); - case LiteralAst(String lexical, String datatype, String lang) -> { + case LiteralAst(String lexical, String lang, String datatype) -> { if (lang != null && !lang.isBlank()) { yield factory.createLiteral(lexical, lang); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepository.java b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepository.java index ce0f6eec5..be834b94d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepository.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/repository/CoreseRepository.java @@ -1,7 +1,7 @@ package fr.inria.corese.core.next.query.impl.repository; +import fr.inria.corese.core.next.data.Values; import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory; import fr.inria.corese.core.next.query.api.exception.RepositoryException; import fr.inria.corese.core.next.query.api.repository.Repository; import fr.inria.corese.core.next.query.api.repository.RepositoryConnection; @@ -39,7 +39,7 @@ public CoreseRepository(StorageManager storage) { public CoreseRepository(StorageManager storage, StorageConfig config) { this.storage = Objects.requireNonNull(storage, "storage"); this.lifecycle = Objects.requireNonNull(storage.lifecycle(), "storage lifecycle"); - this.valueFactory = new CoreseValueFactory(); + this.valueFactory = Values.factory(); initialize(Objects.requireNonNull(config, "config")); this.open = true; } @@ -55,8 +55,9 @@ private void initialize(StorageConfig config) { } try { lifecycle.initialize(config); - } catch (StorageException | IllegalStateException e) { - throw new RepositoryException("Failed to initialize repository: " + e.getMessage(), e); + } catch (StorageException | IllegalStateException failure) { + throw new RepositoryException( + "Failed to initialize repository: " + failure.getMessage(), failure); } } @@ -76,8 +77,9 @@ public synchronized void close() throws RepositoryException { } try { lifecycle.shutdown(); - } catch (StorageException | IllegalStateException e) { - throw new RepositoryException("Failed to shut down repository: " + e.getMessage(), e); + } catch (StorageException | IllegalStateException failure) { + throw new RepositoryException( + "Failed to shut down repository: " + failure.getMessage(), failure); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java index 21bb0c3fe..01bb43103 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/QueryPrologueAst.java @@ -16,11 +16,9 @@ * after the prologue (parser options initial base, possibly overridden by * {@code BASE}). *

- * For now this type is only attached to {@link SelectQueryAst}; other query - * forms still expose - * prefix/base state via - * {@link fr.inria.corese.core.next.data.api.namespace.PrefixMapping} on - * {@link QueryAst}. + * Every query form carries its own immutable prologue snapshot, allowing + * parsing and compilation of unrelated queries to run concurrently without + * process-wide namespace state. */ public record QueryPrologueAst(List prefixDeclarations, IriAst baseIri) implements VisitableAst { diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java deleted file mode 100644 index e3517519f..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExistTerm.java +++ /dev/null @@ -1,55 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; -import fr.inria.corese.core.next.query.impl.kgram.api.core.ExprType; -import fr.inria.corese.core.next.query.impl.kgram.core.Exp; -import fr.inria.corese.core.sparql.triple.parser.ASTBuffer; -import fr.inria.corese.core.sparql.triple.parser.Term; - -import java.util.Objects; - -final class AstBackedExistTerm extends Term { - - private final GroupGraphPatternAst patternAst; - private Exp compiledPattern; - - AstBackedExistTerm(GroupGraphPatternAst patternAst) { - super("exists"); - setOper(ExprType.EXIST); - this.patternAst = Objects.requireNonNull(patternAst, "patternAst"); - } - - GroupGraphPatternAst patternAst() { - return patternAst; - } - - void setCompiledPattern(Exp compiledPattern) { - this.compiledPattern = compiledPattern; - } - - Exp compiledPattern() { - return compiledPattern; - } - - /** - * The established {@code Term} contract reports an existence test through - * {@code getExist() != null}, but this bridge carries the pattern as a next-KGRAM - * {@link Exp} instead. Reporting it here keeps - * {@code isRecExist()} true, which the engine relies on to place FILTER EXISTS correctly - * (QuerySorter, in-scope filters of OPTIONAL/MINUS). - */ - @Override - public boolean isTermExist() { - return true; - } - - @Override - public boolean isExist() { - return true; - } - - @Override - public ASTBuffer toString(ASTBuffer sb) { - return sb.append("exists {...}"); - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java index 2450e76f8..46192ea36 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedExpr.java @@ -1,66 +1,51 @@ package fr.inria.corese.core.next.query.impl.sparql.bridge; -import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; -import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.query.impl.kgram.api.core.BindingContext; -import fr.inria.corese.core.next.query.impl.kgram.api.core.DatatypeValue; import fr.inria.corese.core.next.query.impl.kgram.api.core.Expr; +import fr.inria.corese.core.next.query.impl.kgram.api.core.ExprType; import fr.inria.corese.core.next.query.impl.kgram.api.core.Filter; -import fr.inria.corese.core.next.query.impl.kgram.adapter.BindingAdapter; -import fr.inria.corese.core.next.query.impl.kgram.adapter.TripleParserEvalSupport; import fr.inria.corese.core.next.query.impl.kgram.api.query.Environment; import fr.inria.corese.core.next.query.impl.kgram.api.query.Evaluator; import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; -import fr.inria.corese.core.sparql.api.Computer; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.function.term.Binding; -import fr.inria.corese.core.sparql.triple.parser.Expression; +import fr.inria.corese.core.next.query.impl.sparql.ast.AggregateAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; +import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VariableScopeAnalyzer; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; -/** - * Wraps a {@link fr.inria.corese.core.sparql.triple.parser.Expression} (SPARQL interpreter tree) as a - * {@link fr.inria.corese.core.next.query.impl.kgram.api.core.Expr}. - * - */ +/** Immutable KGRAM expression backed directly by a Corese-next AST term. */ public final class AstBackedExpr implements Expr { - private final Expression delegate; - private final Optional sourceAst; + private final TermAst source; + private final WhereCompiler whereCompiler; private final NextFilterFromAst filterView; + private int index = ExprType.UNBOUND; + private int subtype = ExprType.GLOBAL; + private int operator; + private boolean publicExpression; - public AstBackedExpr(Expression delegate) { - this(delegate, Optional.empty()); + public AstBackedExpr(TermAst source) { + this(source, null); } - public AstBackedExpr(Expression delegate, Optional sourceAst) { - this.delegate = Objects.requireNonNull(delegate, "delegate"); - this.sourceAst = Objects.requireNonNull(sourceAst, "sourceAst"); + AstBackedExpr(TermAst source, WhereCompiler whereCompiler) { + this.source = Objects.requireNonNull(source, "source"); + this.whereCompiler = whereCompiler; + this.operator = operator(source); this.filterView = new NextFilterFromAst(this); } - /** - * The underlying SPARQL interpreter {@link Expression} (triple.parser), for metadata and {@link Filter#getFilterExpression()}. - */ - public Expression asTripleParserExpression() { - return delegate; - } - public Optional sourceAst() { - return sourceAst; - } - - private static Expr wrapInterpreterSubexpr(Object node) { - if (node == null) { - return null; - } - if (node instanceof Expression ex) { - return new AstBackedExpr(ex); - } - throw new IllegalArgumentException("Cannot wrap as AstBackedExpr: " + node); + return Optional.of(source); } @Override @@ -70,245 +55,340 @@ public Filter getFilter() { @Override public Object getPattern() { - if (delegate instanceof AstBackedExistTerm exist) { - return exist.compiledPattern(); - } - return delegate.getPattern(); + return switch (source) { + case ExistsAst(var pattern) -> + whereCompiler == null ? null : whereCompiler.compile(pattern); + default -> null; + }; } @Override public boolean isSystem() { - return delegate.isSystem(); + return false; } @Override public boolean isPublic() { - return delegate.isPublic(); + return publicExpression; } @Override - public void setPublic(boolean b) { - delegate.setPublic(b); + public void setPublic(boolean value) { + publicExpression = value; } @Override public boolean isDynamic() { - return delegate.isDynamic(); + return false; } @Override public boolean isTrace() { - return delegate.isTrace(); + return false; } @Override public boolean isDebug() { - return delegate.isDebug(); + return false; } @Override public String getLabel() { - return delegate.getLabel(); + return source.getName(); } @Override public String getModality() { - return delegate.getModality(); + return null; } @Override public List getExpList() { - List in = delegate.getExpList(); - List out = new ArrayList<>(in.size()); - for (Object o : in) { - out.add(wrapInterpreterSubexpr(o)); + List expressions = new ArrayList<>(); + for (TermAst child : children(source)) { + expressions.add(new AstBackedExpr(child, whereCompiler)); } - return out; + return List.copyOf(expressions); } @Override - public Expr getExp(int i) { - return wrapInterpreterSubexpr(delegate.getExp(i)); + public Expr getExp(int childIndex) { + return getExpList().get(childIndex); } @Override - public void setExp(int i, Expr e) { - if (e instanceof AstBackedExpr ab) { - delegate.setExp(i, ab.delegate); - } else { - throw new IllegalArgumentException("Expr must be AstBackedExpr"); - } + public void setExp(int childIndex, Expr expression) { + throw new UnsupportedOperationException("Corese-next AST expressions are immutable"); } @Override public Expr getArg() { - return wrapInterpreterSubexpr(delegate.getArg()); + return getExpList().isEmpty() ? null : getExpList().getFirst(); } @Override - public void setArg(Expr exp) { - if (exp instanceof AstBackedExpr ab) { - delegate.setArg(ab.delegate); - } else { - throw new IllegalArgumentException("Expr must be AstBackedExpr"); - } + public void setArg(Expr expression) { + throw new UnsupportedOperationException("Corese-next AST expressions are immutable"); } @Override public DatatypeValue getValue() { - return NextDatatypeValueAdapter.ofNullable(delegate.getValue()); + if (source instanceof IriAst || source instanceof LiteralAst) { + SparqlTermResolver resolver = whereCompiler == null + ? new SparqlTermResolver(null) + : whereCompiler.termResolver(); + return CoreseAstQueryBuilder.toNode(source, resolver).getDatatypeValue(); + } + return null; } @Override public DatatypeValue getDatatypeValue() { - return NextDatatypeValueAdapter.ofNullable(delegate.getDatatypeValue()); + return getValue(); } @Override public int type() { - return delegate.type(); + return switch (source) { + case VarAst ignored -> ExprType.VARIABLE; + case IriAst ignored -> ExprType.CONSTANT; + case LiteralAst ignored -> ExprType.CONSTANT; + case BooleanExpressionAst ignored -> ExprType.BOOLEAN; + case ConstraintAst ignored -> ExprType.FUNCTION; + }; } @Override public int subtype() { - return delegate.subtype(); + return subtype; } @Override - public void setSubtype(int n) { - delegate.setSubtype(n); + public void setSubtype(int value) { + subtype = value; } @Override public int oper() { - return delegate.oper(); + return operator; } @Override - public boolean match(int oper) { - return delegate.match(oper); + public boolean match(int value) { + return operator == value; } @Override - public void setOper(int n) { - delegate.setOper(n); + public void setOper(int value) { + operator = value; } @Override public boolean isAggregate() { - return delegate.isAggregate(); + return source instanceof AggregateAst; } @Override public boolean isRecAggregate() { - return delegate.isRecAggregate(); + return new VariableScopeAnalyzer().containsAggregate(source); } @Override public boolean isExist() { - return delegate.isExist(); + return source instanceof ExistsAst || source instanceof NotExistsAst; } @Override public boolean isRecExist() { - return delegate.isRecExist(); + return contains(ExistsAst.class) || contains(NotExistsAst.class); } @Override public boolean isVariable() { - return delegate.isVariable(); + return source instanceof VarAst; } @Override public boolean isConstant() { - return delegate.isConstant(); + return source instanceof IriAst || source instanceof LiteralAst; } @Override public boolean isFuncall() { - return delegate.isFuncall(); + return source instanceof FunctionCallAst; } @Override public boolean isBound() { - return delegate.isBound(); + return source instanceof BoundAst; } @Override public boolean isDistinct() { - return delegate.isDistinct(); + return source instanceof AggregateAst aggregate && aggregate.distinct(); } @Override public int arity() { - return delegate.arity(); + return children(source).size(); } @Override public int getIndex() { - return delegate.getIndex(); + return index; } @Override - public void setIndex(int index) { - delegate.setIndex(index); + public void setIndex(int value) { + index = value; } @Override public Expr getDefine() { - return wrapInterpreterSubexpr(delegate.getDefine()); + return null; } @Override - public void setDefine(Expr exp) { - if (exp instanceof AstBackedExpr ab) { - delegate.setDefine(ab.delegate); - } else { - throw new IllegalArgumentException("Expr must be AstBackedExpr"); - } + public void setDefine(Expr expression) { + throw new UnsupportedOperationException("Corese-next AST expressions are immutable"); } @Override public Expr getFunction() { - return wrapInterpreterSubexpr(delegate.getFunction()); + return null; } @Override public Expr getBody() { - return wrapInterpreterSubexpr(delegate.getBody()); + return null; } @Override public Expr getVariable() { - return wrapInterpreterSubexpr(delegate.getVariable()); + return isVariable() ? this : null; } @Override public Expr getDefinition() { - return wrapInterpreterSubexpr(delegate.getDefinition()); + return null; } @Override public boolean hasMetadata(String name) { - return delegate.hasMetadata(name); + return false; } @Override - public IDatatype evalWE(Evaluator eval, BindingContext b, Environment env, Producer p) { - if (!(eval instanceof Computer computer)) { - throw new QueryEvaluationException("Evaluator must implement Computer for triple.parser Expression evaluation"); + public DatatypeValue evalWE( + Evaluator evaluator, + BindingContext bindings, + Environment environment, + Producer producer) { + return NativeExpressionEvaluator.evaluate(source, evaluator, environment, producer, whereCompiler); + } + + boolean contains(Class type) { + if (type.isInstance(source)) { + return true; } - Binding binding = bindingFrom(b); - return TripleParserEvalSupport.evalWE(delegate, computer, binding, env, p); + for (TermAst child : children(source)) { + if (new AstBackedExpr(child, whereCompiler).contains(type)) { + return true; + } + } + return false; } - private static Binding bindingFrom(BindingContext b) { - if (b instanceof BindingAdapter(Binding delegate1)) { - return delegate1; + private static int operator(TermAst term) { + if (term instanceof VarAst) { + return ExprType.VARIABLE; + } + if (term instanceof IriAst || term instanceof LiteralAst) { + return ExprType.CONSTANT; + } + if (term instanceof FunctionCallAst call) { + return functionOperator(call); } - if (b instanceof Binding binding) { - return binding; + return switch (term) { + case AndAst ignored -> ExprType.AND; + case OrAst ignored -> ExprType.OR; + case BooleanNotAst ignored -> ExprType.NOT; + case NotExistsAst ignored -> ExprType.NOT; + case EqualsAst ignored -> ExprType.EQ; + case DifferentAst ignored -> ExprType.NE; + case LowerThanAst ignored -> ExprType.LT; + case LowerOrEqualThanAst ignored -> ExprType.LE; + case GreaterThanAst ignored -> ExprType.GT; + case GreaterOrEqualThanAst ignored -> ExprType.GE; + case AddAst ignored -> ExprType.PLUS; + case UnaryPlusAst ignored -> ExprType.PLUS; + case SubtractAst ignored -> ExprType.MINUS; + case UnaryMinusAst ignored -> ExprType.MINUS; + case MultiplyAst ignored -> ExprType.MULT; + case BoundAst ignored -> ExprType.BOUND; + case SameTermAst ignored -> ExprType.SAMETERM; + case LangAst ignored -> ExprType.LANG; + case DatatypeAst ignored -> ExprType.DATATYPE; + case BinaryRegexAst ignored -> ExprType.REGEX; + case TrinaryRegexAst ignored -> ExprType.REGEX; + case ExistsAst ignored -> ExprType.EXIST; + case BnodeAst ignored -> ExprType.BNODE; + case CoalesceAst ignored -> ExprType.COALESCE; + case IfAst ignored -> ExprType.IF; + case StrLenAst ignored -> ExprType.STRLEN; + case ContainsAst ignored -> ExprType.CONTAINS; + case ConcatAst ignored -> ExprType.CONCAT; + case IriFunctionAst ignored -> ExprType.URI; + default -> ExprType.UNDEF; + }; + } + + static List children(TermAst term) { + if (term instanceof UnaryConstraintAst unary) { + return List.of(unary.argument()); } - throw new QueryEvaluationException("BindingContext must be BindingAdapter or Binding"); + if (term instanceof BinaryConstraintAst binary) { + return List.of(binary.getLeftArgument(), binary.getRightArgument()); + } + if (term instanceof UnlimitedArgumentsFunctionAst unlimited) { + return unlimited.arguments(); + } + return switch (term) { + case AggregateAst aggregate -> aggregate.expression() == null + ? List.of() : List.of(aggregate.expression()); + case FunctionCallAst call -> call.arguments(); + case BnodeAst bnode -> bnode.getLabel() == null ? List.of() : List.of(bnode.getLabel()); + case TrinaryRegexAst regex -> List.of(regex.getString(), regex.getPattern(), regex.getFlags()); + case SubstrAst substring -> substring.getLength() == null + ? List.of(substring.getString(), substring.getStart()) + : List.of(substring.getString(), substring.getStart(), substring.getLength()); + case ReplaceAst replace -> replace.hasFlags() + ? List.of(replace.getString(), replace.getPattern(), replace.getReplacement(), replace.getFlags()) + : List.of(replace.getString(), replace.getPattern(), replace.getReplacement()); + case IfAst(var condition, var thenExpr, var elseExpr) -> List.of(condition, thenExpr, elseExpr); + case InAst(var left, var candidates) -> prepend(left, candidates); + case NotInAst(var left, var candidates) -> prepend(left, candidates); + case NotExistsAst(var pattern) -> List.of(new ExistsAst(pattern)); + default -> List.of(); + }; + } + + private static int functionOperator(FunctionCallAst call) { + if (call.functionName() instanceof IriAst(String raw)) { + String name = raw.startsWith("<") && raw.endsWith(">") + ? raw.substring(1, raw.length() - 1) + : raw; + if (name.equals("unnest") || name.endsWith("/unnest") || name.endsWith("#unnest")) { + return ExprType.UNNEST; + } + } + return ExprType.UNDEF; + } + + private static List prepend(TermAst first, List remaining) { + List terms = new ArrayList<>(remaining.size() + 1); + terms.add(first); + terms.addAll(remaining); + return List.copyOf(terms); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java index 72c132d4e..7cc92c029 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilder.java @@ -10,9 +10,6 @@ import fr.inria.corese.core.next.query.impl.kgram.core.Exp; import fr.inria.corese.core.next.query.impl.kgram.core.Query; import fr.inria.corese.core.next.query.impl.kgram.tool.NodeImpl; -import fr.inria.corese.core.sparql.triple.parser.Atom; -import fr.inria.corese.core.sparql.triple.parser.Expression; -import fr.inria.corese.core.sparql.triple.parser.Variable; import java.util.ArrayList; import java.util.LinkedHashSet; @@ -60,19 +57,16 @@ public Query toNextQuery(AskQueryAst askQueryAst) { Objects.requireNonNull(askQueryAst, "askQueryAst"); rejectUnsupportedAskClauses(askQueryAst); - QueryPrologueAst prev = SparqlAstToExpression.getCurrentPrologue(); - try { - SparqlAstToExpression.setCurrentPrologue(askQueryAst.prologue()); - Query query = createQuery( - askQueryAst.whereClause(), - askQueryAst.datasetClause(), - askQueryAst.solutionModifier()); - applyOrderBy(query, askQueryAst.solutionModifier()); - query.setAsk(true); - return query; - } finally { - SparqlAstToExpression.setCurrentPrologue(prev); - } + WhereCompiler compiler = whereCompiler.withPrologue(askQueryAst.prologue()); + Query query = createQuery( + askQueryAst.whereClause(), + askQueryAst.datasetClause(), + askQueryAst.solutionModifier(), + compiler); + applyOrderBy(query, askQueryAst.solutionModifier(), compiler); + query.setAsk(true); + query.setAST(askQueryAst); + return query; } /** @@ -88,20 +82,17 @@ public Query toNextQuery(SelectQueryAst selectQueryAst) { Objects.requireNonNull(selectQueryAst, "selectQueryAst"); rejectUnsupportedSelectClauses(selectQueryAst); - QueryPrologueAst prev = SparqlAstToExpression.getCurrentPrologue(); - try { - SparqlAstToExpression.setCurrentPrologue(selectQueryAst.prologue()); - Query query = createQuery( - selectQueryAst.whereClause(), - selectQueryAst.datasetClause(), - selectQueryAst.solutionModifier()); - applyProjection(query, selectQueryAst.projection()); - query.setDistinct(selectQueryAst.solutionModifier().distinct()); - applyOrderBy(query, selectQueryAst.solutionModifier()); - return query; - } finally { - SparqlAstToExpression.setCurrentPrologue(prev); - } + WhereCompiler compiler = whereCompiler.withPrologue(selectQueryAst.prologue()); + Query query = createQuery( + selectQueryAst.whereClause(), + selectQueryAst.datasetClause(), + selectQueryAst.solutionModifier(), + compiler); + applyProjection(query, selectQueryAst.projection()); + query.setDistinct(selectQueryAst.solutionModifier().distinct()); + applyOrderBy(query, selectQueryAst.solutionModifier(), compiler); + query.setAST(selectQueryAst); + return query; } /** @@ -118,20 +109,17 @@ public Query toNextQuery(DescribeQueryAst describeQueryAst) { Objects.requireNonNull(describeQueryAst, "describeQueryAst"); rejectUnsupportedDescribeClauses(describeQueryAst); - QueryPrologueAst prev = SparqlAstToExpression.getCurrentPrologue(); - try { - SparqlAstToExpression.setCurrentPrologue(describeQueryAst.prologue()); - Query query = createQuery( - describeQueryAst.whereClause(), - describeQueryAst.datasetClause(), - describeQueryAst.solutionModifier()); - applyOrderBy(query, describeQueryAst.solutionModifier()); - List describedNodes = describeNodes(query, describeQueryAst); - lowerDescribeToConstructQuery(query, describedNodes); - return query; - } finally { - SparqlAstToExpression.setCurrentPrologue(prev); - } + WhereCompiler compiler = whereCompiler.withPrologue(describeQueryAst.prologue()); + Query query = createQuery( + describeQueryAst.whereClause(), + describeQueryAst.datasetClause(), + describeQueryAst.solutionModifier(), + compiler); + applyOrderBy(query, describeQueryAst.solutionModifier(), compiler); + List describedNodes = describeNodes(query, describeQueryAst, compiler); + lowerDescribeToConstructQuery(query, describedNodes); + query.setAST(describeQueryAst); + return query; } /** @@ -148,22 +136,19 @@ public Query toNextQuery(ConstructQueryAst constructQueryAst) { Objects.requireNonNull(constructQueryAst, "constructQueryAst"); rejectUnsupportedConstructClauses(constructQueryAst); - QueryPrologueAst prev = SparqlAstToExpression.getCurrentPrologue(); - try { - SparqlAstToExpression.setCurrentPrologue(constructQueryAst.prologue()); - Query query = createQuery( - constructQueryAst.whereClause(), - constructQueryAst.datasetClause(), - constructQueryAst.solutionModifier()); - applyOrderBy(query, constructQueryAst.solutionModifier()); - Exp template = compileConstructTemplate(query, constructQueryAst.constructTemplate()); - query.setConstruct(true); - query.setConstruct(template); - query.setConstructNodes(template.getNodes()); - return query; - } finally { - SparqlAstToExpression.setCurrentPrologue(prev); - } + WhereCompiler compiler = whereCompiler.withPrologue(constructQueryAst.prologue()); + Query query = createQuery( + constructQueryAst.whereClause(), + constructQueryAst.datasetClause(), + constructQueryAst.solutionModifier(), + compiler); + applyOrderBy(query, constructQueryAst.solutionModifier(), compiler); + Exp template = compileConstructTemplate(query, constructQueryAst.constructTemplate(), compiler); + query.setConstruct(true); + query.setConstruct(template); + query.setConstructNodes(template.getNodes()); + query.setAST(constructQueryAst); + return query; } /** @@ -183,7 +168,7 @@ public Filter toNextFilter(TermAst filterExpression) { */ public Filter toNextFilter(ConstraintAst filterExpression) { Objects.requireNonNull(filterExpression, "filterExpression"); - return SparqlAstToExpression.toNextFilter(filterExpression, whereCompiler); + return new AstBackedExpr(filterExpression, whereCompiler).getFilter(); } /** @@ -194,13 +179,22 @@ public Filter toNextFilter(ConstraintAst filterExpression) { * {@link WhereCompiler} need a single shared term-to-node conversion rule.

*/ static Node toNode(TermAst term) { - Expression expression = SparqlAstToExpression.convert(term); - if (expression instanceof Atom atom) { - return new NodeImpl(atom); - } - throw new IllegalArgumentException( - "A query term must be a variable, IRI or literal, got: " - + term.getClass().getSimpleName()); + return toNode(term, new SparqlTermResolver(null)); + } + + static Node toNode(TermAst term, SparqlTermResolver resolver) { + return switch (term) { + case VarAst(String name) -> NodeImpl.forVariable(name); + case IriAst(String raw) when raw.startsWith("_:") -> NodeImpl.forBlank(raw.substring(2)); + case IriAst(String raw) -> NodeImpl.forIRI(resolver.resolveIri(raw)); + case LiteralAst(String lexical, String lang, String datatype) -> NodeImpl.forLiteral( + resolver.unquoteLexical(lexical), + resolver.normalizeDatatypeIri(datatype), + lang); + default -> throw new IllegalArgumentException( + "A query term must be a variable, IRI or literal, got: " + + term.getClass().getSimpleName()); + }; } static TermAst simplePredicate(PathAst path) { @@ -302,27 +296,28 @@ private static void rejectUnsupportedDescribeClauses(DescribeQueryAst describeQu private Query createQuery( GroupGraphPatternAst whereClause, DatasetClauseAst datasetClause, - SolutionModifierAst solutionModifier) { - Query query = Query.create(whereCompiler.compile(whereClause)); + SolutionModifierAst solutionModifier, + WhereCompiler compiler) { + Query query = Query.create(compiler.compile(whereClause)); // Collect visible nodes once so later clauses (projection, ORDER BY, DESCRIBE) // can resolve variables against the compiled runtime body. query.collect(); - applyDataset(query, datasetClause); + applyDataset(query, datasetClause, compiler); applyLimitOffset(query, solutionModifier); return query; } - private void applyDataset(Query query, DatasetClauseAst datasetClause) { - query.setFrom(toNodeList(datasetClause.graphs())); - query.setNamed(toNodeList(datasetClause.namedGraphs())); + private void applyDataset(Query query, DatasetClauseAst datasetClause, WhereCompiler compiler) { + query.setFrom(toNodeList(datasetClause.graphs(), compiler)); + query.setNamed(toNodeList(datasetClause.namedGraphs(), compiler)); query.setDatasetSpecified( !datasetClause.graphs().isEmpty() || !datasetClause.namedGraphs().isEmpty()); } - private List toNodeList(Iterable iris) { + private List toNodeList(Iterable iris, WhereCompiler compiler) { List nodes = new ArrayList<>(); for (IriAst iri : iris) { - nodes.add(toNode(iri)); + nodes.add(toNode(iri, compiler.termResolver())); } return nodes; } @@ -360,7 +355,8 @@ private void applyProjection(Query query, ProjectionAst projection) { * A described variable reuses its runtime node (so it is the one bound by the body) and * fails fast when it is not visible; a described IRI becomes a fresh constant node.

*/ - private List describeNodes(Query query, DescribeQueryAst describeQueryAst) { + private List describeNodes( + Query query, DescribeQueryAst describeQueryAst, WhereCompiler compiler) { if (describeQueryAst.isDescribeAll()) { return query.selectNodesFromPattern(); } @@ -374,7 +370,7 @@ private List describeNodes(Query query, DescribeQueryAst describeQueryAst) } nodes.add(node); } else { - nodes.add(toNode(term)); + nodes.add(toNode(term, compiler.termResolver())); } } return nodes; @@ -418,7 +414,7 @@ private DescribePattern describePattern(Node describedNode, int index) { } private Node createSyntheticDescribeNode(String role, int describedIndex, int directionIndex) { - return new NodeImpl(Variable.create("__describe_" + role + "_" + describedIndex + "_" + directionIndex)); + return NodeImpl.forVariable("__describe_" + role + "_" + describedIndex + "_" + directionIndex); } private record DescribePattern(Exp outgoing, Exp incoming, Exp optionalBody) { @@ -430,21 +426,26 @@ private record DescribePattern(Exp outgoing, Exp incoming, Exp optionalBody) { *

Variables reuse already-visible query nodes. Other expressions are wrapped as runtime * filters and attached to synthetic internal nodes, just like the historical pipeline does.

*/ - private void applyOrderBy(Query query, SolutionModifierAst solutionModifier) { + private void applyOrderBy( + Query query, SolutionModifierAst solutionModifier, WhereCompiler compiler) { if (!solutionModifier.hasOrderBy()) { return; } List orderByExpressions = new ArrayList<>(); int syntheticIndex = 0; for (OrderConditionAst orderCondition : solutionModifier.orderBy()) { - Exp orderExpression = toOrderByExpression(query, orderCondition, syntheticIndex++); + Exp orderExpression = toOrderByExpression(query, orderCondition, syntheticIndex++, compiler); orderExpression.status(orderCondition.orderDirection() == ASTConstants.OrderDirection.DESC); orderByExpressions.add(orderExpression); } query.setOrderBy(orderByExpressions); } - private Exp toOrderByExpression(Query query, OrderConditionAst orderCondition, int syntheticIndex) { + private Exp toOrderByExpression( + Query query, + OrderConditionAst orderCondition, + int syntheticIndex, + WhereCompiler compiler) { TermAst expression = orderCondition.expression(); if (expression instanceof VarAst(String name)) { Exp selectExpression = query.getSelectExp(name); @@ -457,14 +458,14 @@ private Exp toOrderByExpression(Query query, OrderConditionAst orderCondition, i } return Exp.create(Type.NODE, node); } - Filter filter = SparqlAstToExpression.toNextFilter(expression, whereCompiler); + Filter filter = new AstBackedExpr(expression, compiler).getFilter(); Exp exp = Exp.create(Type.NODE, createSyntheticOrderNode(syntheticIndex)); exp.setFilter(filter); return exp; } private Node createSyntheticOrderNode(int syntheticIndex) { - return new NodeImpl(Variable.create("__order_by_" + syntheticIndex)); + return NodeImpl.forVariable("__order_by_" + syntheticIndex); } private List toNodeExpressions(List nodes) { @@ -533,12 +534,13 @@ private static void rejectUnsupportedConstructClauses(ConstructQueryAst construc * Compiles a {@code CONSTRUCT} template into a KGRAM {@link Exp} (a BGP of edges), kept separate * from the {@code WHERE} body and carried by {@link Query#setConstruct(Exp)}. */ - private Exp compileConstructTemplate(Query query, ConstructTemplateAst template) { + private Exp compileConstructTemplate( + Query query, ConstructTemplateAst template, WhereCompiler compiler) { Exp bgp = Exp.create(Type.BGP); for (TriplePatternAst triple : template.triplePatternAsts()) { - Node subject = constructNode(query, triple.subject()); - Node predicate = constructNode(query, simplePredicate(triple.predicate())); - Node object = constructNode(query, triple.object()); + Node subject = constructNode(query, triple.subject(), compiler); + Node predicate = constructNode(query, simplePredicate(triple.predicate()), compiler); + Node object = constructNode(query, triple.object(), compiler); bgp.add(new AstBackedEdge(subject, predicate, object)); } return bgp; @@ -550,11 +552,11 @@ private Exp compileConstructTemplate(Query query, ConstructTemplateAst template) * valid SPARQL and simply skips its triple at instantiation, so this does not throw). IRIs, blank * nodes and literals become fresh constant nodes. */ - private Node constructNode(Query query, TermAst term) { + private Node constructNode(Query query, TermAst term, WhereCompiler compiler) { if (term instanceof VarAst(String name)) { Node bound = visibleBodyNode(query, name); - return bound != null ? bound : toNode(term); + return bound != null ? bound : toNode(term, compiler.termResolver()); } - return toNode(term); + return toNode(term, compiler.termResolver()); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverter.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverter.java deleted file mode 100644 index 36dc009f0..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverter.java +++ /dev/null @@ -1,46 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory; -import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; -import fr.inria.corese.core.sparql.api.IDatatype; - -/** - * Converts KGRAM {@link Node} constants to API {@link Value} instances. - * - *

This is the single point in the bridge layer that is allowed to inspect - * {@link IDatatype} on behalf of callers outside {@code next.query.impl.kgram}.

- */ -public final class KgramNodeConverter { - - private KgramNodeConverter() {} - - /** - * Converts a KGRAM constant {@link Node} to the corresponding API {@link Value}. - * - * @param node the KGRAM node to convert (must not be null) - * @param factory the value factory used to create API term instances - * @return the API value, or {@code null} when the node kind is not supported - */ - public static Value nodeToValue(Node node, CoreseValueFactory factory) { - IDatatype dt = node.getDatatypeValue(); - if (dt.isURI()) { - return factory.createIRI(dt.getLabel()); - } - if (dt.isBlank()) { - return factory.createBNode(dt.getLabel()); - } - if (dt.isLiteral()) { - String lang = dt.getLang(); - if (lang != null && !lang.isEmpty()) { - return factory.createLiteral(dt.getLabel(), lang); - } - String datatypeUri = dt.getDatatypeURI(); - if (datatypeUri != null) { - return factory.createLiteral(dt.getLabel(), factory.createIRI(datatypeUri)); - } - return factory.createLiteral(dt.getLabel()); - } - return null; - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeBooleanExpressionEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeBooleanExpressionEvaluator.java new file mode 100644 index 000000000..13bb1bf21 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeBooleanExpressionEvaluator.java @@ -0,0 +1,185 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.AndAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryConstraintAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryRegexAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BooleanExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BooleanNotAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BoundAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ContainsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.DifferentAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.EqualsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ExistsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.GreaterOrEqualThanAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.GreaterThanAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.InAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.IsBlankAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.IsIriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.IsLiteralAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.LangMatchesAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.LowerOrEqualThanAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.LowerThanAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NotExistsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NotInAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.OrAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.SameTermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrEndsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrStartsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.TrinaryRegexAst; + +import java.util.List; + +/** Evaluates boolean expressions using the SPARQL error truth tables. */ +final class NativeBooleanExpressionEvaluator { + + private NativeBooleanExpressionEvaluator() { + } + + static DatatypeValue evaluate(BooleanExpressionAst expression, NativeEvaluationContext context) { + boolean result = switch (expression) { + case AndAst binary -> and(binary, context); + case OrAst binary -> or(binary, context); + case BooleanNotAst unary -> !context.effectiveBooleanValue(unary.argument()); + case EqualsAst binary -> left(binary, context).equalsWE(right(binary, context)); + case DifferentAst binary -> !left(binary, context).equalsWE(right(binary, context)); + case LowerThanAst binary -> compare(binary, context) < 0; + case LowerOrEqualThanAst binary -> compare(binary, context) <= 0; + case GreaterThanAst binary -> compare(binary, context) > 0; + case GreaterOrEqualThanAst binary -> compare(binary, context) >= 0; + case SameTermAst binary -> left(binary, context).sameTerm(right(binary, context)); + case BoundAst bound -> bound.argument() instanceof VarAst(var name) + && context.variable(name) != null; + case IsIriAst unary -> context.required(unary.argument()).isIRI(); + case IsBlankAst unary -> context.required(unary.argument()).isBNode(); + case IsLiteralAst unary -> context.required(unary.argument()).isLiteral(); + case StrStartsAst binary -> NativeStringExpressionEvaluator.startsWith(binary, context); + case StrEndsAst binary -> NativeStringExpressionEvaluator.endsWith(binary, context); + case ContainsAst binary -> NativeStringExpressionEvaluator.contains(binary, context); + case LangMatchesAst binary -> languageMatches( + context.stringLiteral(binary.getLeftArgument()).getLabel(), + context.stringLiteral(binary.getRightArgument()).getLabel()); + case BinaryRegexAst regex -> NativeStringExpressionEvaluator.regex(regex, context); + case TrinaryRegexAst regex -> NativeStringExpressionEvaluator.regex(regex, context); + case InAst(var left, var candidates) -> in(context.required(left), candidates, context); + case NotInAst(var left, var candidates) -> !in(context.required(left), candidates, context); + case ExistsAst(var pattern) -> context.exists(pattern); + case NotExistsAst(var pattern) -> !context.exists(pattern); + default -> throw unsupported(expression); + }; + return context.values().createLiteral(result); + } + + private static boolean and(BinaryConstraintAst expression, NativeEvaluationContext context) { + BooleanResult left = booleanResult(expression.getLeftArgument(), context); + if (left.isFalse()) { + return false; + } + BooleanResult right = booleanResult(expression.getRightArgument(), context); + if (right.isFalse()) { + return false; + } + left.throwFailure(); + right.throwFailure(); + return true; + } + + private static boolean or(BinaryConstraintAst expression, NativeEvaluationContext context) { + BooleanResult left = booleanResult(expression.getLeftArgument(), context); + if (left.isTrue()) { + return true; + } + BooleanResult right = booleanResult(expression.getRightArgument(), context); + if (right.isTrue()) { + return true; + } + left.throwFailure(); + right.throwFailure(); + return false; + } + + private static BooleanResult booleanResult(TermAst expression, NativeEvaluationContext context) { + try { + return BooleanResult.value(context.effectiveBooleanValue(expression)); + } catch (QueryEvaluationException failure) { + return BooleanResult.failure(failure); + } + } + + private static boolean in( + DatatypeValue left, + List candidates, + NativeEvaluationContext context) { + QueryEvaluationException failure = null; + for (TermAst candidate : candidates) { + try { + DatatypeValue right = context.required(candidate); + if (left.equalsWE(right)) { + return true; + } + } catch (QueryEvaluationException candidateFailure) { + failure = candidateFailure; + } + } + if (failure != null) { + throw failure; + } + return false; + } + + private static DatatypeValue left(BinaryConstraintAst expression, NativeEvaluationContext context) { + return context.required(expression.getLeftArgument()); + } + + private static DatatypeValue right(BinaryConstraintAst expression, NativeEvaluationContext context) { + return context.required(expression.getRightArgument()); + } + + private static int compare(BinaryConstraintAst expression, NativeEvaluationContext context) { + return NativeValueComparison.compare(left(expression, context), right(expression, context)); + } + + private static boolean languageMatches(String language, String range) { + if ("*".equals(range)) { + return !language.isEmpty(); + } + String normalizedLanguage = language.toLowerCase(java.util.Locale.ROOT); + String normalizedRange = range.toLowerCase(java.util.Locale.ROOT); + return normalizedLanguage.equals(normalizedRange) + || normalizedLanguage.startsWith(normalizedRange + "-"); + } + + private static UnsupportedQueryFeatureException unsupported(BooleanExpressionAst expression) { + return new UnsupportedQueryFeatureException( + "Boolean expression is not supported yet: " + expression.getClass().getSimpleName()); + } + + private record BooleanResult(Boolean value, QueryEvaluationException failure) { + + static BooleanResult value(boolean value) { + return new BooleanResult(value, null); + } + + static BooleanResult failure(QueryEvaluationException failure) { + return new BooleanResult(null, failure); + } + + boolean isTrue() { + return Boolean.TRUE.equals(value); + } + + boolean isFalse() { + return Boolean.FALSE.equals(value); + } + + void throwFailure() { + if (failure != null) { + throw failure; + } + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java new file mode 100644 index 000000000..6a84241f7 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeEvaluationContext.java @@ -0,0 +1,141 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.factory.ValueFactory; +import fr.inria.corese.core.next.data.api.literal.RDFDatatype; +import fr.inria.corese.core.next.data.api.literal.XSDDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; +import fr.inria.corese.core.next.query.impl.kgram.api.query.Environment; +import fr.inria.corese.core.next.query.impl.kgram.api.query.Evaluator; +import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; +import fr.inria.corese.core.next.query.impl.kgram.core.SparqlException; +import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; + +import java.time.OffsetDateTime; +import java.util.Objects; + +/** Query-scoped collaborators and value checks shared by native expression evaluators. */ +final class NativeEvaluationContext { + + private final ValueFactory values = Values.factory(); + private final Evaluator evaluator; + private final Environment environment; + private final Producer producer; + private final WhereCompiler whereCompiler; + + NativeEvaluationContext( + Evaluator evaluator, + Environment environment, + Producer producer, + WhereCompiler whereCompiler) { + this.evaluator = Objects.requireNonNull(evaluator, "evaluator"); + this.environment = environment; + this.producer = producer; + this.whereCompiler = whereCompiler; + } + + DatatypeValue evaluate(TermAst expression) { + try { + return NativeExpressionEvaluator.evaluateExpression(expression, this); + } catch (QueryEvaluationException | UnsupportedQueryFeatureException failure) { + throw failure; + } catch (RuntimeException failure) { + throw new QueryEvaluationException( + "Failed to evaluate SPARQL expression " + expression.getName(), failure); + } + } + + DatatypeValue variable(String name) { + if (environment == null) { + return null; + } + Node node = environment.getNode(name); + return node == null ? null : node.getDatatypeValue(); + } + + DatatypeValue constant(TermAst expression) { + return CoreseAstQueryBuilder.toNode(expression, termResolver()).getDatatypeValue(); + } + + DatatypeValue required(TermAst expression) { + return required(evaluate(expression)); + } + + DatatypeValue required(DatatypeValue value) { + if (value == null) { + throw new QueryEvaluationException("Expression references an unbound variable"); + } + return value; + } + + Literal literal(TermAst expression) { + DatatypeValue value = required(expression); + if (value instanceof Literal literal) { + return literal; + } + throw new QueryEvaluationException("Expected an RDF literal"); + } + + Literal stringLiteral(TermAst expression) { + Literal literal = literal(expression); + if (literal.getCoreDatatype() == XSDDatatype.STRING + || literal.getCoreDatatype() == RDFDatatype.LANGSTRING) { + return literal; + } + throw new QueryEvaluationException("Expected a string RDF literal"); + } + + boolean effectiveBooleanValue(TermAst expression) { + return effectiveBooleanValue(required(expression)); + } + + boolean effectiveBooleanValue(DatatypeValue value) { + DatatypeValue boundValue = required(value); + if (!(boundValue instanceof Literal literal)) { + throw new QueryEvaluationException("RDF term has no SPARQL effective boolean value"); + } + if (literal.getCoreDatatype() == XSDDatatype.BOOLEAN) { + return literal.booleanValue(); + } + if (literal.isNumber()) { + double number = literal.doubleValue(); + return number != 0.0d && !Double.isNaN(number); + } + if (literal.getCoreDatatype() == XSDDatatype.STRING + || literal.getCoreDatatype() == RDFDatatype.LANGSTRING) { + return !literal.getLabel().isEmpty(); + } + throw new QueryEvaluationException("RDF literal has no SPARQL effective boolean value"); + } + + boolean exists(GroupGraphPatternAst pattern) { + if (environment == null || environment.getEval() == null || whereCompiler == null) { + throw new QueryEvaluationException("EXISTS requires an active query evaluation context"); + } + try { + return environment.getEval().exists( + producer, environment.getGraphNode(), whereCompiler.compile(pattern)); + } catch (SparqlException exception) { + throw new QueryEvaluationException("Failed to evaluate EXISTS graph pattern", exception); + } + } + + OffsetDateTime queryEvaluationTime() { + return evaluator.getQueryEvaluationTime(); + } + + ValueFactory values() { + return values; + } + + SparqlTermResolver termResolver() { + return whereCompiler == null + ? new SparqlTermResolver(null) + : whereCompiler.termResolver(); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeExpressionEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeExpressionEvaluator.java new file mode 100644 index 000000000..238872853 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeExpressionEvaluator.java @@ -0,0 +1,96 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.kgram.api.query.Environment; +import fr.inria.corese.core.next.query.impl.kgram.api.query.Evaluator; +import fr.inria.corese.core.next.query.impl.kgram.api.query.Producer; +import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BnodeAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BooleanExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.CoalesceAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FunctionCallAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.IfAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.IriExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.LiteralExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NumericExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.XsdDateTimeExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.XsdDayTimeDurationExpressionAst; + +import java.util.List; +import java.util.Objects; + +/** Dispatches Corese-next AST expressions to small native evaluators. */ +final class NativeExpressionEvaluator { + + private NativeExpressionEvaluator() { + } + + static DatatypeValue evaluate( + TermAst expression, + Evaluator evaluator, + Environment environment, + Producer producer, + WhereCompiler whereCompiler) { + Objects.requireNonNull(expression, "expression"); + NativeEvaluationContext context = new NativeEvaluationContext( + evaluator, environment, producer, whereCompiler); + return context.evaluate(expression); + } + + static DatatypeValue evaluateExpression( + TermAst expression, + NativeEvaluationContext context) { + return switch (expression) { + case VarAst(var name) -> context.variable(name); + case IriAst ignored -> context.constant(expression); + case LiteralAst ignored -> context.constant(expression); + case BooleanExpressionAst booleanExpression -> + NativeBooleanExpressionEvaluator.evaluate(booleanExpression, context); + case NumericExpressionAst numericExpression -> + NativeNumericExpressionEvaluator.evaluate(numericExpression, context); + case IriExpressionAst iriExpression -> + NativeIriExpressionEvaluator.evaluate(iriExpression, context); + case XsdDateTimeExpressionAst dateTimeExpression -> + NativeTemporalExpressionEvaluator.evaluateDateTime(dateTimeExpression, context); + case XsdDayTimeDurationExpressionAst durationExpression -> + NativeTemporalExpressionEvaluator.evaluateDuration(durationExpression, context); + case LiteralExpressionAst literalExpression -> + NativeStringExpressionEvaluator.evaluate(literalExpression, context); + case CoalesceAst coalesce -> coalesce(coalesce.arguments(), context); + case IfAst(var condition, var thenExpr, var elseExpr) -> context.evaluate( + context.effectiveBooleanValue(condition) ? thenExpr : elseExpr); + case BnodeAst bnode -> bnode.getLabel() == null + ? context.values().createBNode() + : context.values().createBNode(context.required(bnode.getLabel()).stringValue()); + case FunctionCallAst function -> throw new UnsupportedQueryFeatureException( + "Extension function evaluation is not supported yet: " + function.getName()); + default -> throw new UnsupportedQueryFeatureException( + "Expression is not supported yet by the native evaluator: " + + expression.getClass().getSimpleName()); + }; + } + + private static DatatypeValue coalesce( + List arguments, + NativeEvaluationContext context) { + QueryEvaluationException lastFailure = null; + for (TermAst argument : arguments) { + try { + DatatypeValue value = context.evaluate(argument); + if (value != null) { + return value; + } + } catch (QueryEvaluationException failure) { + lastFailure = failure; + } + } + throw new QueryEvaluationException( + "COALESCE has no bound, successfully evaluated argument", + lastFailure); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeIriExpressionEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeIriExpressionEvaluator.java new file mode 100644 index 000000000..ea6d77dd0 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeIriExpressionEvaluator.java @@ -0,0 +1,41 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.DatatypeAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.IriExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.IriFunctionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UuidAst; + +import java.util.UUID; + +/** Evaluates expressions whose result is an IRI. */ +final class NativeIriExpressionEvaluator { + + private NativeIriExpressionEvaluator() { + } + + static DatatypeValue evaluate(IriExpressionAst expression, NativeEvaluationContext context) { + return switch (expression) { + case DatatypeAst datatype -> datatype(context.required(datatype.argument()), context); + case IriFunctionAst iri -> context.values().createIRI( + context.termResolver().resolveIri( + context.required(iri.argument()).stringValue())); + case UuidAst ignored -> context.values().createIRI("urn:uuid:" + UUID.randomUUID()); + default -> throw new UnsupportedQueryFeatureException( + "IRI expression is not supported yet: " + + expression.getClass().getSimpleName()); + }; + } + + private static DatatypeValue datatype( + DatatypeValue value, + NativeEvaluationContext context) { + if (!(value instanceof Literal literal)) { + throw new QueryEvaluationException("DATATYPE expects an RDF literal"); + } + return context.values().createIRI(literal.getDatatype().stringValue()); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeNumericExpressionEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeNumericExpressionEvaluator.java new file mode 100644 index 000000000..d6a97d330 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeNumericExpressionEvaluator.java @@ -0,0 +1,285 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.literal.XSDDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.AbsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.AddAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryConstraintAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.CeilAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.DayAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.DivideAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.FloorAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.HoursAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.MinutesAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.MonthAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.MultiplyAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NumericExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.RandAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.RoundAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.SecondsAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrLenAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.SubtractAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UnaryMinusAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UnaryPlusAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.YearAst; + +import javax.xml.datatype.XMLGregorianCalendar; +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.util.concurrent.ThreadLocalRandom; + +/** Evaluates numeric expressions with SPARQL numeric type promotion. */ +final class NativeNumericExpressionEvaluator { + + private NativeNumericExpressionEvaluator() { + } + + static DatatypeValue evaluate( + NumericExpressionAst expression, + NativeEvaluationContext context) { + return switch (expression) { + case AddAst binary -> arithmetic(binary, context, Operation.ADD); + case SubtractAst binary -> arithmetic(binary, context, Operation.SUBTRACT); + case MultiplyAst binary -> arithmetic(binary, context, Operation.MULTIPLY); + case DivideAst binary -> arithmetic(binary, context, Operation.DIVIDE); + case UnaryPlusAst unary -> numericLiteral(context.required(unary.argument())).literal(); + case UnaryMinusAst unary -> unaryMinus( + numericLiteral(context.required(unary.argument())), context); + case AbsAst unary -> absolute( + numericLiteral(context.required(unary.argument())), context); + case CeilAst unary -> rounded( + numericLiteral(context.required(unary.argument())), context, Rounding.CEILING); + case FloorAst unary -> rounded( + numericLiteral(context.required(unary.argument())), context, Rounding.FLOOR); + case RoundAst unary -> rounded( + numericLiteral(context.required(unary.argument())), context, Rounding.NEAREST); + case StrLenAst unary -> stringLength(unary, context); + case RandAst ignored -> context.values().createLiteral( + ThreadLocalRandom.current().nextDouble()); + case YearAst unary -> context.values().createLiteral(calendar(unary.argument(), context).getYear()); + case MonthAst unary -> context.values().createLiteral(calendar(unary.argument(), context).getMonth()); + case DayAst unary -> context.values().createLiteral(calendar(unary.argument(), context).getDay()); + case HoursAst unary -> context.values().createLiteral(calendar(unary.argument(), context).getHour()); + case MinutesAst unary -> context.values().createLiteral(calendar(unary.argument(), context).getMinute()); + case SecondsAst unary -> seconds(calendar(unary.argument(), context), context); + default -> throw unsupported(expression); + }; + } + + static double numericDouble(DatatypeValue value) { + return numericLiteral(value).literal().doubleValue(); + } + + private static DatatypeValue arithmetic( + BinaryConstraintAst expression, + NativeEvaluationContext context, + Operation operation) { + NumericLiteral left = numericLiteral(context.required(expression.getLeftArgument())); + NumericLiteral right = numericLiteral(context.required(expression.getRightArgument())); + NumericKind resultKind = NumericKind.promote(left.kind(), right.kind(), operation); + return switch (resultKind) { + case DOUBLE -> context.values().createLiteral( + doubleOperation(left.doubleValue(), right.doubleValue(), operation)); + case FLOAT -> context.values().createLiteral((float) + doubleOperation(left.doubleValue(), right.doubleValue(), operation)); + case DECIMAL -> context.values().createLiteral( + decimalOperation(left.decimalValue(), right.decimalValue(), operation)); + case INTEGER -> context.values().createLiteral(switch (operation) { + case ADD -> left.literal().integerValue().add(right.literal().integerValue()); + case SUBTRACT -> left.literal().integerValue().subtract(right.literal().integerValue()); + case MULTIPLY -> left.literal().integerValue().multiply(right.literal().integerValue()); + case DIVIDE -> throw new IllegalStateException("Integer division must promote to decimal"); + }); + }; + } + + private static double doubleOperation(double left, double right, Operation operation) { + return switch (operation) { + case ADD -> left + right; + case SUBTRACT -> left - right; + case MULTIPLY -> left * right; + case DIVIDE -> left / right; + }; + } + + private static BigDecimal decimalOperation( + BigDecimal left, + BigDecimal right, + Operation operation) { + return switch (operation) { + case ADD -> left.add(right); + case SUBTRACT -> left.subtract(right); + case MULTIPLY -> left.multiply(right); + case DIVIDE -> left.divide(right, MathContext.DECIMAL128); + }; + } + + private static DatatypeValue unaryMinus( + NumericLiteral value, + NativeEvaluationContext context) { + return switch (value.kind()) { + case DOUBLE -> context.values().createLiteral(-value.literal().doubleValue()); + case FLOAT -> context.values().createLiteral(-value.literal().floatValue()); + case DECIMAL -> context.values().createLiteral(value.decimalValue().negate()); + case INTEGER -> context.values().createLiteral(value.literal().integerValue().negate()); + }; + } + + private static DatatypeValue absolute( + NumericLiteral value, + NativeEvaluationContext context) { + return switch (value.kind()) { + case DOUBLE -> context.values().createLiteral(Math.abs(value.literal().doubleValue())); + case FLOAT -> context.values().createLiteral(Math.abs(value.literal().floatValue())); + case DECIMAL -> context.values().createLiteral(value.decimalValue().abs()); + case INTEGER -> context.values().createLiteral(value.literal().integerValue().abs()); + }; + } + + private static DatatypeValue rounded( + NumericLiteral value, + NativeEvaluationContext context, + Rounding rounding) { + return switch (value.kind()) { + case DOUBLE -> context.values().createLiteral(rounding.apply(value.literal().doubleValue())); + case FLOAT -> context.values().createLiteral((float) rounding.apply(value.literal().floatValue())); + case DECIMAL -> context.values().createLiteral( + rounding.apply(value.decimalValue())); + case INTEGER -> value.literal(); + }; + } + + private static DatatypeValue stringLength(StrLenAst expression, NativeEvaluationContext context) { + String text = context.stringLiteral(expression.argument()).getLabel(); + return context.values().createLiteral(text.codePointCount(0, text.length())); + } + + private static XMLGregorianCalendar calendar( + fr.inria.corese.core.next.query.impl.sparql.ast.TermAst expression, + NativeEvaluationContext context) { + return NativeTemporalExpressionEvaluator.calendar(expression, context); + } + + private static DatatypeValue seconds( + XMLGregorianCalendar calendar, + NativeEvaluationContext context) { + BigDecimal seconds = BigDecimal.valueOf(calendar.getSecond()); + if (calendar.getFractionalSecond() != null) { + seconds = seconds.add(calendar.getFractionalSecond()); + } + return context.values().createLiteral(seconds); + } + + private static NumericLiteral numericLiteral(DatatypeValue value) { + if (!(value instanceof Literal literal) || !literal.isNumber()) { + throw new QueryEvaluationException("Expected a numeric RDF literal"); + } + return new NumericLiteral(literal, NumericKind.of(literal)); + } + + private static UnsupportedQueryFeatureException unsupported(NumericExpressionAst expression) { + return new UnsupportedQueryFeatureException( + "Numeric expression is not supported yet: " + expression.getClass().getSimpleName()); + } + + private enum Operation { + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE + } + + private enum NumericKind { + INTEGER, + DECIMAL, + FLOAT, + DOUBLE; + + static NumericKind of(Literal literal) { + if (!(literal.getCoreDatatype() instanceof XSDDatatype datatype)) { + throw new QueryEvaluationException("Expected an XML Schema numeric datatype"); + } + return switch (datatype) { + case DOUBLE -> DOUBLE; + case FLOAT -> FLOAT; + case DECIMAL -> DECIMAL; + default -> INTEGER; + }; + } + + static NumericKind promote(NumericKind left, NumericKind right, Operation operation) { + if (left == DOUBLE || right == DOUBLE) { + return DOUBLE; + } + if (left == FLOAT || right == FLOAT) { + return FLOAT; + } + if (left == DECIMAL || right == DECIMAL || operation == Operation.DIVIDE) { + return DECIMAL; + } + return INTEGER; + } + } + + private record NumericLiteral(Literal literal, NumericKind kind) { + + BigDecimal decimalValue() { + return kind == NumericKind.INTEGER + ? new BigDecimal(literal.integerValue()) + : literal.decimalValue(); + } + + double doubleValue() { + return literal.doubleValue(); + } + } + + private enum Rounding { + CEILING { + @Override + double apply(double value) { + return Math.ceil(value); + } + + @Override + BigDecimal apply(BigDecimal value) { + return value.setScale(0, RoundingMode.CEILING); + } + }, + FLOOR { + @Override + double apply(double value) { + return Math.floor(value); + } + + @Override + BigDecimal apply(BigDecimal value) { + return value.setScale(0, RoundingMode.FLOOR); + } + }, + NEAREST { + @Override + double apply(double value) { + return Math.floor(value + 0.5d); + } + + @Override + BigDecimal apply(BigDecimal value) { + BigDecimal floor = value.setScale(0, RoundingMode.FLOOR); + return value.subtract(floor).compareTo(HALF) >= 0 + ? floor.add(BigDecimal.ONE) + : floor; + } + }; + + private static final BigDecimal HALF = new BigDecimal("0.5"); + + abstract double apply(double value); + + abstract BigDecimal apply(BigDecimal value); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeStringExpressionEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeStringExpressionEvaluator.java new file mode 100644 index 000000000..3447da9a7 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeStringExpressionEvaluator.java @@ -0,0 +1,324 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryConstraintAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BinaryRegexAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ConcatAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.EncodeForUriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.LangAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.LcaseAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.LiteralExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.Md5Ast; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ReplaceAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.Sha1Ast; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.Sha256Ast; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.Sha384Ast; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.Sha512Ast; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrAfterAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrBeforeAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrDtAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrLangAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.StrUuidAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.SubstrAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.TrinaryRegexAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.TzAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.UcaseAst; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import java.util.regex.Pattern; + +/** Evaluates SPARQL expressions that produce string literals. */ +final class NativeStringExpressionEvaluator { + + private NativeStringExpressionEvaluator() { + } + + static DatatypeValue evaluate( + LiteralExpressionAst expression, + NativeEvaluationContext context) { + return switch (expression) { + case StrAst unary -> context.values().createLiteral( + context.required(unary.argument()).stringValue()); + case StrAfterAst binary -> after(binary, context); + case StrBeforeAst binary -> before(binary, context); + case StrLangAst binary -> stringWithLanguage(binary, context); + case StrDtAst binary -> stringWithDatatype(binary, context); + case LangAst unary -> language(unary.argument(), context); + case LcaseAst unary -> changeCase(unary.argument(), context, false); + case UcaseAst unary -> changeCase(unary.argument(), context, true); + case EncodeForUriAst unary -> context.values().createLiteral( + encodeForUri(context.stringLiteral(unary.argument()).getLabel())); + case ConcatAst concat -> concat(concat.arguments(), context); + case ReplaceAst replace -> replace(replace, context); + case SubstrAst substring -> substring(substring, context); + case Md5Ast unary -> digest("MD5", unary.argument(), context); + case Sha1Ast unary -> digest("SHA-1", unary.argument(), context); + case Sha256Ast unary -> digest("SHA-256", unary.argument(), context); + case Sha384Ast unary -> digest("SHA-384", unary.argument(), context); + case Sha512Ast unary -> digest("SHA-512", unary.argument(), context); + case StrUuidAst ignored -> context.values().createLiteral(UUID.randomUUID().toString()); + case TzAst unary -> context.values().createLiteral( + NativeTemporalExpressionEvaluator.timezoneLabel( + NativeTemporalExpressionEvaluator.calendar(unary.argument(), context))); + default -> throw unsupported(expression); + }; + } + + static boolean startsWith(BinaryConstraintAst expression, NativeEvaluationContext context) { + StringOperands operands = stringOperands(expression, context); + return operands.left().startsWith(operands.right()); + } + + static boolean endsWith(BinaryConstraintAst expression, NativeEvaluationContext context) { + StringOperands operands = stringOperands(expression, context); + return operands.left().endsWith(operands.right()); + } + + static boolean contains(BinaryConstraintAst expression, NativeEvaluationContext context) { + StringOperands operands = stringOperands(expression, context); + return operands.left().contains(operands.right()); + } + + static boolean regex(BinaryRegexAst expression, NativeEvaluationContext context) { + String value = context.stringLiteral(expression.getString()).getLabel(); + String pattern = context.stringLiteral(expression.getPattern()).getLabel(); + return compilePattern(pattern, "").matcher(value).find(); + } + + static boolean regex(TrinaryRegexAst expression, NativeEvaluationContext context) { + String value = context.stringLiteral(expression.getString()).getLabel(); + String pattern = context.stringLiteral(expression.getPattern()).getLabel(); + String flags = context.stringLiteral(expression.getFlags()).getLabel(); + return compilePattern(pattern, flags).matcher(value).find(); + } + + private static StringOperands stringOperands( + BinaryConstraintAst expression, + NativeEvaluationContext context) { + Literal left = context.stringLiteral(expression.getLeftArgument()); + Literal right = context.stringLiteral(expression.getRightArgument()); + ensureCompatibleArguments(left, right); + return new StringOperands(left.getLabel(), right.getLabel(), left); + } + + private static DatatypeValue before( + BinaryConstraintAst expression, + NativeEvaluationContext context) { + StringOperands operands = stringOperands(expression, context); + int separator = operands.left().indexOf(operands.right()); + String result = separator < 0 ? "" : operands.left().substring(0, separator); + return stringLike(operands.source(), result, context); + } + + private static DatatypeValue after( + BinaryConstraintAst expression, + NativeEvaluationContext context) { + StringOperands operands = stringOperands(expression, context); + int separator = operands.left().indexOf(operands.right()); + String result = separator < 0 + ? "" + : operands.left().substring(separator + operands.right().length()); + return stringLike(operands.source(), result, context); + } + + private static DatatypeValue stringWithLanguage( + BinaryConstraintAst expression, + NativeEvaluationContext context) { + String label = context.stringLiteral(expression.getLeftArgument()).getLabel(); + String language = context.stringLiteral(expression.getRightArgument()).getLabel(); + return context.values().createLiteral(label, language); + } + + private static DatatypeValue stringWithDatatype( + BinaryConstraintAst expression, + NativeEvaluationContext context) { + String label = context.stringLiteral(expression.getLeftArgument()).getLabel(); + DatatypeValue datatype = context.required(expression.getRightArgument()); + if (!datatype.isIRI()) { + throw new QueryEvaluationException("STRDT expects an IRI as its second argument"); + } + return context.values().createLiteral(label, context.values().createIRI(datatype.stringValue())); + } + + private static DatatypeValue language(TermAst expression, NativeEvaluationContext context) { + Literal literal = context.literal(expression); + return context.values().createLiteral(literal.getLanguage().orElse("")); + } + + private static DatatypeValue changeCase( + TermAst expression, + NativeEvaluationContext context, + boolean upperCase) { + Literal source = context.stringLiteral(expression); + String result = upperCase + ? source.getLabel().toUpperCase(Locale.ROOT) + : source.getLabel().toLowerCase(Locale.ROOT); + return stringLike(source, result, context); + } + + private static DatatypeValue concat(List arguments, NativeEvaluationContext context) { + StringBuilder result = new StringBuilder(); + String commonLanguage = null; + boolean preserveLanguage = !arguments.isEmpty(); + for (TermAst argument : arguments) { + Literal literal = context.stringLiteral(argument); + result.append(literal.getLabel()); + String language = literal.getLanguage().orElse(null); + if (commonLanguage == null) { + commonLanguage = language; + } else if (language == null || !commonLanguage.equalsIgnoreCase(language)) { + preserveLanguage = false; + } + if (language == null) { + preserveLanguage = false; + } + } + return preserveLanguage + ? context.values().createLiteral(result.toString(), commonLanguage) + : context.values().createLiteral(result.toString()); + } + + private static DatatypeValue replace(ReplaceAst expression, NativeEvaluationContext context) { + Literal source = context.stringLiteral(expression.getString()); + String pattern = context.stringLiteral(expression.getPattern()).getLabel(); + String replacement = context.stringLiteral(expression.getReplacement()).getLabel(); + String flags = expression.hasFlags() + ? context.stringLiteral(expression.getFlags()).getLabel() + : ""; + String result = compilePattern(pattern, flags) + .matcher(source.getLabel()) + .replaceAll(replacement); + return stringLike(source, result, context); + } + + private static DatatypeValue substring(SubstrAst expression, NativeEvaluationContext context) { + Literal source = context.stringLiteral(expression.getString()); + double start = NativeNumericExpressionEvaluator.numericDouble( + context.required(expression.getStart())); + Double length = expression.getLength() == null + ? null + : NativeNumericExpressionEvaluator.numericDouble( + context.required(expression.getLength())); + String result = codePointSubstring(source.getLabel(), start, length); + return stringLike(source, result, context); + } + + private static String codePointSubstring(String value, double start, Double length) { + if (Double.isNaN(start) || start == Double.POSITIVE_INFINITY + || (length != null && Double.isNaN(length))) { + return ""; + } + int codePointCount = value.codePointCount(0, value.length()); + long roundedStart = xpathRound(start); + long firstPosition = Math.max(1L, roundedStart); + long endPosition = length == null + ? codePointCount + 1L + : saturatedAdd(roundedStart, xpathRound(length)); + long exclusivePosition = Math.min(codePointCount + 1L, endPosition); + if (exclusivePosition <= firstPosition || firstPosition > codePointCount) { + return ""; + } + int from = value.offsetByCodePoints(0, Math.toIntExact(firstPosition - 1L)); + int to = value.offsetByCodePoints(0, Math.toIntExact(exclusivePosition - 1L)); + return value.substring(from, to); + } + + private static long xpathRound(double value) { + if (value == Double.NEGATIVE_INFINITY) { + return Long.MIN_VALUE; + } + if (value == Double.POSITIVE_INFINITY) { + return Long.MAX_VALUE; + } + return (long) Math.floor(value + 0.5d); + } + + private static long saturatedAdd(long left, long right) { + try { + return Math.addExact(left, right); + } catch (ArithmeticException overflow) { + return right < 0 ? Long.MIN_VALUE : Long.MAX_VALUE; + } + } + + private static DatatypeValue digest( + String algorithm, + TermAst expression, + NativeEvaluationContext context) { + String value = context.stringLiteral(expression).getLabel(); + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + byte[] bytes = digest.digest(value.getBytes(StandardCharsets.UTF_8)); + return context.values().createLiteral(HexFormat.of().formatHex(bytes)); + } catch (NoSuchAlgorithmException exception) { + throw new QueryEvaluationException("Digest algorithm unavailable: " + algorithm, exception); + } + } + + private static Pattern compilePattern(String expression, String flags) { + return Pattern.compile(expression, regexOptions(flags)); + } + + private static int regexOptions(String flags) { + int options = 0; + if (flags.contains("i")) { + options |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE; + } + if (flags.contains("m")) { + options |= Pattern.MULTILINE; + } + if (flags.contains("s")) { + options |= Pattern.DOTALL; + } + if (flags.contains("x")) { + options |= Pattern.COMMENTS; + } + return options; + } + + private static String encodeForUri(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8) + .replace("+", "%20") + .replace("%7E", "~"); + } + + private static DatatypeValue stringLike( + Literal source, + String value, + NativeEvaluationContext context) { + return source.getLanguage() + .map(language -> context.values().createLiteral(value, language)) + .orElseGet(() -> context.values().createLiteral(value)); + } + + private static void ensureCompatibleArguments(Literal left, Literal right) { + String leftLanguage = left.getLanguage().orElse(null); + String rightLanguage = right.getLanguage().orElse(null); + boolean compatible = rightLanguage == null + || leftLanguage != null && leftLanguage.equalsIgnoreCase(rightLanguage); + if (!compatible) { + throw new QueryEvaluationException("String arguments have incompatible language tags"); + } + } + + private static UnsupportedQueryFeatureException unsupported( + LiteralExpressionAst expression) { + return new UnsupportedQueryFeatureException( + "String expression is not supported yet: " + expression.getClass().getSimpleName()); + } + + private record StringOperands(String left, String right, Literal source) { + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeTemporalExpressionEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeTemporalExpressionEvaluator.java new file mode 100644 index 000000000..9aa3b6068 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeTemporalExpressionEvaluator.java @@ -0,0 +1,86 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.data.api.vocabulary.XSD; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; +import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.NowAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.TimezoneAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.XsdDateTimeExpressionAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.XsdDayTimeDurationExpressionAst; + +import javax.xml.datatype.DatatypeConstants; +import javax.xml.datatype.XMLGregorianCalendar; + +/** Evaluates date-time and timezone expressions. */ +final class NativeTemporalExpressionEvaluator { + + private NativeTemporalExpressionEvaluator() { + } + + static DatatypeValue evaluateDateTime( + XsdDateTimeExpressionAst expression, + NativeEvaluationContext context) { + if (expression instanceof NowAst) { + return context.values().createLiteral(context.queryEvaluationTime()); + } + throw new UnsupportedQueryFeatureException( + "Date-time expression is not supported yet: " + + expression.getClass().getSimpleName()); + } + + static DatatypeValue evaluateDuration( + XsdDayTimeDurationExpressionAst expression, + NativeEvaluationContext context) { + if (expression instanceof TimezoneAst timezone) { + return timezoneDuration(calendar(timezone.argument(), context), context); + } + throw new UnsupportedQueryFeatureException( + "Duration expression is not supported yet: " + + expression.getClass().getSimpleName()); + } + + static XMLGregorianCalendar calendar(TermAst expression, NativeEvaluationContext context) { + DatatypeValue value = context.required(expression); + if (value instanceof Literal literal) { + return literal.calendarValue(); + } + throw new QueryEvaluationException("Date/time function expects a calendar literal"); + } + + static String timezoneLabel(XMLGregorianCalendar calendar) { + int minutes = calendar.getTimezone(); + if (minutes == DatatypeConstants.FIELD_UNDEFINED) { + return ""; + } + if (minutes == 0) { + return "Z"; + } + int absoluteMinutes = Math.abs(minutes); + return "%s%02d:%02d".formatted( + minutes < 0 ? "-" : "+", + absoluteMinutes / 60, + absoluteMinutes % 60); + } + + private static DatatypeValue timezoneDuration( + XMLGregorianCalendar calendar, + NativeEvaluationContext context) { + int minutes = calendar.getTimezone(); + if (minutes == DatatypeConstants.FIELD_UNDEFINED) { + throw new QueryEvaluationException( + "TIMEZONE expects a date/time value with a timezone"); + } + int absoluteMinutes = Math.abs(minutes); + String sign = minutes < 0 ? "-" : ""; + String lexical = minutes == 0 + ? "PT0S" + : "%sPT%dH%dM".formatted( + sign, + absoluteMinutes / 60, + absoluteMinutes % 60); + return context.values().createLiteral(lexical, XSD.xsdDayTimeDuration.getIRI()); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeValueComparison.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeValueComparison.java new file mode 100644 index 000000000..be538817a --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeValueComparison.java @@ -0,0 +1,78 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.literal.XSDDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; + +import javax.xml.datatype.DatatypeConstants; + +/** Implements SPARQL relational comparison without RDF ordering tie-breakers. */ +final class NativeValueComparison { + + private NativeValueComparison() { + } + + static int compare(DatatypeValue left, DatatypeValue right) { + if (left.isNumber() && right.isNumber() + && left instanceof Literal leftLiteral + && right instanceof Literal rightLiteral) { + return compareNumbers(leftLiteral, rightLiteral); + } + if (left instanceof Literal leftLiteral && right instanceof Literal rightLiteral) { + return compareLiterals(leftLiteral, rightLiteral); + } + throw incomparable(); + } + + private static int compareNumbers(Literal left, Literal right) { + if (isFloatingPoint(left) || isFloatingPoint(right)) { + return compareFloatingPoint(left.doubleValue(), right.doubleValue()); + } + return left.decimalValue().compareTo(right.decimalValue()); + } + + private static int compareLiterals(Literal left, Literal right) { + if (left.getCoreDatatype() == XSDDatatype.STRING + && right.getCoreDatatype() == XSDDatatype.STRING) { + return left.getLabel().compareTo(right.getLabel()); + } + if (isComparableCalendar(left, right)) { + int comparison = left.calendarValue().compare(right.calendarValue()); + if (comparison != DatatypeConstants.INDETERMINATE) { + return comparison; + } + } + throw incomparable(); + } + + private static int compareFloatingPoint(double left, double right) { + if (Double.isNaN(left) || Double.isNaN(right)) { + throw new QueryEvaluationException("NaN is not order-comparable"); + } + if (left < right) { + return -1; + } + return left > right ? 1 : 0; + } + + private static boolean isFloatingPoint(Literal literal) { + return literal.getCoreDatatype() == XSDDatatype.FLOAT + || literal.getCoreDatatype() == XSDDatatype.DOUBLE; + } + + private static boolean isComparableCalendar(Literal left, Literal right) { + if (left.getCoreDatatype() != right.getCoreDatatype()) { + return false; + } + return left.getCoreDatatype() instanceof XSDDatatype datatype && switch (datatype) { + case DATE, DATETIME, TIME -> true; + default -> false; + }; + } + + private static QueryEvaluationException incomparable() { + return new QueryEvaluationException( + "RDF values are not order-comparable in a SPARQL expression"); + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java deleted file mode 100644 index e2bf9ac05..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextDatatypeValueAdapter.java +++ /dev/null @@ -1,91 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.query.impl.kgram.api.core.DatatypeValue; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException; - -/** - * Adapts a runtime {@link IDatatype} to the Corese-next {@link DatatypeValue} API. - */ -public record NextDatatypeValueAdapter(IDatatype delegate) implements DatatypeValue { - - public static DatatypeValue ofNullable(IDatatype dt) { - return dt == null ? null : new NextDatatypeValueAdapter(dt); - } - - @Override - public String getLabel() { - return delegate.getLabel(); - } - - @Override - public Object getValue() { - return delegate.getValue(); - } - - @Override - public String getDatatypeURI() { - return delegate.getDatatypeURI(); - } - - @Override - public boolean isTrue() { - try { - return delegate.isTrue(); - } catch (CoreseDatatypeException e) { - return false; - } - } - - @Override - public boolean equalsWE(DatatypeValue other) throws CoreseDatatypeException { - IDatatype o = unwrap(other); - return delegate.equalsWE(o); - } - - @Override - public int compare(DatatypeValue other) throws CoreseDatatypeException { - IDatatype o = unwrap(other); - return delegate.compare(o); - } - - @Override - public int intValue() { - return delegate.intValue(); - } - - @Override - public double doubleValue() { - return delegate.doubleValue(); - } - - @Override - public boolean isNumber() { - return delegate.isNumber(); - } - - @Override - public boolean isLiteral() { - return delegate.isLiteral(); - } - - @Override - public boolean isURI() { - return delegate.isURI(); - } - - @Override - public boolean isBlank() { - return delegate.isBlank(); - } - - private static IDatatype unwrap(DatatypeValue other) throws CoreseDatatypeException { - if (other instanceof NextDatatypeValueAdapter(IDatatype delegate1)) { - return delegate1; - } - if (other instanceof IDatatype id) { - return id; - } - throw new CoreseDatatypeException("Incompatible DatatypeValue: " + other); - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java index e46d30b73..6e67e4a00 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NextFilterFromAst.java @@ -3,19 +3,19 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; import fr.inria.corese.core.next.query.impl.kgram.api.core.Expr; import fr.inria.corese.core.next.query.impl.kgram.api.core.Filter; -import fr.inria.corese.core.sparql.triple.parser.Expression; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.BoundAst; +import fr.inria.corese.core.next.query.impl.sparql.parser.semantic.support.VariableScopeAnalyzer; import java.util.List; import java.util.Optional; /** - * {@link Filter} view for an {@link AstBackedExpr}, delegating metadata to the underlying - * {@link fr.inria.corese.core.sparql.triple.parser.Expression} - * while exposing the Corese-next {@link Expr} API. + * {@link Filter} view exposing native AST metadata through the Corese-next {@link Expr} API. */ public final class NextFilterFromAst implements Filter { private final AstBackedExpr owner; + private final VariableScopeAnalyzer variables = new VariableScopeAnalyzer(); NextFilterFromAst(AstBackedExpr owner) { this.owner = owner; @@ -23,12 +23,12 @@ public final class NextFilterFromAst implements Filter { @Override public List getVariables() { - return owner.asTripleParserExpression().getVariables(); + return List.copyOf(variables.collectReferencedVariables(owner.sourceAst().orElseThrow())); } @Override public List getVariables(boolean excludeLocal) { - return owner.asTripleParserExpression().getVariables(excludeLocal); + return getVariables(); } @Override @@ -37,33 +37,33 @@ public Expr getExp() { } @Override - public Expression getFilterExpression() { - return owner.asTripleParserExpression(); + public TermAst getFilterExpression() { + return owner.sourceAst().orElseThrow(); } @Override public boolean isBound() { - return owner.asTripleParserExpression().isBound(); + return owner.contains(BoundAst.class); } @Override public boolean isAggregate() { - return owner.asTripleParserExpression().isAggregate(); + return owner.isAggregate(); } @Override public boolean isRecAggregate() { - return owner.asTripleParserExpression().isRecAggregate(); + return owner.isRecAggregate(); } @Override public boolean isFunctional() { - return owner.asTripleParserExpression().isFunctional(); + return owner.oper() == fr.inria.corese.core.next.query.impl.kgram.api.core.ExprType.UNNEST; } @Override public boolean isRecExist() { - return owner.asTripleParserExpression().isRecExist(); + return owner.isRecExist(); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java deleted file mode 100644 index b5fe29d01..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpression.java +++ /dev/null @@ -1,554 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.data.spi.io.IOConstants; -import fr.inria.corese.core.next.query.api.exception.UnsupportedQueryFeatureException; -import fr.inria.corese.core.next.query.impl.kgram.api.core.ExprType; -import fr.inria.corese.core.next.query.impl.sparql.ast.*; -import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; -import fr.inria.corese.core.next.query.impl.kgram.api.core.Filter; -import fr.inria.corese.core.next.common.text.RdfText; -import fr.inria.corese.core.sparql.datatype.RDF; -import fr.inria.corese.core.next.data.spi.term.IRIUtils; -import fr.inria.corese.core.sparql.triple.cst.Keyword; -import fr.inria.corese.core.sparql.triple.parser.*; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * Converts Corese-next {@link TermAst} nodes (including {@link ConstraintAst}) into - * {@link Expression} trees for the SPARQL interpreter, consumable from KGRAM “next” via - * {@link fr.inria.corese.core.next.query.impl.kgram.api.core.Filter} / {@link AstBackedExpr}. - * - */ -public final class SparqlAstToExpression { - - private static final ThreadLocal CURRENT_PROLOGUE = new ThreadLocal<>(); - - public static QueryPrologueAst getCurrentPrologue() { - return CURRENT_PROLOGUE.get(); - } - - public static void setCurrentPrologue(QueryPrologueAst prologue) { - if (prologue == null) { - CURRENT_PROLOGUE.remove(); - } else { - CURRENT_PROLOGUE.set(prologue); - } - } - - private SparqlAstToExpression() { - } - - /** - * Converts any {@link TermAst} (variable, literal, IRI, or constraint expression) to {@link Expression}. - */ - public static Expression convert(TermAst term) { - return switch (term) { - case VarAst(String name) -> Variable.create(name); - case LiteralAst(String lexical, String lang, String datatype) -> - literalToConstant(lexical, lang, datatype); - case IriAst(String raw) -> iriToConstant(raw); - case ConstraintAst c -> constraintToExpression(c); - default -> throw new IllegalStateException("Unhandled TermAst: " + term.getClass()); - }; - } - - /** - * Converts a {@code FILTER} clause, compiling the graph pattern of any embedded - * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. - */ - public static Filter toNextFilter(FilterAst filterClause, WhereCompiler whereCompiler) { - Objects.requireNonNull(filterClause, "filterClause"); - return toNextFilter(filterClause.operator(), whereCompiler); - } - - /** - * Converts a filter {@link TermAst} to an {@link Expression}, then wraps it as a - * {@link Filter} with {@link Filter#coreseNextSource()} set to {@code filterExpression}. - * Filters containing {@code EXISTS} / {@code NOT EXISTS} require - * {@link #toNextFilter(TermAst, WhereCompiler)} so their graph pattern can be compiled. - */ - public static Filter toNextFilter(TermAst filterExpression) { - return toNextFilter(filterExpression, null); - } - - /** - * Converts a filter {@link TermAst}, then compiles the graph pattern of every embedded - * {@code EXISTS} / {@code NOT EXISTS} with the given {@link WhereCompiler}. - * - */ - public static Filter toNextFilter(TermAst filterExpression, WhereCompiler whereCompiler) { - Expression exprTree = convert(filterExpression); - compileExists(exprTree, whereCompiler); - initializeExpList(exprTree); - AstBackedExpr expr = new AstBackedExpr(exprTree, Optional.of(filterExpression)); - return expr.getFilter(); - } - - /** - * Second pass: compiles the graph pattern carried by every {@link AstBackedExistTerm} of the - * expression tree. Mirrors {@code compileExist} of the historical pipeline. - */ - private static void compileExists(Expression expression, WhereCompiler whereCompiler) { - if (expression instanceof AstBackedExistTerm exist) { - if (whereCompiler == null) { - throw new IllegalArgumentException( - "EXISTS / NOT EXISTS filter conversion requires a WhereCompiler " - + "to compile its graph pattern"); - } - exist.setCompiledPattern(whereCompiler.compile(exist.patternAst())); - return; - } - if (expression instanceof Term term) { - for (Expression arg : term.getArgs()) { - compileExists(arg, whereCompiler); - } - } - } - - /** - * Initializes the {@code Expr} list ({@code lExp}) of every {@link Term} in the tree. - */ - private static void initializeExpList(Expression expression) { - if (expression instanceof Term term) { - for (Expression arg : term.getArgs()) { - initializeExpList(arg); - } - term.setExpList(new ArrayList<>(term.getArgs())); - } - } - - private static Constant literalToConstant(String lexical, String lang, String datatype) { - if (lang != null && !lang.isEmpty()) { - return Constant.create(unquoteLexical(lexical), RDF.rdflangString, lang); - } - if (datatype != null && !datatype.isEmpty()) { - return Constant.create(unquoteLexical(lexical), normalizeDatatypeIri(datatype), null); - } - return Constant.createString(unquoteLexical(lexical)); - } - - private static String unquoteLexical(String lexical) { - if (lexical.length() >= 2 && lexical.startsWith("\"")) { - if (lexical.endsWith("\"")) { - return lexical.substring(1, lexical.length() - 1); - } - int langIdx = lexical.lastIndexOf('"'); - if (langIdx > 0) { - return lexical.substring(1, langIdx); - } - } - return lexical; - } - - /** - * Normalizes a datatype IRI string from a literal AST term. - *

If enclosed in angle brackets (<...>), it is treated as an explicit IRI_REF and resolved against BASE. - * If it already contains a URI scheme delimiter (://), it is preserved as an absolute URI. - * Otherwise, it is treated as a prefixed name (e.g. {@code xsd:integer}) and expanded.

- */ - private static String normalizeDatatypeIri(String dt) { - if (dt == null) { - return null; - } - if (dt.startsWith("<") && dt.endsWith(">")) { - String inner = RdfText.stripAngleBrackets(dt); - return resolveRelativeIri(inner, CURRENT_PROLOGUE.get()); - } - if (dt.contains("://")) { - return dt; - } - return resolvePrefixedIri(dt, CURRENT_PROLOGUE.get()); - } - - /** - * Converts a raw IRI/PName token string into a runtime {@link Constant}. - *

Blank nodes are instantiated as blank constants; IRIs and prefixed names are resolved via - * the query prologue and instantiated as resource constants.

- */ - private static Constant iriToConstant(String rawIri) { - if (rawIri == null) { - return null; - } - if (rawIri.startsWith(IOConstants.BLANK_NODE_PREFIX)) { - return Constant.createBlank(rawIri.substring(IOConstants.BLANK_NODE_PREFIX.length())); - } - String resolved = resolveIri(rawIri, CURRENT_PROLOGUE.get()); - return Constant.createResource(resolved, resolved); - } - - /** - * Resolves a raw SPARQL term against the query prologue. - *
    - *
  • <...> explicit IRI references are resolved against {@code BASE} if relative.
  • - *
  • Blank node labels (_:...) are preserved as is.
  • - *
  • SPARQL keyword {@code a} resolves to {@code rdf:type}.
  • - *
  • Prefixed names (prefix:local) are expanded using declared prefixes or Corese default namespaces.
  • - *
- */ - static String resolveIri(String raw, QueryPrologueAst prologue) { - if (raw == null) { - return null; - } - if (raw.startsWith("<") && raw.endsWith(">")) { - String inner = RdfText.stripAngleBrackets(raw); - return resolveRelativeIri(inner, prologue); - } - if (raw.startsWith(IOConstants.BLANK_NODE_PREFIX)) { - return raw; - } - if (raw.equals("a")) { - return RDF.RDF + "type"; - } - return resolvePrefixedIri(raw, prologue); - } - - /** - * Expands a prefixed name (e.g. {@code ex:item}, {@code :item}) using the query prologue declarations, - * falling back to default namespaces from {@link NSManager}. - */ - private static String resolvePrefixedIri(String raw, QueryPrologueAst prologue) { - int colon = raw.indexOf(':'); - if (colon >= 0) { - String prefix = raw.substring(0, colon); - String local = raw.substring(colon + 1); - String ns = findNamespace(prefix, prologue); - if (ns != null) { - return ns + unescapePName(local); - } - } - return raw; - } - - /** - * Finds the namespace associated with a prefix in the query prologue, or in the global {@link NSManager}. - */ - private static String findNamespace(String prefix, QueryPrologueAst prologue) { - if (prologue != null && prologue.prefixDeclarations() != null) { - for (PrefixDeclarationAst decl : prologue.prefixDeclarations()) { - String p = decl.prefix(); - if (p != null && p.endsWith(":")) { - p = p.substring(0, p.length() - 1); - } - if (Objects.equals(p, prefix)) { - return RdfText.stripAngleBrackets(decl.namespace().raw()); - } - } - } - return NSManager.nsm().getNamespace(prefix); - } - - /** - * Resolves a relative IRI against the prologue {@code BASE} URI if present and absolute. - */ - private static String resolveRelativeIri(String clean, QueryPrologueAst prologue) { - if (prologue != null && prologue.baseIri() != null) { - String base = RdfText.stripAngleBrackets(prologue.baseIri().raw()); - if (IRIUtils.isAbsoluteIRI(base) && !IRIUtils.isAbsoluteIRI(clean)) { - return IRIUtils.resolveIRIAgainstBase(base, clean); - } - } - return clean; - } - - /** - * Unescapes SPARQL PN_LOCAL_ESC sequences (e.g. \: -> :, \. -> .) and Unicode escapes (\\uXXXX). - */ - private static String unescapePName(String local) { - if (local == null || !local.contains("\\")) { - return local; - } - StringBuilder sb = new StringBuilder(); - int i = 0; - int len = local.length(); - while (i < len) { - char c = local.charAt(i); - if (c == '\\' && i + 1 < len) { - i = appendEscaped(sb, local, i + 1); - } else { - sb.append(c); - i++; - } - } - return sb.toString(); - } - - private static int appendEscaped(StringBuilder sb, String str, int nextIdx) { - char next = str.charAt(nextIdx); - if (next == 'u' || next == 'U') { - int hexLen = (next == 'u') ? 4 : 8; - if (nextIdx + 1 + hexLen <= str.length()) { - String hex = str.substring(nextIdx + 1, nextIdx + 1 + hexLen); - int codePoint = Integer.parseInt(hex, 16); - sb.appendCodePoint(codePoint); - return nextIdx + 1 + hexLen; - } - } - sb.append(next); - return nextIdx + 1; - } - - private static Expression constraintToExpression(ConstraintAst constraint) { - Expression expr = operatorToExpression(constraint); - if (expr != null) { - return expr; - } - expr = stringAndHashFunctionToExpression(constraint); - if (expr != null) { - return expr; - } - expr = builtinToExpression(constraint); - if (expr != null) { - return expr; - } - throw new UnsupportedQueryFeatureException( - "Filter expression is not supported yet by the next pipeline: " - + constraint.getClass().getSimpleName()); - } - - private static Expression operatorToExpression(ConstraintAst constraint) { - return switch (constraint) { - case AndAst andAst -> - Term.create(KeywordHolder.SEAND, - convert(andAst.getLeftArgument()), convert(andAst.getRightArgument())); - case OrAst orAst -> - Term.create(KeywordHolder.SEOR, - convert(orAst.getLeftArgument()), convert(orAst.getRightArgument())); - case EqualsAst equalsAst -> - Term.create("=", convert(equalsAst.getLeftArgument()), convert(equalsAst.getRightArgument())); - case DifferentAst differentAst -> - Term.create("!=", convert(differentAst.getLeftArgument()), convert(differentAst.getRightArgument())); - case LowerThanAst lowerThanAst -> - Term.create("<", convert(lowerThanAst.getLeftArgument()), convert(lowerThanAst.getRightArgument())); - case LowerOrEqualThanAst lowerOrEqualThanAst -> - Term.create("<=", - convert(lowerOrEqualThanAst.getLeftArgument()), - convert(lowerOrEqualThanAst.getRightArgument())); - case GreaterThanAst greaterThanAst -> - Term.create(">", - convert(greaterThanAst.getLeftArgument()), - convert(greaterThanAst.getRightArgument())); - case GreaterOrEqualThanAst greaterOrEqualThanAst -> - Term.create(">=", - convert(greaterOrEqualThanAst.getLeftArgument()), - convert(greaterOrEqualThanAst.getRightArgument())); - case AddAst addAst -> - Term.create("+", convert(addAst.getLeftArgument()), convert(addAst.getRightArgument())); - case SubtractAst subtractAst -> - Term.create("-", convert(subtractAst.getLeftArgument()), convert(subtractAst.getRightArgument())); - case MultiplyAst multiplyAst -> - Term.create("*", convert(multiplyAst.getLeftArgument()), convert(multiplyAst.getRightArgument())); - case DivideAst divideAst -> - Term.create("/", convert(divideAst.getLeftArgument()), convert(divideAst.getRightArgument())); - case UnaryPlusAst unaryPlusAst -> - Term.create("+", convert(unaryPlusAst.argument())); - case UnaryMinusAst unaryMinusAst -> - Term.create("-", convert(unaryMinusAst.argument())); - case BooleanNotAst booleanNotAst -> - notTerm(convert(booleanNotAst.argument())); - default -> null; - }; - } - - private static Expression stringAndHashFunctionToExpression(ConstraintAst constraint) { - return switch (constraint) { - case StrStartsAst strStartsAst -> - functionTerm("strstarts", - convert(strStartsAst.getLeftArgument()), - convert(strStartsAst.getRightArgument())); - case StrEndsAst strEndsAst -> - functionTerm("strends", - convert(strEndsAst.getLeftArgument()), - convert(strEndsAst.getRightArgument())); - case ContainsAst containsAst -> - functionTerm("contains", - convert(containsAst.getLeftArgument()), - convert(containsAst.getRightArgument())); - case StrBeforeAst strBeforeAst -> - functionTerm("strbefore", - convert(strBeforeAst.getLeftArgument()), - convert(strBeforeAst.getRightArgument())); - case StrAfterAst strAfterAst -> - functionTerm("strafter", - convert(strAfterAst.getLeftArgument()), - convert(strAfterAst.getRightArgument())); - case StrLangAst strLangAst -> - functionTerm("strlang", - convert(strLangAst.getLeftArgument()), - convert(strLangAst.getRightArgument())); - case StrDtAst strDtAst -> - functionTerm(Processor.STRDT, - convert(strDtAst.getLeftArgument()), - convert(strDtAst.getRightArgument())); - case IriFunctionAst iriFunctionAst -> - functionTerm("iri", convert(iriFunctionAst.argument())); - case LcaseAst lcaseAst -> - functionTerm("lcase", convert(lcaseAst.argument())); - case UcaseAst ucaseAst -> - functionTerm("ucase", convert(ucaseAst.argument())); - case EncodeForUriAst encodeForUriAst -> - functionTerm("encode_for_uri", convert(encodeForUriAst.argument())); - case Md5Ast md5Ast -> - functionTerm("md5", convert(md5Ast.argument())); - case Sha1Ast sha1Ast -> - functionTerm("sha1", convert(sha1Ast.argument())); - case Sha256Ast sha256Ast -> - functionTerm("sha256", convert(sha256Ast.argument())); - case Sha384Ast sha384Ast -> - functionTerm("sha384", convert(sha384Ast.argument())); - case Sha512Ast sha512Ast -> - functionTerm("sha512", convert(sha512Ast.argument())); - case ReplaceAst replaceAst -> - replaceToTerm(replaceAst); - case SubstrAst substrAst -> - substrToTerm(substrAst); - case ConcatAst concatAst -> - variadicTerm("concat", concatAst.arguments()); - case BinaryRegexAst binaryRegexAst -> - regexTerm(convert(binaryRegexAst.getString()), convert(binaryRegexAst.getPattern())); - case TrinaryRegexAst trinaryRegexAst -> - regexTerm(convert(trinaryRegexAst.getString()), - convert(trinaryRegexAst.getPattern()), - convert(trinaryRegexAst.getFlags())); - case BnodeAst bnodeAst -> - bnodeToTerm(bnodeAst); - default -> null; - }; - } - - private static Expression builtinToExpression(ConstraintAst constraint) { - return switch (constraint) { - case BoundAst boundAst -> - functionTerm(Processor.BOUND, convert(boundAst.argument())); - case IsIriAst isIriAst -> - functionTerm("isIRI", convert(isIriAst.argument())); - case IsBlankAst isBlankAst -> - functionTerm("isBlank", convert(isBlankAst.argument())); - case IsLiteralAst isLiteralAst -> - functionTerm("isLiteral", convert(isLiteralAst.argument())); - case StrAst strAst -> - functionTerm("str", convert(strAst.argument())); - case LangAst langAst -> - functionTerm("lang", convert(langAst.argument())); - case DatatypeAst datatypeAst -> - functionTerm("datatype", convert(datatypeAst.argument())); - case SameTermAst sameTermAst -> - functionTerm("sameTerm", - convert(sameTermAst.getLeftArgument()), - convert(sameTermAst.getRightArgument())); - case LangMatchesAst langMatchesAst -> - functionTerm("langMatches", - convert(langMatchesAst.getLeftArgument()), - convert(langMatchesAst.getRightArgument())); - case FunctionCallAst(TermAst functionName, List arguments) -> - functionCallAst(functionName, arguments); - case CoalesceAst coalesceAst -> - variadicTerm(Processor.COALESCE, coalesceAst.arguments()); - case IfAst(TermAst condition, TermAst thenExpr, TermAst elseExpr) -> - Term.function(Processor.IF, convert(condition), convert(thenExpr), convert(elseExpr)); - case ExistsAst(GroupGraphPatternAst pattern) -> - new AstBackedExistTerm(pattern); - case NotExistsAst(GroupGraphPatternAst pattern) -> - notTerm(new AstBackedExistTerm(pattern)); - default -> null; - }; - } - - /** - * Builds a boolean negation with the runtime operator code already set. - * - *

The interpreter resolves operator codes in a later compilation phase - * ({@code Processor.type(Term, ASTQuery)}) that this bridge never runs, so {@code oper()} is set - * explicitly here. This keeps {@code !EXISTS { ... }} and {@code NOT EXISTS { ... }} - * equivalent to KGRAM.

- */ - private static Term notTerm(Expression expression) { - Term not = Term.create("!", expression); - not.setOper(ExprType.NOT); - return not; - } - - private static Term functionTerm(String name, Expression arg) { - Term t = Term.function(name); - t.add(arg); - return t; - } - - private static Term functionTerm(String name, Expression a1, Expression a2) { - Term t = Term.function(name); - t.add(a1); - t.add(a2); - return t; - } - - private static Term regexTerm(Expression s, Expression pattern) { - Term t = Term.function("regex"); - t.add(s); - t.add(pattern); - return t; - } - - private static Term regexTerm(Expression s, Expression pattern, Expression flags) { - Term t = Term.function("regex"); - t.add(s); - t.add(pattern); - t.add(flags); - return t; - } - - private static Term functionCallAst(TermAst functionName, List arguments) { - String name = SparqlBuiltinFunctionNameResolver.fromFunctionTerm(functionName); - Term t = Term.function(name); - for (TermAst arg : arguments) { - t.add(convert(arg)); - } - return t; - } - - private static Term variadicTerm(String name, List args) { - Term t = Term.function(name); - for (TermAst arg : args) { - t.add(convert(arg)); - } - return t; - } - - private static Term replaceToTerm(ReplaceAst r) { - Term t = - Term.function( - "replace", - convert(r.getString()), - convert(r.getPattern()), - convert(r.getReplacement())); - if (r.hasFlags()) { - t.add(convert(r.getFlags())); - } - return t; - } - - private static Term substrToTerm(SubstrAst s) { - if (s.getLength() != null) { - return Term.function( - "substr", - convert(s.getString()), - convert(s.getStart()), - convert(s.getLength())); - } - return Term.function("substr", convert(s.getString()), convert(s.getStart())); - } - - private static Term bnodeToTerm(BnodeAst b) { - if (b.getLabel() == null) { - return Term.function(Processor.BNODE); - } - return Term.function(Processor.BNODE, convert(b.getLabel())); - } - - private static final class KeywordHolder { - static final String SEAND = Keyword.SEAND; - static final String SEOR = Keyword.SEOR; - } -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java deleted file mode 100644 index 24ff3cad8..000000000 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlBuiltinFunctionNameResolver.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; -import fr.inria.corese.core.sparql.triple.parser.Processor; - -import static fr.inria.corese.core.next.common.text.RdfText.localNameFromIriToken; - -/** - * Maps SPARQL IRI / QName tokens (function position) to the names used in {@link Processor}'s operator table. - */ -public final class SparqlBuiltinFunctionNameResolver { - - private SparqlBuiltinFunctionNameResolver() { - } - - /** - * Resolves the function name from a term that must be an {@link IriAst}. - */ - public static String fromFunctionTerm(TermAst functionName) { - if (!(functionName instanceof IriAst(String raw))) { - throw new IllegalArgumentException("Function name must be IriAst, got " + functionName); - } - return localNameFromIriToken(raw); - } - - -} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java new file mode 100644 index 000000000..ca3cdcd51 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolver.java @@ -0,0 +1,132 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.common.text.RdfText; +import fr.inria.corese.core.next.data.api.vocabulary.RDF; +import fr.inria.corese.core.next.data.impl.namespace.PrefixHandler; +import fr.inria.corese.core.next.data.spi.io.IOConstants; +import fr.inria.corese.core.next.data.spi.term.IRIUtils; +import fr.inria.corese.core.next.query.impl.sparql.ast.PrefixDeclarationAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst; + +import java.util.Objects; + +/** Resolves SPARQL terms against one immutable query-prologue snapshot. */ +final class SparqlTermResolver { + + private final PrefixHandler prefixes; + private final String baseIri; + + SparqlTermResolver(QueryPrologueAst prologue) { + QueryPrologueAst effectivePrologue = prologue == null ? QueryPrologueAst.empty() : prologue; + this.prefixes = new PrefixHandler(true); + for (PrefixDeclarationAst declaration : effectivePrologue.prefixDeclarations()) { + prefixes.setPrefix(normalizePrefix(declaration.prefix()), + RdfText.stripAngleBrackets(declaration.namespace().raw())); + } + this.baseIri = effectivePrologue.baseIri().raw(); + } + + String resolveIri(String raw) { + if (raw == null) { + return null; + } + if (raw.startsWith("<") && raw.endsWith(">")) { + return resolveRelativeIri(RdfText.stripAngleBrackets(raw)); + } + if (raw.startsWith(IOConstants.BLANK_NODE_PREFIX)) { + return raw; + } + if (raw.equals("a")) { + return RDF.type.getIRI().stringValue(); + } + if (raw.contains("://") && IRIUtils.isAbsoluteIRI(raw)) { + return raw; + } + return resolvePrefixedIri(raw); + } + + String normalizeDatatypeIri(String datatype) { + if (datatype == null || datatype.isEmpty()) { + return null; + } + if (datatype.startsWith("<") && datatype.endsWith(">")) { + return resolveRelativeIri(RdfText.stripAngleBrackets(datatype)); + } + if (datatype.contains("://") + || (IRIUtils.isAbsoluteIRI(datatype) && !prefixes.hasPrefix(prefix(datatype)))) { + return datatype; + } + return resolvePrefixedIri(datatype); + } + + String unquoteLexical(String lexical) { + Objects.requireNonNull(lexical, "lexical"); + if (lexical.length() < 2 || !lexical.startsWith("\"")) { + return lexical; + } + if (lexical.endsWith("\"")) { + return lexical.substring(1, lexical.length() - 1); + } + int closingQuote = lexical.lastIndexOf('"'); + return closingQuote > 0 ? lexical.substring(1, closingQuote) : lexical; + } + + private String resolvePrefixedIri(String raw) { + int colon = raw.indexOf(':'); + if (colon < 0) { + return raw; + } + String namespace = prefixes.getNamespace(raw.substring(0, colon)); + return namespace == null ? raw : namespace + unescapePName(raw.substring(colon + 1)); + } + + private String resolveRelativeIri(String iri) { + if (IRIUtils.isAbsoluteIRI(baseIri) && !IRIUtils.isAbsoluteIRI(iri)) { + return IRIUtils.resolveIRIAgainstBase(baseIri, iri); + } + return iri; + } + + private static String normalizePrefix(String prefix) { + return prefix != null && prefix.endsWith(":") + ? prefix.substring(0, prefix.length() - 1) + : prefix; + } + + private static String prefix(String iri) { + int colon = iri.indexOf(':'); + return colon < 0 ? iri : iri.substring(0, colon); + } + + private static String unescapePName(String local) { + if (local == null || !local.contains("\\")) { + return local; + } + StringBuilder result = new StringBuilder(local.length()); + int index = 0; + while (index < local.length()) { + char character = local.charAt(index); + if (character == '\\' && index + 1 < local.length()) { + index = appendEscaped(result, local, index + 1); + } else { + result.append(character); + index++; + } + } + return result.toString(); + } + + private static int appendEscaped(StringBuilder result, String value, int escapeIndex) { + char escape = value.charAt(escapeIndex); + if (escape == 'u' || escape == 'U') { + int length = escape == 'u' ? 4 : 8; + if (escapeIndex + 1 + length <= value.length()) { + result.appendCodePoint(Integer.parseInt( + value.substring(escapeIndex + 1, escapeIndex + 1 + length), 16)); + return escapeIndex + 1 + length; + } + } + result.append(escape); + return escapeIndex + 1; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java index 66ba5a95e..60b774864 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/WhereCompiler.java @@ -8,6 +8,7 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.MinusAst; import fr.inria.corese.core.next.query.impl.sparql.ast.OptionalAst; import fr.inria.corese.core.next.query.impl.sparql.ast.PatternAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst; import fr.inria.corese.core.next.query.impl.sparql.ast.ServiceAst; import fr.inria.corese.core.next.query.impl.sparql.ast.TriplePatternAst; import fr.inria.corese.core.next.query.impl.sparql.ast.UnionAst; @@ -27,6 +28,24 @@ */ public final class WhereCompiler { + private final SparqlTermResolver termResolver; + + public WhereCompiler() { + this(QueryPrologueAst.empty()); + } + + private WhereCompiler(QueryPrologueAst prologue) { + termResolver = new SparqlTermResolver(prologue); + } + + WhereCompiler withPrologue(QueryPrologueAst prologue) { + return new WhereCompiler(prologue); + } + + SparqlTermResolver termResolver() { + return termResolver; + } + /** * Compiles a full SPARQL {@code WHERE} clause into the runtime body carried by a * KGRAM {@link Query}. @@ -100,14 +119,15 @@ private Exp compileBgp(BgpAst bgp) { } private Edge toEdge(TriplePatternAst triple) { - Node subject = CoreseAstQueryBuilder.toNode(triple.subject()); - Node predicate = CoreseAstQueryBuilder.toNode(CoreseAstQueryBuilder.simplePredicate(triple.predicate())); - Node object = CoreseAstQueryBuilder.toNode(triple.object()); + Node subject = CoreseAstQueryBuilder.toNode(triple.subject(), termResolver); + Node predicate = CoreseAstQueryBuilder.toNode( + CoreseAstQueryBuilder.simplePredicate(triple.predicate()), termResolver); + Node object = CoreseAstQueryBuilder.toNode(triple.object(), termResolver); return new AstBackedEdge(subject, predicate, object); } private Exp compileFilter(FilterAst filter) { - Filter nextFilter = SparqlAstToExpression.toNextFilter(filter, this); + Filter nextFilter = new AstBackedExpr(filter.operator(), this).getFilter(); return Exp.create(Type.FILTER, nextFilter); } @@ -143,8 +163,8 @@ private Exp compileMinus(MinusAst minus) { * Compiles {@code BIND(expression AS ?var)} into a KGRAM {@link Exp}. */ private Exp compileBind(BindAst bind) { - Filter filter = SparqlAstToExpression.toNextFilter(bind.expression(), this); - Node variable = CoreseAstQueryBuilder.toNode(bind.variable()); + Filter filter = new AstBackedExpr(bind.expression(), this).getFilter(); + Node variable = CoreseAstQueryBuilder.toNode(bind.variable(), termResolver); Exp exp = Exp.create(Type.BIND); exp.setFilter(filter); exp.setFunctional(filter.isFunctional()); @@ -156,7 +176,7 @@ private Exp compileBind(BindAst bind) { * Compiles {@code SERVICE { ... }} into a KGRAM {@link Exp}. */ private Exp compileService(ServiceAst service) { - Node endpoint = CoreseAstQueryBuilder.toNode(service.endpoint()); + Node endpoint = CoreseAstQueryBuilder.toNode(service.endpoint(), termResolver); Exp endpointNode = Exp.create(Type.NODE, endpoint); Query body = Query.create(compile(service.pattern())); body.setService(true); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java index 797f3f325..25aa803ee 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/CoreseBindingSet.java @@ -1,13 +1,14 @@ package fr.inria.corese.core.next.query.impl.sparql.execution; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueConverter; import fr.inria.corese.core.next.query.api.result.Binding; import fr.inria.corese.core.next.query.api.result.BindingSet; import fr.inria.corese.core.next.query.impl.kgram.core.Mapping; import fr.inria.corese.core.next.query.impl.result.CoreseBinding; import java.util.Iterator; +import java.util.Map; import java.util.Objects; import java.util.Set; @@ -17,7 +18,6 @@ public final class CoreseBindingSet implements BindingSet { private final Mapping mapping; - private static final CoreseValueConverter CONVERTER = new CoreseValueConverter(); public CoreseBindingSet(Mapping mapping) { this.mapping = Objects.requireNonNull(mapping, "mapping"); @@ -35,19 +35,22 @@ public boolean hasBinding(String name) { @Override public Value getValue(String name) { - if (this.hasBinding(name)) { - return CONVERTER.fromCoreseNode(this.mapping.getValue(name)); - } - return null; + return this.mapping.getValue(name) instanceof Value value ? value : null; } @Override - @SuppressWarnings("NullableProblems") public Iterator iterator() { return this.mapping.getMap().entrySet().stream() - .map(entry -> new CoreseBinding( - entry.getKey(), - CONVERTER.fromCoreseNode(entry.getValue()))) + .map(CoreseBindingSet::toBinding) .iterator(); } + + private static Binding toBinding( + Map.Entry entry) { + if (entry.getValue() instanceof Value value) { + return new CoreseBinding(entry.getKey(), value); + } + throw new IllegalStateException( + "Query binding is not backed by an RDF value: " + entry.getKey()); + } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java index 5f63423d5..334cb3751 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutor.java @@ -1,12 +1,11 @@ package fr.inria.corese.core.next.query.impl.sparql.execution; -import fr.inria.corese.core.next.data.api.term.BNode; +import fr.inria.corese.core.next.data.Values; import fr.inria.corese.core.next.data.api.term.IRI; -import fr.inria.corese.core.next.data.api.term.Literal; import fr.inria.corese.core.next.data.api.term.Resource; import fr.inria.corese.core.next.data.api.model.Statement; import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory; +import fr.inria.corese.core.next.data.api.factory.ValueFactory; import fr.inria.corese.core.next.query.api.dataset.Dataset; import fr.inria.corese.core.next.query.api.exception.QueryEvaluationException; import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; @@ -22,7 +21,6 @@ import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import fr.inria.corese.core.next.query.impl.sparql.ast.SelectQueryAst; import fr.inria.corese.core.next.query.impl.sparql.bridge.CoreseAstQueryBuilder; -import fr.inria.corese.core.next.query.impl.sparql.bridge.KgramNodeConverter; import fr.inria.corese.core.next.query.impl.kgram.api.core.Edge; import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; import fr.inria.corese.core.next.query.impl.kgram.core.Eval; @@ -40,11 +38,10 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.TimeoutException; /** * Internal orchestrator for the Corese-next SPARQL query path. @@ -61,18 +58,6 @@ */ public final class NextSparqlPipelineExecutor { - /** - * Shared scheduler used to enforce query timeouts. A single daemon thread is - * sufficient because the scheduled task is lightweight (set a flag and call - * {@link Eval#finish()}). - */ - private static final ScheduledExecutorService TIMEOUT_SCHEDULER = - Executors.newSingleThreadScheduledExecutor(r -> { - Thread t = new Thread(r, "sparql-query-timeout"); - t.setDaemon(true); - return t; - }); - private final StorageManager storage; private final SparqlParser parser; private final CoreseAstQueryBuilder queryBuilder; @@ -253,26 +238,35 @@ private Mappings evaluateCore(Eval eval, Query kgramQuery, Mapping initialMappin /** * Runs {@code eval.query()} with a cooperative timeout. * - *

A daemon-thread scheduler calls {@link Eval#finish()} after the deadline - * to signal the KGRAM engine to stop at the next opportunity. If the evaluation - * completes naturally before the deadline, the scheduler task is cancelled and - * results are returned normally.

+ *

The evaluation runs in a request-scoped virtual thread. At the deadline, + * {@link Eval#finish()} cooperatively stops KGRAM and the task is interrupted. + * No process-wide scheduler or mutable global timeout state is retained.

*/ private Mappings evaluateWithTimeout(Eval eval, Query kgramQuery, Mapping initialMapping, long timeoutMillis) { - AtomicBoolean timedOut = new AtomicBoolean(false); - ScheduledFuture canceller = TIMEOUT_SCHEDULER.schedule(() -> { - timedOut.set(true); - eval.finish(); - }, timeoutMillis, TimeUnit.MILLISECONDS); - + FutureTask evaluation = new FutureTask<>( + () -> evaluateCore(eval, kgramQuery, initialMapping)); + Thread thread = Thread.ofVirtual().name("sparql-query").start(evaluation); try { - Mappings result = evaluateCore(eval, kgramQuery, initialMapping); - if (timedOut.get()) { - throw new QueryTimeoutException(timeoutMillis); + return evaluation.get(timeoutMillis, TimeUnit.MILLISECONDS); + } catch (TimeoutException exception) { + eval.finish(); + evaluation.cancel(true); + throw new QueryTimeoutException(timeoutMillis); + } catch (InterruptedException exception) { + eval.finish(); + evaluation.cancel(true); + Thread.currentThread().interrupt(); + throw new QueryEvaluationException("Query evaluation was interrupted", exception); + } catch (ExecutionException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof QueryEvaluationException queryFailure) { + throw queryFailure; } - return result; + throw new QueryEvaluationException("Query evaluation failed", cause); } finally { - canceller.cancel(false); + if (thread.isAlive() && evaluation.isCancelled()) { + thread.interrupt(); + } } } @@ -297,7 +291,7 @@ private List buildConstructStatements(Query kgramQuery, Mappings mapp } constructTemplate.getEdgeList(templateEdges); - CoreseValueFactory factory = new CoreseValueFactory(); + ValueFactory factory = Values.factory(); List statements = new ArrayList<>(); for (Mapping mapping : mappings) { @@ -310,9 +304,9 @@ private List buildConstructStatements(Query kgramQuery, Mappings mapp continue; } - Value subject = kgramNodeToApiValue(subjectNode, factory); - Value predicate = kgramNodeToApiValue(predicateNode, factory); - Value object = kgramNodeToApiValue(objectNode, factory); + Value subject = kgramNodeToApiValue(subjectNode); + Value predicate = kgramNodeToApiValue(predicateNode); + Value object = kgramNodeToApiValue(objectNode); if (subject instanceof Resource s && predicate instanceof IRI p && object != null) { statements.add(factory.createStatement(s, p, object)); @@ -341,13 +335,13 @@ private Node resolveTemplateNode(Node templateNode, Mapping mapping) { /** * Converts a KGRAM constant {@link Node} to the corresponding API {@link Value}. * - *

Delegates to {@link KgramNodeConverter} so that this class does not depend on - * {@code IDatatype} directly.

- * * @return the API value, or {@code null} when the node kind is not supported */ - private Value kgramNodeToApiValue(Node node, CoreseValueFactory factory) { - return KgramNodeConverter.nodeToValue(node, factory); + private Value kgramNodeToApiValue(Node node) { + if (node.getDatatypeValue() instanceof Value value) { + return value; + } + return null; } // ------------------------------------------------------------------------- @@ -413,20 +407,6 @@ private Mapping buildInitialMapping(BindingSet bindings) { * @return a constant node, or {@code null} when the value type is not supported */ private Node valueToKgramNode(Value value) { - if (value instanceof IRI iri) { - return NodeImpl.forIRI(iri.stringValue()); - } else if (value instanceof BNode bNode) { - return NodeImpl.forBlank(bNode.getID()); - } else if (value instanceof Literal literal) { - String lang = literal.getLanguage().orElse(null); - if (lang != null && !lang.isEmpty()) { - return NodeImpl.forLiteral(literal.getLabel(), null, lang); - } - String datatypeUri = literal.getDatatype() != null - ? literal.getDatatype().stringValue() - : null; - return NodeImpl.forLiteral(literal.getLabel(), datatypeUri, null); - } - return null; + return value == null ? null : NodeImpl.forValue(value); } } diff --git a/src/main/java/fr/inria/corese/core/sparql/triple/function/core/Extern.java b/src/main/java/fr/inria/corese/core/sparql/triple/function/core/Extern.java index 7037ac3a1..019ad81e6 100644 --- a/src/main/java/fr/inria/corese/core/sparql/triple/function/core/Extern.java +++ b/src/main/java/fr/inria/corese/core/sparql/triple/function/core/Extern.java @@ -34,34 +34,29 @@ public IDatatype eval(Computer eval, Binding b, Environment env, Producer p) thr } Processor proc = getProcessor(); proc.compile(); - if (proc.getProcessor() instanceof FunctionEvaluator){ - FunctionEvaluator fe = (FunctionEvaluator) proc.getProcessor(); + if (proc.getProcessor() instanceof FunctionEvaluator fe){ fe.setProducer(p); fe.setEnvironment(env); } String name = proc.getMethod().getName(); try { - return (IDatatype) proc.getMethod().invoke(proc.getProcessor(), param); - } catch (IllegalArgumentException e) { - trace(e, "eval", name, param); - } catch (IllegalAccessException e) { + return (IDatatype) proc.getMethod().invoke(proc.getProcessor(), (Object[]) param); + } catch (IllegalArgumentException | IllegalAccessException | NullPointerException e) { trace(e, "eval", name, param); } catch (InvocationTargetException e) { - if (e.getCause() instanceof EngineException) { - throw (EngineException) e.getCause(); + if (e.getCause() instanceof EngineException engineException) { + throw engineException; } trace(e, "eval", name, param); - } catch (NullPointerException e) { - trace(e, "eval", name, param); } return null; } void trace(Exception e, String title, String name, IDatatype[] ldt){ - String str = ""; + StringBuilder str = new StringBuilder(); for (IDatatype dt : ldt) { - str += dt + " "; + str.append(dt).append(' '); } logger.error(title + " "+ name + " " + str, e); } diff --git a/src/main/java/fr/inria/corese/core/sparql/triple/function/script/JavaDScall.java b/src/main/java/fr/inria/corese/core/sparql/triple/function/script/JavaDScall.java index d6bbc6d6c..02ae58dae 100644 --- a/src/main/java/fr/inria/corese/core/sparql/triple/function/script/JavaDScall.java +++ b/src/main/java/fr/inria/corese/core/sparql/triple/function/script/JavaDScall.java @@ -23,11 +23,12 @@ * @author Olivier Corby, Wimmics INRIA I3S, 2017 * */ -public class JavaDScall extends JavaFunction { +@SuppressWarnings("java:S110") // Corese extension function hierarchy exceeds 5 parents by design +public final class JavaDScall extends JavaFunction { private static final Logger logger = LoggerFactory.getLogger(JavaDScall.class); - String javaName; + private String javaName; public JavaDScall() {} @@ -50,15 +51,14 @@ public IDatatype eval(Computer eval, Binding b, Environment env, Producer p) thr object = dt.getNodeObject(); } - Class[] types = new Class[param.length]; + Class[] types = new Class[param.length]; Arrays.fill(types, IDatatype.class); try { Method meth = object.getClass().getMethod(javaName, types); - Object obj = meth.invoke(object, param); - IDatatype res = DatatypeMap.getValue(obj); - return res; - } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + return DatatypeMap.getValue(meth.invoke(object, (Object[]) param)); + } catch (NoSuchMethodException | SecurityException | IllegalAccessException + | IllegalArgumentException | InvocationTargetException ex) { logger.error("An unexpected error has occurred", ex); } return null; diff --git a/src/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java b/src/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java index 4d21fbacd..1481ebdb2 100644 --- a/src/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java +++ b/src/test/java/fr/inria/corese/core/next/architecture/NextModuleBoundaryTest.java @@ -23,6 +23,7 @@ class NextModuleBoundaryTest { private static final Path NEXT_SOURCES = CORE_SOURCES.resolve("next"); private static final Pattern CORESE_TYPE_REFERENCE = Pattern.compile( "\\bfr\\.inria\\.corese\\.core(?:\\.[A-Za-z_$][A-Za-z0-9_$]*)+"); + private static final Pattern STATIC_IMPORT_PREFIX = Pattern.compile("^static\\s+"); @Test void sharedCodeMustNotDependOnDomainModules() throws IOException { @@ -67,6 +68,25 @@ void publicContractsMustNotDependOnTheLegacyPipeline() throws IOException { } } + @Test + void queryRuntimeMustNotDependOnTheLegacyPipeline() throws IOException { + Path querySources = NEXT_SOURCES.resolve("query"); + assertNoReferences( + querySources, + reference -> reference.startsWith("fr.inria.corese.core.kgram.") + || reference.startsWith("fr.inria.corese.core.sparql.")); + assertNoSourceText( + querySources, + List.of( + "IDatatype", + "CoreseValueFactory", + "BindingAdapter", + "DatatypeAdapter", + "KgramNodeConverter", + "NextDatatypeValueAdapter", + "StorageManagerKgramValues")); + } + @Test void legacyCodeMustNotDependOnNextImplementations() throws IOException { assertNoReferences( @@ -84,9 +104,9 @@ void publicApiAndSpiPackagesMustBeDocumented() throws IOException { } @Test - void nonKgramImplementationPackagesMustBeDocumented() throws IOException { + void implementationPackagesMustBeDocumented() throws IOException { assertPackagesAreDocumented( - nonKgramImplementationDirectories(), "Undocumented non-KGRAM implementation packages:"); + implementationDirectories(), "Undocumented implementation packages:"); } private static void assertPackagesAreDocumented(List packageRoots, String message) @@ -119,10 +139,11 @@ private static List publicContractDirectories() { NEXT_SOURCES.resolve("query/api")); } - private static List nonKgramImplementationDirectories() { + private static List implementationDirectories() { return List.of( NEXT_SOURCES.resolve("data/impl"), NEXT_SOURCES.resolve("storage/impl"), + NEXT_SOURCES.resolve("query/impl/kgram"), NEXT_SOURCES.resolve("query/impl/sparql")); } @@ -144,9 +165,9 @@ private static void assertNoImports( for (String line : Files.readAllLines(source)) { String trimmed = line.trim(); if (trimmed.startsWith("import ")) { - String imported = trimmed - .substring("import ".length()) - .replaceFirst("^static\\s+", "") + String imported = STATIC_IMPORT_PREFIX + .matcher(trimmed.substring("import ".length())) + .replaceFirst("") .replace(";", ""); if (forbidden.test(imported)) { violations.add(NEXT_SOURCES.relativize(source) + " -> " + imported); @@ -192,4 +213,22 @@ private static void assertNoReferences( fail("Forbidden next-module dependencies:\n" + String.join("\n", violations)); } } + + private static void assertNoSourceText(Path sourceDirectory, List forbiddenTokens) + throws IOException { + List violations = new ArrayList<>(); + try (Stream paths = Files.walk(sourceDirectory)) { + for (Path source : paths.filter(path -> path.toString().endsWith(".java")).toList()) { + String content = Files.readString(source); + for (String token : forbiddenTokens) { + if (content.contains(token)) { + violations.add(NEXT_SOURCES.relativize(source) + " -> " + token); + } + } + } + } + if (!violations.isEmpty()) { + fail("Forbidden legacy tokens in next.query:\n" + String.join("\n", violations)); + } + } } diff --git a/src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java b/src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java new file mode 100644 index 000000000..34d65c878 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/data/api/model/RdfValueOrderTest.java @@ -0,0 +1,80 @@ +package fr.inria.corese.core.next.data.api.model; + +import fr.inria.corese.core.next.data.Values; +import fr.inria.corese.core.next.data.api.factory.ValueFactory; +import fr.inria.corese.core.next.data.api.literal.XSDDatatype; +import fr.inria.corese.core.next.data.api.term.Literal; +import fr.inria.corese.core.next.data.api.term.Value; +import fr.inria.corese.core.next.data.api.vocabulary.XSD; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RdfValueOrderTest { + + private final ValueFactory values = Values.factory(); + + @Test + void followsSparqlTermCategoryOrder() { + DatatypeValue blank = values.createBNode("b"); + DatatypeValue iri = values.createIRI("http://example.org/resource"); + DatatypeValue literal = values.createLiteral("value"); + DatatypeValue triple = values.createTriple( + values.createIRI("http://example.org/subject"), + values.createIRI("http://example.org/predicate"), + (Value) literal); + + assertTrue(RdfValueOrder.compareValues(null, blank) < 0); + assertTrue(blank.compare(iri) < 0); + assertTrue(iri.compare(literal) < 0); + assertTrue(literal.compare(triple) < 0); + } + + @Test + void comparesNumericLiteralsByValueBeforeLexicalForm() { + DatatypeValue two = values.createLiteral(new BigDecimal("2")); + DatatypeValue ten = values.createLiteral(new BigDecimal("10")); + + assertTrue(two.compare(ten) < 0); + } + + @Test + void usesRdfTermTieBreakersForEqualNumericValues() { + DatatypeValue integer = values.createLiteral("1", XSD.xsdInteger.getIRI()); + DatatypeValue decimal = values.createLiteral("1.0", XSD.xsdDecimal.getIRI()); + + assertTrue(integer.equalsWE(decimal)); + assertNotEquals(0, integer.compare(decimal)); + } + + @Test + void ordersBooleansAndStringsDeterministically() { + assertTrue(values.createLiteral(false).compare(values.createLiteral(true)) < 0); + assertTrue(values.createLiteral("alpha").compare(values.createLiteral("beta")) < 0); + assertEquals(0, values.createLiteral("same").compare(values.createLiteral("same"))); + } + + @Test + void numericValueEqualityHandlesNanAndSignedZero() { + Literal leftNan = floatingPointLiteral(Double.NaN); + Literal rightNan = floatingPointLiteral(Double.NaN); + + assertFalse(leftNan.equalsWE(rightNan)); + assertTrue(values.createLiteral(-0.0d).equalsWE(values.createLiteral(0.0d))); + } + + private static Literal floatingPointLiteral(double value) { + Literal literal = mock(Literal.class, CALLS_REAL_METHODS); + when(literal.getCoreDatatype()).thenReturn(XSDDatatype.DOUBLE); + when(literal.doubleValue()).thenReturn(value); + return literal; + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapterTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapterTest.java deleted file mode 100644 index ead5a971f..000000000 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/BindingAdapterTest.java +++ /dev/null @@ -1,257 +0,0 @@ -package fr.inria.corese.core.next.query.impl.kgram.adapter; - -import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; -import fr.inria.corese.core.next.query.impl.kgram.core.Mappings; -import fr.inria.corese.core.next.query.impl.kgram.core.Exp; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.triple.function.term.Binding; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** - * Unit tests for the BindingAdapter class. - * Tests the adapter pattern implementation that converts a Binding - * into a BindingContext. - */ -@DisplayName("BindingAdapter Tests") -class BindingAdapterTest { - - private Binding mockBinding; - private BindingAdapter adapter; - private fr.inria.corese.core.kgram.api.core.Expr mockExpr; - private Node mockNode; - private IDatatype mockDatatype; - - @BeforeEach - void setUp() { - mockBinding = mock(Binding.class); - mockExpr = mock(fr.inria.corese.core.kgram.api.core.Expr.class); - mockNode = mock(Node.class); - mockDatatype = mock(IDatatype.class); - - adapter = new BindingAdapter(mockBinding); - } - - @Nested - @DisplayName("Constructor Tests") - class ConstructorTests { - - @Test - @DisplayName("Should create adapter with valid binding") - void testConstructorWithValidBinding() { - BindingAdapter ba = new BindingAdapter(mockBinding); - assertNotNull(ba, "Adapter should not be null"); - assertEquals(mockBinding, ba.delegate(), "Delegate should match"); - } - - @Test - @DisplayName("Should throw exception with null binding") - void testConstructorWithNullBinding() { - assertThrows(IllegalArgumentException.class, - () -> new BindingAdapter(null), - "Should throw IllegalArgumentException for null delegate"); - } - } - - @Nested - @DisplayName("Delegate Tests") - class DelegateTests { - - @Test - @DisplayName("Should return delegate") - void testDelegate() { - Binding result = adapter.delegate(); - assertSame(mockBinding, result, "Should return same delegate instance"); - } - } - - - @Nested - @DisplayName("setValue Tests") - class SetValueTests { - - @Test - @DisplayName("Should set value by variable name") - void testSetValue() { - String varName = "x"; - when(mockExpr.getLabel()).thenReturn(varName); - when(mockBinding.getVariables()).thenReturn(createExprList(mockExpr)); - when(mockNode.getDatatypeValue()).thenReturn(mockDatatype); - - adapter.setValue(varName, mockNode); - verify(mockBinding).set(mockExpr, mockDatatype); - } - - @Test - @DisplayName("Should set null value") - void testSetNullValue() { - String varName = "x"; - when(mockExpr.getLabel()).thenReturn(varName); - when(mockBinding.getVariables()).thenReturn(createExprList(mockExpr)); - - adapter.setValue(varName, null); - verify(mockBinding).set(mockExpr, null); - } - - @Test - @DisplayName("Should do nothing for unknown variable") - void testSetValueUnknownVariable() { - when(mockBinding.getVariables()).thenReturn(new ArrayList<>()); - - adapter.setValue("unknown", mockNode); - verify(mockBinding, never()).set(any(), any()); - } - } - - @Nested - @DisplayName("isDefined Tests") - class IsDefinedTests { - - @Test - @DisplayName("Should return true for defined variable") - void testIsDefinedTrue() { - String varName = "x"; - when(mockExpr.getLabel()).thenReturn(varName); - when(mockBinding.getVariables()).thenReturn(createExprList(mockExpr)); - when(mockBinding.isBound(varName)).thenReturn(true); - - boolean result = adapter.isDefined(varName); - assertTrue(result, "Should return true for defined variable"); - } - - - @Test - @DisplayName("Should return false for unknown variable") - void testIsDefinedUnknown() { - when(mockBinding.getVariables()).thenReturn(new ArrayList<>()); - - boolean result = adapter.isDefined("unknown"); - assertFalse(result, "Should return false for unknown variable"); - } - } - - @Nested - @DisplayName("copy Tests") - class CopyTests { - - @Test - @DisplayName("Should copy from another BindingAdapter") - void testCopyFromBindingAdapter() { - Binding otherBinding = mock(Binding.class); - BindingAdapter other = new BindingAdapter(otherBinding); - - adapter.copy(other); - verify(mockBinding).share(otherBinding); - } - - @Test - @DisplayName("Should handle null other") - void testCopyNull() { - assertDoesNotThrow(() -> adapter.copy(null), - "Should not throw when copying from null"); - } - } - - - @Nested - @DisplayName("visit Tests") - class VisitTests { - - @Test - @DisplayName("Should visit with next kgram types") - void testVisitNextKgramTypes() { - Exp mockExp = - mock(Exp.class); - Mappings mockMappings1 = mock(Mappings.class); - Mappings mockMappings2 = mock(Mappings.class); - - assertDoesNotThrow(() -> adapter.visit(mockExp, mockNode, mockMappings1, mockMappings2), - "Should not throw when visiting with next kgram types"); - } - - @Test - @DisplayName("Should visit with legacy kgram types") - void testVisitLegacyKgramTypes() { - fr.inria.corese.core.kgram.core.Exp mockExp = - mock(fr.inria.corese.core.kgram.core.Exp.class); - fr.inria.corese.core.kgram.api.core.Node mockLegacyNode = - mock(fr.inria.corese.core.kgram.api.core.Node.class); - fr.inria.corese.core.kgram.core.Mappings mockMappings1 = - mock(fr.inria.corese.core.kgram.core.Mappings.class); - fr.inria.corese.core.kgram.core.Mappings mockMappings2 = - mock(fr.inria.corese.core.kgram.core.Mappings.class); - - assertDoesNotThrow(() -> adapter.visit(mockExp, mockLegacyNode, mockMappings1, mockMappings2), - "Should not throw when visiting with legacy kgram types"); - } - - - @Test - @DisplayName("Should handle visit with null parameters") - void testVisitWithNullParameters() { - Exp mockExp = - mock(Exp.class); - - assertDoesNotThrow(() -> adapter.visit(mockExp, null, null, null), - "Should not throw when visiting with null parameters"); - } - } - - @Nested - @DisplayName("equals Tests") - class EqualsTests { - - @Test - @DisplayName("Should be equal to itself") - void testEqualsSelf() { - assertEquals(adapter, adapter, "Should be equal to itself"); - } - - @Test - @DisplayName("Should not be equal to null") - void testNotEqualsNull() { - assertNotEquals(null, adapter, "Should not be equal to null"); - } - - - } - - @Nested - @DisplayName("Record Tests") - class RecordTests { - - @Test - @DisplayName("Should have consistent hashCode") - void testHashCode() { - int hash1 = adapter.hashCode(); - int hash2 = adapter.hashCode(); - assertEquals(hash1, hash2, "HashCode should be consistent"); - } - - @Test - @DisplayName("Should have proper toString") - void testToString() { - String str = adapter.toString(); - assertNotNull(str, "toString should not return null"); - assertTrue(str.contains("BindingAdapter"), - "toString should contain class name"); - } - } - - // Helper method to create a list of Expr - private List createExprList( - fr.inria.corese.core.kgram.api.core.Expr... exprs) { - List list = new ArrayList<>(); - Collections.addAll(list, exprs); - return list; - } -} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapterTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapterTest.java deleted file mode 100644 index 20e4cfbac..000000000 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/adapter/DatatypeAdapterTest.java +++ /dev/null @@ -1,347 +0,0 @@ -package fr.inria.corese.core.next.query.impl.kgram.adapter; - -import fr.inria.corese.core.next.query.impl.kgram.api.core.DatatypeValue; -import fr.inria.corese.core.sparql.api.IDatatype; -import fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -@DisplayName("DatatypeAdapter Tests") -class DatatypeAdapterTest { - - private IDatatype mockDatatype; - private DatatypeAdapter adapter; - - - @BeforeEach - void setUp() { - mockDatatype = mock(IDatatype.class); - adapter = new DatatypeAdapter(mockDatatype); - } - - - @Nested - @DisplayName("Constructor Tests") - class ConstructorTests { - - @Test - @DisplayName("Should create adapter with valid datatype") - void testConstructorWithValidDatatype() { - DatatypeAdapter da = new DatatypeAdapter(mockDatatype); - assertNotNull(da, "Adapter should not be null"); - assertEquals(mockDatatype, da.delegate(), "Delegate should match"); - } - - @Test - @DisplayName("Should throw exception with null datatype") - void testConstructorWithNullDatatype() { - assertThrows(IllegalArgumentException.class, - () -> new DatatypeAdapter(null), - "Should throw IllegalArgumentException for null delegate"); - } - } - - @Nested - @DisplayName("Delegate Tests") - class DelegateTests { - - @Test - @DisplayName("Should return delegate") - void testDelegate() { - IDatatype result = adapter.delegate(); - assertSame(mockDatatype, result, "Should return same delegate instance"); - } - } - - @Nested - @DisplayName("unwrap Tests") - class UnwrapTests { - - @Test - @DisplayName("Should unwrap DatatypeAdapter") - void testUnwrapAdapter() { - IDatatype result = DatatypeAdapter.unwrap(adapter); - assertSame(mockDatatype, result, "Should unwrap to delegate"); - } - - @Test - @DisplayName("Should return null for null value") - void testUnwrapNull() { - assertNull(null, "Should return null for null value"); - } - - @Test - @DisplayName("Should return null for non-adapter value") - void testUnwrapNonAdapter() { - DatatypeValue nonAdapter = mock(DatatypeValue.class); - IDatatype result = DatatypeAdapter.unwrap(nonAdapter); - assertNull(result, "Should return null for non-adapter value"); - } - } - - @Nested - @DisplayName("getLabel Tests") - class GetLabelTests { - - @Test - @DisplayName("Should get label") - void testGetLabel() { - String expectedLabel = "test-label"; - when(mockDatatype.getLabel()).thenReturn(expectedLabel); - - String result = adapter.getLabel(); - assertEquals(expectedLabel, result, "Label should match"); - verify(mockDatatype).getLabel(); - } - - @Test - @DisplayName("Should return null label") - void testGetNullLabel() { - when(mockDatatype.getLabel()).thenReturn(null); - - String result = adapter.getLabel(); - assertNull(result, "Should return null label"); - } - } - - - @Nested - @DisplayName("getDatatypeURI Tests") - class GetDatatypeURITests { - - @Test - @DisplayName("Should get datatype URI") - void testGetDatatypeURI() { - String expectedURI = "http://www.w3.org/2001/XMLSchema#string"; - when(mockDatatype.getDatatypeURI()).thenReturn(expectedURI); - - String result = adapter.getDatatypeURI(); - assertEquals(expectedURI, result, "Datatype URI should match"); - verify(mockDatatype).getDatatypeURI(); - } - - @Test - @DisplayName("Should return null datatype URI") - void testGetNullDatatypeURI() { - when(mockDatatype.getDatatypeURI()).thenReturn(null); - - String result = adapter.getDatatypeURI(); - assertNull(result, "Should return null datatype URI"); - } - } - - @Nested - @DisplayName("isTrue Tests") - class IsTrueTests { - - @Test - @DisplayName("Should return true") - void testIsTrueReturnsTrue() { - when(mockDatatype.isTrueTest()).thenReturn(true); - - boolean result = adapter.isTrue(); - assertTrue(result, "Should return true"); - verify(mockDatatype).isTrueTest(); - } - - @Test - @DisplayName("Should return false") - void testIsTrueReturnsFalse() { - when(mockDatatype.isTrueTest()).thenReturn(false); - - boolean result = adapter.isTrue(); - assertFalse(result, "Should return false"); - verify(mockDatatype).isTrueTest(); - } - } - - @Nested - @DisplayName("equalsWE Tests") - class EqualsWETests { - - @Test - @DisplayName("Should compare equal datatypes") - void testEqualsWETrue() throws CoreseDatatypeException { - IDatatype otherDatatype = mock(IDatatype.class); - DatatypeAdapter other = new DatatypeAdapter(otherDatatype); - when(mockDatatype.equalsWE(otherDatatype)).thenReturn(true); - - boolean result = adapter.equalsWE(other); - assertTrue(result, "Should be equal"); - verify(mockDatatype).equalsWE(otherDatatype); - } - - @Test - @DisplayName("Should compare unequal datatypes") - void testEqualsWEFalse() throws CoreseDatatypeException { - IDatatype otherDatatype = mock(IDatatype.class); - DatatypeAdapter other = new DatatypeAdapter(otherDatatype); - when(mockDatatype.equalsWE(otherDatatype)).thenReturn(false); - - boolean result = adapter.equalsWE(other); - assertFalse(result, "Should not be equal"); - verify(mockDatatype).equalsWE(otherDatatype); - } - - @Test - @DisplayName("Should return false for non-adapter value") - void testEqualsWENonAdapter() throws CoreseDatatypeException { - DatatypeValue other = mock(DatatypeValue.class); - - boolean result = adapter.equalsWE(other); - assertFalse(result, "Should return false for non-adapter value"); - } - - @Test - @DisplayName("Should propagate CoreseDatatypeException") - void testEqualsWEException() throws CoreseDatatypeException { - IDatatype otherDatatype = mock(IDatatype.class); - DatatypeAdapter other = new DatatypeAdapter(otherDatatype); - CoreseDatatypeException exception = new CoreseDatatypeException("Test error"); - when(mockDatatype.equalsWE(otherDatatype)).thenThrow(exception); - - assertThrows(CoreseDatatypeException.class, - () -> adapter.equalsWE(other), - "Should propagate CoreseDatatypeException"); - } - } - - @Nested - @DisplayName("compare Tests") - class CompareTests { - - @Test - @DisplayName("Should compare less than") - void testCompareLessThan() throws CoreseDatatypeException { - IDatatype otherDatatype = mock(IDatatype.class); - DatatypeAdapter other = new DatatypeAdapter(otherDatatype); - when(mockDatatype.compare(otherDatatype)).thenReturn(-1); - - int result = adapter.compare(other); - assertEquals(-1, result, "Should return -1 for less than"); - verify(mockDatatype).compare(otherDatatype); - } - - @Test - @DisplayName("Should compare equal") - void testCompareEqual() throws CoreseDatatypeException { - IDatatype otherDatatype = mock(IDatatype.class); - DatatypeAdapter other = new DatatypeAdapter(otherDatatype); - when(mockDatatype.compare(otherDatatype)).thenReturn(0); - - int result = adapter.compare(other); - assertEquals(0, result, "Should return 0 for equal"); - verify(mockDatatype).compare(otherDatatype); - } - - @Test - @DisplayName("Should compare greater than") - void testCompareGreaterThan() throws CoreseDatatypeException { - IDatatype otherDatatype = mock(IDatatype.class); - DatatypeAdapter other = new DatatypeAdapter(otherDatatype); - when(mockDatatype.compare(otherDatatype)).thenReturn(1); - - int result = adapter.compare(other); - assertEquals(1, result, "Should return 1 for greater than"); - verify(mockDatatype).compare(otherDatatype); - } - - @Test - @DisplayName("Should throw exception for non-adapter value") - void testCompareNonAdapter() { - DatatypeValue other = mock(DatatypeValue.class); - - assertThrows(IllegalArgumentException.class, - () -> adapter.compare(other), - "Should throw IllegalArgumentException for non-adapter value"); - } - - @Test - @DisplayName("Should propagate CoreseDatatypeException") - void testCompareException() throws CoreseDatatypeException { - IDatatype otherDatatype = mock(IDatatype.class); - DatatypeAdapter other = new DatatypeAdapter(otherDatatype); - CoreseDatatypeException exception = new CoreseDatatypeException("Test error"); - when(mockDatatype.compare(otherDatatype)).thenThrow(exception); - - assertThrows(CoreseDatatypeException.class, - () -> adapter.compare(other), - "Should propagate CoreseDatatypeException"); - } - } - - @Nested - @DisplayName("intValue Tests") - class IntValueTests { - - @Test - @DisplayName("Should get int value") - void testIntValue() { - int expectedValue = 42; - when(mockDatatype.intValue()).thenReturn(expectedValue); - - int result = adapter.intValue(); - assertEquals(expectedValue, result, "Int value should match"); - verify(mockDatatype).intValue(); - } - - @Test - @DisplayName("Should get zero int value") - void testIntValueZero() { - when(mockDatatype.intValue()).thenReturn(0); - - int result = adapter.intValue(); - assertEquals(0, result, "Int value should be zero"); - } - - @Test - @DisplayName("Should get negative int value") - void testIntValueNegative() { - when(mockDatatype.intValue()).thenReturn(-10); - - int result = adapter.intValue(); - assertEquals(-10, result, "Int value should be negative"); - } - } - - @Nested - @DisplayName("doubleValue Tests") - class DoubleValueTests { - - @Test - @DisplayName("Should get double value") - void testDoubleValue() { - double expectedValue = 3.14; - when(mockDatatype.doubleValue()).thenReturn(expectedValue); - - double result = adapter.doubleValue(); - assertEquals(expectedValue, result, "Double value should match"); - verify(mockDatatype).doubleValue(); - } - - @Test - @DisplayName("Should get zero double value") - void testDoubleValueZero() { - when(mockDatatype.doubleValue()).thenReturn(0.0); - - double result = adapter.doubleValue(); - assertEquals(0.0, result, "Double value should be zero"); - } - - @Test - @DisplayName("Should get negative double value") - void testDoubleValueNegative() { - when(mockDatatype.doubleValue()).thenReturn(-5.5); - - double result = adapter.doubleValue(); - assertEquals(-5.5, result, "Double value should be negative"); - } - } - - -} \ No newline at end of file diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalTest.java index f6b26fc0e..21d7cbafe 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/EvalTest.java @@ -351,65 +351,40 @@ void testAddPlugin() { } @Nested - @DisplayName("Static Configuration Tests") - class StaticConfigurationTests { + @DisplayName("Evaluation Configuration Tests") + class EvaluationConfigurationTests { @Test @DisplayName("Should get and set push edge mappings") void testPushEdgeMappings() { - boolean original = Eval.isPushEdgeMappings(); - - Eval.setPushEdgeMappings(false); - assertFalse(Eval.isPushEdgeMappings(), + eval.setPushEdgeMappings(false); + assertFalse(eval.isPushEdgeMappings(), "PushEdgeMappings should be false"); - - // Reset to original - Eval.setPushEdgeMappings(original); } @Test @DisplayName("Should get and set parameter graph mappings") void testParameterGraphMappings() { - boolean original = Eval.isParameterGraphMappings(); - - Eval.setParameterGraphMappings(false); - assertFalse(Eval.isParameterGraphMappings(), + eval.setParameterGraphMappings(false); + assertFalse(eval.isParameterGraphMappings(), "ParameterGraphMappings should be false"); - - // Reset to original - Eval.setParameterGraphMappings(original); } @Test @DisplayName("Should get and set parameter union mappings") void testParameterUnionMappings() { - boolean original = Eval.isParameterUnionMappings(); - - Eval.setParameterUnionMappings(false); - assertFalse(Eval.isParameterUnionMappings(), + eval.setParameterUnionMappings(false); + assertFalse(eval.isParameterUnionMappings(), "ParameterUnionMappings should be false"); - - // Reset to original - Eval.setParameterUnionMappings(original); } @Test @DisplayName("Should set all new mappings versions") void testSetNewMappingsVersion() { - // Save originals - boolean origPush = Eval.isPushEdgeMappings(); - boolean origGraph = Eval.isParameterGraphMappings(); - boolean origUnion = Eval.isParameterUnionMappings(); - - Eval.setNewMappingsVersion(false); - assertFalse(Eval.isPushEdgeMappings(), "All flags should be false"); - assertFalse(Eval.isParameterGraphMappings(), "All flags should be false"); - assertFalse(Eval.isParameterUnionMappings(), "All flags should be false"); - - // Reset to originals - Eval.setPushEdgeMappings(origPush); - Eval.setParameterGraphMappings(origGraph); - Eval.setParameterUnionMappings(origUnion); + eval.setNewMappingsVersion(false); + assertFalse(eval.isPushEdgeMappings(), "All flags should be false"); + assertFalse(eval.isParameterGraphMappings(), "All flags should be false"); + assertFalse(eval.isParameterUnionMappings(), "All flags should be false"); } } @@ -473,25 +448,6 @@ void testIsJoinMappings() { } } - @Nested - @DisplayName("Static Count Tests") - class StaticCountTests { - - @Test - @DisplayName("Should access static count") - void testStaticCount() { - int count = Eval.count; - assertTrue(count >= 0, "Static count should be non-negative"); - } - - @Test - @DisplayName("Should access display result max") - void testDisplayResultMax() { - int max = Eval.DISPLAY_RESULT_MAX; - assertTrue(max > 0, "Display result max should be positive"); - } - } - @Nested @DisplayName("Clean Tests") class CleanTests { @@ -503,4 +459,4 @@ void testClean() { "Clean should not throw exception"); } } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/MappingsTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/MappingsTest.java index 016c87e22..d6bca2ed4 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/MappingsTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/MappingsTest.java @@ -2,7 +2,7 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.Edge; import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; -import fr.inria.corese.core.sparql.api.IDatatype; +import fr.inria.corese.core.next.data.api.model.DatatypeValue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -285,12 +285,12 @@ void testGetNodeEmpty() { @Test @DisplayName("Should get value by name") void testGetValue() { - IDatatype mockValue = mock(IDatatype.class); + DatatypeValue mockValue = mock(DatatypeValue.class); when(mockNode.getDatatypeValue()).thenReturn(mockValue); when(mockMapping.getNode("?x")).thenReturn(mockNode); mappings.add(mockMapping); - IDatatype result = mappings.getValue("?x"); + DatatypeValue result = mappings.getValue("?x"); assertEquals(mockValue, result, "Should return value"); } } @@ -779,4 +779,4 @@ void testGetLoop() { assertEquals(mappings, loop, "Loop should return self"); } } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/QueryTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/QueryTest.java index 6630123a1..56f4a2283 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/QueryTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/core/QueryTest.java @@ -3,8 +3,8 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.ExpType; import fr.inria.corese.core.next.query.impl.kgram.api.core.Filter; import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; -import fr.inria.corese.core.sparql.triple.parser.ASTExtension; -import fr.inria.corese.core.sparql.triple.parser.ASTQuery; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.impl.sparql.parser.SparqlParser; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -655,9 +655,9 @@ class ASTTests { @Test @DisplayName("Should set and get AST") void testSetAndGetAST() { - ASTQuery mockAST = mock(ASTQuery.class); - query.setAST(mockAST); - assertEquals(mockAST, query.getAST(), "AST should match"); + QueryAst ast = new SparqlParser().parse("SELECT * WHERE { ?s ?p ?o }"); + query.setAST(ast); + assertSame(ast, query.getAST(), "AST should match"); } @Test @@ -706,20 +706,6 @@ void testSetAndIsExtension() { assertTrue(query.isExtension(), "Query should be extension"); } - @Test - @DisplayName("Should set and get extension") - void testSetAndGetExtension() { - ASTExtension mockExt = mock(ASTExtension.class); - query.setExtension(mockExt); - assertEquals(mockExt, query.getExtension(), "Extension should match"); - } - - @Test - @DisplayName("Should get actual extension") - void testGetActualExtension() { - assertDoesNotThrow(() -> query.getActualExtension(), - "Getting actual extension should not throw"); - } } @Nested diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/execution/RdfTermMatcherTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/execution/RdfTermMatcherTest.java index ea688a07b..dfada41a5 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/execution/RdfTermMatcherTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/execution/RdfTermMatcherTest.java @@ -6,8 +6,6 @@ import fr.inria.corese.core.next.query.impl.kgram.api.query.Matcher; import fr.inria.corese.core.next.query.impl.kgram.tool.EnvironmentImpl; import fr.inria.corese.core.next.query.impl.kgram.tool.NodeImpl; -import fr.inria.corese.core.sparql.triple.parser.Constant; -import fr.inria.corese.core.sparql.triple.parser.Variable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -19,11 +17,11 @@ class RdfTermMatcherTest { private final RdfTermMatcher matcher = new RdfTermMatcher(); private static Node iri(String label) { - return new NodeImpl(Constant.createResource(label)); + return NodeImpl.forIRI(label); } - private static Node var(String name) { - return new NodeImpl(Variable.create(name)); + private static Node variable(String name) { + return NodeImpl.forVariable(name); } /** @@ -53,20 +51,20 @@ void constantRejectsDifferentConstant() { @Test @DisplayName("An unbound variable matches any term (null environment)") void unboundVariableMatchesAnyTerm() { - assertTrue(matcher.match(var("s"), iri("http://example.org/a"), null)); + assertTrue(matcher.match(variable("s"), iri("http://example.org/a"), null)); } @Test @DisplayName("An unbound variable matches any term (environment returns null)") void unboundVariableInEnvironmentMatchesAnyTerm() { - Environment env = bind(var("other"), iri("http://example.org/z")); // ?s stays unbound - assertTrue(matcher.match(var("s"), iri("http://example.org/a"), env)); + Environment env = bind(variable("other"), iri("http://example.org/z")); // ?s stays unbound + assertTrue(matcher.match(variable("s"), iri("http://example.org/a"), env)); } @Test @DisplayName("An already-bound variable matches only its bound value (BGP join, compatible)") void boundVariableMatchesSameValue() { - Node s = var("s"); + Node s = variable("s"); Environment env = bind(s, iri("http://example.org/a")); assertTrue(matcher.match(s, iri("http://example.org/a"), env)); } @@ -74,7 +72,7 @@ void boundVariableMatchesSameValue() { @Test @DisplayName("An already-bound variable rejects a different value (BGP join, conflicting)") void boundVariableRejectsDifferentValue() { - Node s = var("s"); + Node s = variable("s"); Environment env = bind(s, iri("http://example.org/a")); assertFalse(matcher.match(s, iri("http://example.org/b"), env)); } @@ -93,7 +91,7 @@ void nullIsNotAWildcard() { @DisplayName("Variable-predicate edge { ?s ?p ?o } matches a concrete triple (predicate via edge variable)") void edgeWithVariablePredicateMatches() { // query: ?s ?p ?o (predicate is a variable, exposed via getEdgeVariable()) - Edge query = new TestEdge(var("s"), null, var("p"), var("o")); + Edge query = new TestEdge(variable("s"), null, variable("p"), variable("o")); // target:

Edge target = new TestEdge(iri("http://example.org/a"), iri("http://example.org/p"), null, iri("http://example.org/b")); @@ -105,7 +103,7 @@ void edgeWithVariablePredicateMatches() { @DisplayName("Fixed-predicate edge { ?s

?o } matches only the same predicate (predicate via edge node)") void edgeWithFixedPredicateMatchesSamePredicate() { // query: ?s

?o (fixed predicate, edge variable is null → predicate via getEdgeNode()) - Edge query = new TestEdge(var("s"), iri("http://example.org/p"), null, var("o")); + Edge query = new TestEdge(variable("s"), iri("http://example.org/p"), null, variable("o")); Edge samePredicate = new TestEdge(iri("http://example.org/a"), iri("http://example.org/p"), null, iri("http://example.org/b")); @@ -186,4 +184,4 @@ public Edge getEdge() { return this; } } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdgeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdgeTest.java index 9cd412ed6..7179335f4 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdgeTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerEdgeTest.java @@ -31,15 +31,15 @@ void exposesStorageStatementAsKgramEdge() { assertEquals(predicate.stringValue(), edge.getProperty().getLabel()); assertEquals(predicate.stringValue(), edge.getEdgeLabel()); assertEquals("Alice", edge.getNode(1).getLabel()); - assertEquals("en", edge.getNode(1).getDatatypeValue().getLang()); + assertEquals("en", ((Literal) edge.getNode(1).getDatatypeValue()).getLanguage().orElseThrow()); assertEquals(graph.stringValue(), edge.getGraph().getLabel()); assertTrue(edge.contains(edge.getNode(0))); } @Test - void convertsTypedLiteralThroughSharedKgramValueHelper() { + void exposesTypedLiteralDirectlyAsKgramNode() { IRI integerDatatype = valueFactory.createIRI("http://www.w3.org/2001/XMLSchema#integer"); - Node node = StorageManagerKgramValues.node(valueFactory.createLiteral("42", integerDatatype)); + Node node = NodeImpl.forValue(valueFactory.createLiteral("42", integerDatatype)); assertEquals("42", node.getLabel()); assertEquals(integerDatatype.stringValue(), node.getDatatypeValue().getDatatypeURI()); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducerTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducerTest.java index 51cca08fa..22a99766c 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducerTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/kgram/tool/StorageManagerProducerTest.java @@ -20,9 +20,6 @@ import fr.inria.corese.core.next.query.impl.kgram.execution.RdfTermMatcher; import fr.inria.corese.core.next.query.impl.kgram.execution.SparqlKgramEvaluator; import fr.inria.corese.core.next.storage.impl.memory.MemoryStorageManager; -import fr.inria.corese.core.sparql.datatype.DatatypeMap; -import fr.inria.corese.core.sparql.triple.parser.Constant; -import fr.inria.corese.core.sparql.triple.parser.Variable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -301,15 +298,15 @@ private List edges(Edge queryEdge, Node graphNode, List from, Enviro } private static Node variable(String name) { - return new NodeImpl(Variable.create(name)); + return NodeImpl.forVariable(name); } private static Node resource(String iri) { - return new NodeImpl(Constant.create(DatatypeMap.newResource(iri))); + return NodeImpl.forIRI(iri); } private static Node literal(String label) { - return new NodeImpl(Constant.create(DatatypeMap.newLiteral(label))); + return NodeImpl.forLiteral(label, null, null); } private static Query selectStarSpoQuery(Node s, Node p, Node o) { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java index e3cf04ad4..c3eb2fbe3 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/AstBackedEdgeTest.java @@ -3,8 +3,6 @@ import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; import fr.inria.corese.core.next.query.impl.kgram.tool.KgramNodes; import fr.inria.corese.core.next.query.impl.kgram.tool.NodeImpl; -import fr.inria.corese.core.sparql.triple.parser.Constant; -import fr.inria.corese.core.sparql.triple.parser.Variable; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -16,9 +14,9 @@ class AstBackedEdgeTest { @Test @DisplayName("contains() recognises the subject and the object") void edgeContainsSubjectAndObject() { - Node subject = new NodeImpl(Variable.create("s")); - Node predicate = new NodeImpl(Variable.create("p")); - Node object = new NodeImpl(Variable.create("o")); + Node subject = NodeImpl.forVariable("s"); + Node predicate = NodeImpl.forVariable("p"); + Node object = NodeImpl.forVariable("o"); AstBackedEdge edge = new AstBackedEdge(subject, predicate, object); @@ -30,43 +28,43 @@ void edgeContainsSubjectAndObject() { @DisplayName("contains() recognises a variable by name (not only the same instance)") void edgeContainsByVariableName() { AstBackedEdge edge = new AstBackedEdge( - new NodeImpl(Variable.create("s")), - new NodeImpl(Variable.create("p")), - new NodeImpl(Variable.create("o"))); + NodeImpl.forVariable("s"), + NodeImpl.forVariable("p"), + NodeImpl.forVariable("o")); - assertTrue(edge.contains(new NodeImpl(Variable.create("s")))); - assertTrue(edge.contains(new NodeImpl(Variable.create("o")))); + assertTrue(edge.contains(NodeImpl.forVariable("s"))); + assertTrue(edge.contains(NodeImpl.forVariable("o"))); } @Test @DisplayName("contains() returns false for an absent variable and for null") void edgeDoesNotContainOtherNode() { AstBackedEdge edge = new AstBackedEdge( - new NodeImpl(Variable.create("s")), - new NodeImpl(Variable.create("p")), - new NodeImpl(Variable.create("o"))); + NodeImpl.forVariable("s"), + NodeImpl.forVariable("p"), + NodeImpl.forVariable("o")); - assertFalse(edge.contains(new NodeImpl(Variable.create("x")))); + assertFalse(edge.contains(NodeImpl.forVariable("x"))); assertFalse(edge.contains(null)); } @Test @DisplayName("Variable predicate is exposed as edge variable with root property as edge node") void edgeVariableExposedOnlyForVariablePredicate() { - Node predicateVar = new NodeImpl(Variable.create("p")); + Node predicateVar = NodeImpl.forVariable("p"); AstBackedEdge withVarPredicate = new AstBackedEdge( - new NodeImpl(Variable.create("s")), predicateVar, new NodeImpl(Variable.create("o"))); + NodeImpl.forVariable("s"), predicateVar, NodeImpl.forVariable("o")); assertEquals(KgramNodes.ROOT_PROPERTY_URI, withVarPredicate.getEdgeNode().getLabel()); assertEquals(KgramNodes.ROOT_PROPERTY_URI, withVarPredicate.getEdgeLabel()); assertSame(predicateVar, withVarPredicate.getProperty()); assertSame(predicateVar, withVarPredicate.getEdgeVariable()); - Node iriPredicate = new NodeImpl(Constant.createResource("http://example.org/p")); + Node iriPredicate = NodeImpl.forIRI("http://example.org/p"); AstBackedEdge withIriPredicate = new AstBackedEdge( - new NodeImpl(Variable.create("s")), + NodeImpl.forVariable("s"), iriPredicate, - new NodeImpl(Variable.create("o"))); + NodeImpl.forVariable("o")); assertSame(iriPredicate, withIriPredicate.getEdgeNode()); assertSame(iriPredicate, withIriPredicate.getProperty()); assertEquals("http://example.org/p", withIriPredicate.getEdgeLabel()); @@ -77,9 +75,9 @@ void edgeVariableExposedOnlyForVariablePredicate() { @DisplayName("getGraph() is null for the default graph (engine represents it as null)") void defaultGraphIsNull() { AstBackedEdge edge = new AstBackedEdge( - new NodeImpl(Variable.create("s")), - new NodeImpl(Variable.create("p")), - new NodeImpl(Variable.create("o"))); + NodeImpl.forVariable("s"), + NodeImpl.forVariable("p"), + NodeImpl.forVariable("o")); assertNull(edge.getGraph()); } @@ -87,11 +85,11 @@ void defaultGraphIsNull() { @Test @DisplayName("getGraph() returns the graph node passed to the constructor") void explicitGraphIsReturned() { - Node graph = new NodeImpl(Constant.createResource("http://example.org/g")); + Node graph = NodeImpl.forIRI("http://example.org/g"); AstBackedEdge edge = new AstBackedEdge( - new NodeImpl(Variable.create("s")), - new NodeImpl(Variable.create("p")), - new NodeImpl(Variable.create("o")), + NodeImpl.forVariable("s"), + NodeImpl.forVariable("p"), + NodeImpl.forVariable("o"), graph); assertSame(graph, edge.getGraph()); diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java index 42cf1e914..05c0f2e14 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/CoreseAstQueryBuilderTest.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Set; +import java.util.concurrent.Executors; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -85,6 +86,25 @@ void expandsPrefixesInQuery() { assertEquals("http://example.org/ex#o", edge.getNode(1).getLabel()); } + @Test + @DisplayName("Concurrent query compilation keeps prologue namespaces isolated") + void isolatesConcurrentQueryPrologues() throws Exception { + try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { + var first = executor.submit(() -> compilePredicate( + "PREFIX ex: SELECT * WHERE { ?s ex:p ?o }")); + var second = executor.submit(() -> compilePredicate( + "PREFIX ex: SELECT * WHERE { ?s ex:p ?o }")); + + assertEquals("http://first.example/p", first.get()); + assertEquals("http://second.example/p", second.get()); + } + } + + private String compilePredicate(String sparql) { + SelectQueryAst select = assertInstanceOf(SelectQueryAst.class, newParserDefault().parse(sparql)); + return builder.toNextQuery(select).getBody().get(0).get(0).getEdge().getEdgeNode().getLabel(); + } + @Test @DisplayName("Explicit angle-bracketed IRIs are not treated as prefixed names") void preservesAngleBracketedIrisWithoutPrefixExpansion() { diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverterTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverterTest.java deleted file mode 100644 index 52f26c89b..000000000 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/KgramNodeConverterTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.data.api.term.BNode; -import fr.inria.corese.core.next.data.api.term.IRI; -import fr.inria.corese.core.next.data.api.term.Literal; -import fr.inria.corese.core.next.data.api.term.Value; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory; -import fr.inria.corese.core.next.query.impl.kgram.api.core.Node; -import fr.inria.corese.core.next.query.impl.kgram.tool.NodeImpl; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.*; - -class KgramNodeConverterTest { - - private CoreseValueFactory factory; - - @BeforeEach - void setUp() { - factory = new CoreseValueFactory(); - } - - @Test - @DisplayName("IRI node converts to API IRI with the same string value") - void iriNodeConvertsToApiIRI() { - Node node = NodeImpl.forIRI("http://example.org/alice"); - - Value value = KgramNodeConverter.nodeToValue(node, factory); - - assertInstanceOf(IRI.class, value); - assertEquals("http://example.org/alice", value.stringValue()); - } - - @Test - @DisplayName("Blank node converts to API BNode with the same ID") - void blankNodeConvertsToApiBNode() { - Node node = NodeImpl.forBlank("b1"); - - Value value = KgramNodeConverter.nodeToValue(node, factory); - - assertInstanceOf(BNode.class, value); - assertEquals("b1", ((BNode) value).getID()); - } - - @Test - @DisplayName("Language-tagged literal converts to API Literal preserving label and lang") - void langLiteralConvertsToApiLiteral() { - Node node = NodeImpl.forLiteral("hello", null, "en"); - - Value value = KgramNodeConverter.nodeToValue(node, factory); - - assertInstanceOf(Literal.class, value); - Literal lit = (Literal) value; - assertEquals("hello", lit.getLabel()); - assertEquals("en", lit.getLanguage().orElse(null)); - } - - @Test - @DisplayName("Typed literal converts to API Literal preserving label and datatype IRI") - void typedLiteralConvertsToApiLiteral() { - String xsdInteger = "http://www.w3.org/2001/XMLSchema#integer"; - Node node = NodeImpl.forLiteral("42", xsdInteger, null); - - Value value = KgramNodeConverter.nodeToValue(node, factory); - - assertInstanceOf(Literal.class, value); - Literal lit = (Literal) value; - assertEquals("42", lit.getLabel()); - assertNotNull(lit.getDatatype()); - assertEquals(xsdInteger, lit.getDatatype().stringValue()); - assertTrue(lit.getLanguage().isEmpty()); - } - - @Test - @DisplayName("Plain literal (no lang, no explicit datatype) converts to API Literal with label") - void plainLiteralConvertsToApiLiteral() { - Node node = NodeImpl.forLiteral("bare", null, null); - - Value value = KgramNodeConverter.nodeToValue(node, factory); - - assertInstanceOf(Literal.class, value); - assertEquals("bare", ((Literal) value).getLabel()); - } - -} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java deleted file mode 100644 index 51eeaaf28..000000000 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlAstToExpressionTest.java +++ /dev/null @@ -1,72 +0,0 @@ -package fr.inria.corese.core.next.query.impl.sparql.bridge; - -import fr.inria.corese.core.next.query.impl.sparql.ast.GroupGraphPatternAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.LiteralAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.VarAst; -import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.ExistsAst; -import fr.inria.corese.core.next.query.impl.kgram.api.core.ExprType; -import fr.inria.corese.core.sparql.triple.parser.Constant; -import fr.inria.corese.core.sparql.triple.parser.Expression; -import fr.inria.corese.core.sparql.triple.parser.Variable; -import org.junit.jupiter.api.Test; - -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; - -class SparqlAstToExpressionTest { - - @Test - void iriAstToExpression() { - IriAst iri = new IriAst(""); - Expression iriNode = SparqlAstToExpression.convert(iri); - assertNotNull(iriNode); - assertInstanceOf(Constant.class, iriNode); - assertTrue(iriNode.isURI()); - assertEquals("http://ns.inria.fr/test/iri", ((Constant)iriNode).getLabel()); - } - - @Test - void literalAstToExpression() { - LiteralAst lit = new LiteralAst("1234", "fr", null); - Expression litNode = SparqlAstToExpression.convert(lit); - assertNotNull(litNode); - assertInstanceOf(Constant.class, litNode); - assertTrue(litNode.isLiteral()); - assertEquals("1234", litNode.getLabel()); - assertEquals("fr", litNode.getLang()); - } - - @Test - void varAstToExpression() { - VarAst variableAst = new VarAst("var1"); - Expression varNode = SparqlAstToExpression.convert(variableAst); - assertNotNull(varNode); - assertInstanceOf(Variable.class, varNode); - assertEquals("var1", varNode.getLabel()); - } - - @Test - void existsFilterConvertsToExistTerm() { - ExistsAst exists = new ExistsAst(new GroupGraphPatternAst(List.of())); - - Expression term = SparqlAstToExpression.convert(exists); - - assertNotNull(term); - assertEquals(ExprType.EXIST, term.oper()); - assertTrue(term.isExist()); - assertTrue(term.isRecExist()); - } - - @Test - void existsFilterRequiresWhereCompilerWhenConvertedToFilter() { - ExistsAst exists = new ExistsAst(new GroupGraphPatternAst(List.of())); - - IllegalArgumentException error = assertThrows( - IllegalArgumentException.class, - () -> SparqlAstToExpression.toNextFilter(exists)); - - assertTrue(error.getMessage().contains("WhereCompiler")); - } -} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolverTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolverTest.java new file mode 100644 index 000000000..d9b4c2f90 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/bridge/SparqlTermResolverTest.java @@ -0,0 +1,54 @@ +package fr.inria.corese.core.next.query.impl.sparql.bridge; + +import fr.inria.corese.core.next.data.api.vocabulary.RDF; +import fr.inria.corese.core.next.data.api.vocabulary.XSD; +import fr.inria.corese.core.next.query.impl.sparql.ast.IriAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.PrefixDeclarationAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryPrologueAst; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SparqlTermResolverTest { + + @Test + void resolvesTermsFromItsOwnPrologue() { + QueryPrologueAst prologue = new QueryPrologueAst( + List.of(new PrefixDeclarationAst("ex:", new IriAst("http://example.org/"))), + new IriAst("http://base.example/dir/")); + SparqlTermResolver resolver = new SparqlTermResolver(prologue); + + assertEquals("http://example.org/name", resolver.resolveIri("ex:name")); + assertEquals("http://base.example/dir/name", resolver.resolveIri("")); + assertEquals(RDF.type.getIRI().stringValue(), resolver.resolveIri("a")); + } + + @Test + void supportsStandardPrefixesWithoutLegacyNamespaceState() { + SparqlTermResolver resolver = new SparqlTermResolver(null); + + assertEquals(XSD.xsdInteger.getIRI().stringValue(), + resolver.normalizeDatatypeIri("xsd:integer")); + } + + @Test + void absoluteIriIsNeverReinterpretedAsAPrefixedName() { + QueryPrologueAst prologue = new QueryPrologueAst( + List.of(new PrefixDeclarationAst("http:", new IriAst("http://wrong.example/"))), + new IriAst("http://base.example/")); + SparqlTermResolver resolver = new SparqlTermResolver(prologue); + + assertEquals("http://example.org/resource", + resolver.resolveIri("http://example.org/resource")); + } + + @Test + void extractsLiteralLexicalForm() { + SparqlTermResolver resolver = new SparqlTermResolver(null); + + assertEquals("hello", resolver.unquoteLexical("\"hello\"@en")); + assertEquals("plain", resolver.unquoteLexical("plain")); + } +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java index e9e2b4509..51f605458 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/sparql/execution/NextSparqlPipelineExecutorTest.java @@ -1,11 +1,13 @@ package fr.inria.corese.core.next.query.impl.sparql.execution; +import fr.inria.corese.core.next.data.Values; import fr.inria.corese.core.next.data.api.term.IRI; +import fr.inria.corese.core.next.data.api.term.Literal; import fr.inria.corese.core.next.data.api.term.Resource; import fr.inria.corese.core.next.data.api.model.Statement; import fr.inria.corese.core.next.data.api.term.Value; import fr.inria.corese.core.next.data.api.factory.ValueFactory; -import fr.inria.corese.core.next.data.impl.adapter.CoreseValueFactory; +import fr.inria.corese.core.next.data.api.literal.XSDDatatype; import fr.inria.corese.core.next.query.api.dataset.Dataset; import fr.inria.corese.core.next.query.api.exception.QueryTimeoutException; import fr.inria.corese.core.next.query.api.result.Binding; @@ -46,7 +48,7 @@ class NextSparqlPipelineExecutorTest { @BeforeEach void setUp() { - valueFactory = new CoreseValueFactory(); + valueFactory = Values.factory(); storage = MemoryStorageManager.builder().build(); executor = new NextSparqlPipelineExecutor(storage); @@ -87,6 +89,206 @@ void selectJoinRunsEndToEnd() { assertFalse(result.hasNext()); } + @Test + @DisplayName("FILTER evaluates native numeric expressions") + void filterRunsThroughNativeExpressionEvaluator() { + String age = "http://example.org/age"; + insert(iri(ALICE), iri(age), valueFactory.createLiteral(42)); + insert(iri(BOB), iri(age), valueFactory.createLiteral(18)); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?person WHERE { + ?person ?age . + FILTER(?age >= 21) + } + ORDER BY ?person + """); + + assertTrue(result.hasNext()); + assertEquals(ALICE, result.next().getValue("person").stringValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("BIND evaluates native arithmetic and exposes the result") + void bindRunsThroughNativeExpressionEvaluator() { + String age = "http://example.org/age"; + insert(iri(ALICE), iri(age), valueFactory.createLiteral(41)); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?nextAge WHERE { + ?age . + BIND(?age + 1 AS ?nextAge) + } + """); + + assertTrue(result.hasNext()); + assertEquals(42, ((fr.inria.corese.core.next.data.api.term.Literal) + result.next().getValue("nextAge")).intValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("ORDER BY uses numeric value order rather than lexical order") + void orderByUsesNativeRdfValueOrder() { + String rank = "http://example.org/rank"; + insert(iri(ALICE), iri(rank), valueFactory.createLiteral(10)); + insert(iri(BOB), iri(rank), valueFactory.createLiteral(2)); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?rank WHERE { ?person ?rank } + ORDER BY ?rank + """); + + assertEquals(2, ((fr.inria.corese.core.next.data.api.term.Literal) + result.next().getValue("rank")).intValue()); + assertEquals(10, ((fr.inria.corese.core.next.data.api.term.Literal) + result.next().getValue("rank")).intValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("FILTER numeric comparison does not reuse RDF term tie-breakers") + void filterUsesValueComparisonInsteadOfTotalTermOrder() { + String rank = "http://example.org/rank"; + insert(iri(ALICE), iri(rank), valueFactory.createLiteral(1)); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?rank WHERE { + ?rank . + FILTER(?rank < 1.0) + } + """); + + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("STRLEN counts Unicode code points through the native evaluator") + void stringLengthUsesUnicodeCodePoints() { + insert(iri(ALICE), iri(NAME), valueFactory.createLiteral("A🙂")); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?length WHERE { + ?name . + BIND(STRLEN(?name) AS ?length) + } + """); + + assertTrue(result.hasNext()); + assertEquals(2, ((fr.inria.corese.core.next.data.api.term.Literal) + result.next().getValue("length")).intValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("SUBSTR uses Unicode code-point positions and preserves the language tag") + void substringUsesUnicodeCodePointPositions() { + insert(iri(ALICE), iri(NAME), valueFactory.createLiteral("A🙂B", "fr")); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?part WHERE { + ?name . + BIND(SUBSTR(?name, 2, 1) AS ?part) + } + """); + + Literal part = (Literal) result.next().getValue("part"); + assertEquals("🙂", part.getLabel()); + assertEquals("fr", part.getLanguage().orElseThrow()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("NOW returns one stable value throughout a query evaluation") + void nowIsStableWithinOneQuery() { + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?first ?second WHERE { + ?s ?p ?o . + BIND(NOW() AS ?first) + BIND(NOW() AS ?second) + } + """); + + var binding = result.next(); + assertTrue(binding.getValue("first").sameTerm(binding.getValue("second"))); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("Logical operators apply the SPARQL error truth table") + void logicalOperatorsApplySparqlErrorTruthTable() { + assertFalse(executor.evaluateTuple(""" + SELECT ?s WHERE { ?s ?p ?o . FILTER((1 / 0) && false) } + """).hasNext()); + assertTrue(executor.evaluateTuple(""" + SELECT ?s WHERE { ?s ?p ?o . FILTER((1 / 0) || true) } + """).hasNext()); + } + + @Test + @DisplayName("Arithmetic preserves SPARQL floating-point type promotion") + void arithmeticPreservesFloatingPointPromotion() { + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?sum WHERE { ?s ?p ?o . BIND(1e0 + 1 AS ?sum) } + """); + + Literal sum = (Literal) result.next().getValue("sum"); + assertEquals(XSDDatatype.DOUBLE, sum.getCoreDatatype()); + assertEquals(2.0d, sum.doubleValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("FILTER REGEX evaluates without the historical expression interpreter") + void regexFilterRunsThroughNativeExpressionEvaluator() { + insert(iri(ALICE), iri(NAME), valueFactory.createLiteral("Alice")); + insert(iri(BOB), iri(NAME), valueFactory.createLiteral("Bob")); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?person WHERE { + ?person ?name . + FILTER(REGEX(?name, "^ali", "i")) + } + """); + + assertTrue(result.hasNext()); + assertEquals(ALICE, result.next().getValue("person").stringValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("FILTER EXISTS evaluates as a correlated native graph pattern") + void correlatedExistsFilterRunsEndToEnd() { + insert(iri(BOB), iri(NAME), valueFactory.createLiteral("Bob")); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?person WHERE { + ?person ?friend . + FILTER EXISTS { ?friend ?name } + } + """); + + assertTrue(result.hasNext()); + assertEquals(ALICE, result.next().getValue("person").stringValue()); + assertFalse(result.hasNext()); + } + + @Test + @DisplayName("FILTER NOT EXISTS rejects solutions with a correlated match") + void correlatedNotExistsFilterRunsEndToEnd() { + insert(iri(BOB), iri(NAME), valueFactory.createLiteral("Bob")); + + TupleQueryResult result = executor.evaluateTuple(""" + SELECT ?person WHERE { + ?person ?friend . + FILTER NOT EXISTS { ?friend ?name } + } + """); + + assertFalse(result.hasNext()); + } + @Test @DisplayName("ASK WHERE { ?s ?p ?o } returns true when data exists") void askSpoReturnsTrueWhenDataExists() { From 7c2f608df700fc79d8ff09b0efc96508042c511b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20C=C3=A9r=C3=A8s?= Date: Mon, 7 Sep 2026 16:40:55 +0200 Subject: [PATCH 2/2] docs(kgram): document package contracts and resolve sonarlint package warnings (#568) - Add package-info.java documentation across all next.query.impl.kgram subpackages - Comply with SonarLint rule S1228 across the module --- .../next/query/impl/kgram/api/core/package-info.java | 7 +++++++ .../next/query/impl/kgram/api/query/package-info.java | 7 +++++++ .../core/next/query/impl/kgram/core/package-info.java | 8 ++++++++ .../core/next/query/impl/kgram/event/package-info.java | 2 ++ .../next/query/impl/kgram/filter/package-info.java | 2 ++ .../core/next/query/impl/kgram/package-info.java | 10 ++++++++-- .../core/next/query/impl/kgram/path/package-info.java | 2 ++ .../query/impl/kgram/sorter/core/package-info.java | 2 ++ .../impl/kgram/sorter/impl/qpv1/package-info.java | 2 ++ .../core/next/query/impl/kgram/tool/package-info.java | 4 ++++ 10 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/core/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/package-info.java create mode 100644 src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/package-info.java diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/package-info.java new file mode 100644 index 000000000..db0b8611f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/core/package-info.java @@ -0,0 +1,7 @@ +/** + * Internal KGRAM graph, node and expression contracts. + * + *

The historical {@code api} name is retained for source continuity inside + * the isolated engine. These contracts are not part of {@code next.query.api}.

+ */ +package fr.inria.corese.core.next.query.impl.kgram.api.core; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/package-info.java new file mode 100644 index 000000000..f31f7d3e8 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/api/query/package-info.java @@ -0,0 +1,7 @@ +/** + * Internal extension points used by the KGRAM evaluator. + * + *

Storage and expression adapters implement these contracts to drive the + * engine. They are implementation details rather than public query APIs.

+ */ +package fr.inria.corese.core.next.query.impl.kgram.api.query; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/package-info.java new file mode 100644 index 000000000..e9b586dae --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/core/package-info.java @@ -0,0 +1,8 @@ +/** + * KGRAM algebra plan, mappings, evaluation memory and execution loop. + * + *

This package contains the historical engine kernel. New public behavior + * belongs in {@code next.query.api}; integration code belongs in the dedicated + * bridge, execution or storage packages.

+ */ +package fr.inria.corese.core.next.query.impl.kgram.core; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/package-info.java new file mode 100644 index 000000000..8e39f66d4 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/event/package-info.java @@ -0,0 +1,2 @@ +/** Internal events emitted while evaluating a KGRAM query plan. */ +package fr.inria.corese.core.next.query.impl.kgram.event; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/package-info.java new file mode 100644 index 000000000..50fe3119e --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/filter/package-info.java @@ -0,0 +1,2 @@ +/** Internal KGRAM filter matching and validation helpers. */ +package fr.inria.corese.core.next.query.impl.kgram.filter; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/package-info.java index 843ab18bd..58567e297 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/package-info.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/package-info.java @@ -1,7 +1,13 @@ /** * Internal KGRAM execution engine used by the next SPARQL pipeline. * - *

This is a deliberately isolated fork of the legacy engine. It is implementation detail, - * even where copied subpackages retain historical names such as {@code api}.

+ *

This is a deliberately isolated, transitional fork of the historical + * engine. It is implementation detail, even where copied subpackages retain + * names such as {@code api}. Applications must use {@code next.query.api} and + * must not depend on types below this package.

+ * + *

The package preserves the KGRAM execution model while it is decomposed + * into responsibility-based {@code next} components. Its name documents that + * origin; it is not intended to become another public query API.

*/ package fr.inria.corese.core.next.query.impl.kgram; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/package-info.java new file mode 100644 index 000000000..236b161f1 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/path/package-info.java @@ -0,0 +1,2 @@ +/** Internal property-path traversal state and algorithms used by KGRAM. */ +package fr.inria.corese.core.next.query.impl.kgram.path; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/core/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/core/package-info.java new file mode 100644 index 000000000..b54c0f82c --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/core/package-info.java @@ -0,0 +1,2 @@ +/** Internal query-plan cost model and ordering contracts. */ +package fr.inria.corese.core.next.query.impl.kgram.sorter.core; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/package-info.java new file mode 100644 index 000000000..ee48ff87b --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/sorter/impl/qpv1/package-info.java @@ -0,0 +1,2 @@ +/** First-generation heuristic implementation of KGRAM query-plan ordering. */ +package fr.inria.corese.core.next.query.impl.kgram.sorter.impl.qpv1; diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/package-info.java b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/package-info.java new file mode 100644 index 000000000..b19c8213c --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/kgram/tool/package-info.java @@ -0,0 +1,4 @@ +/** + * Internal adapters between KGRAM contracts and Corese-next runtime values or storage. + */ +package fr.inria.corese.core.next.query.impl.kgram.tool;