diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/FilterArgumentsValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/FilterArgumentsValidationRule.java index 5b9f99f24..a95607fca 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/FilterArgumentsValidationRule.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/FilterArgumentsValidationRule.java @@ -1,14 +1,13 @@ package fr.inria.corese.core.next.query.impl.parser.semantic.rule; -import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; import fr.inria.corese.core.next.query.impl.parser.semantic.support.AbstractAstVisitor; import fr.inria.corese.core.next.query.impl.sparql.ast.*; -import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; import java.util.ArrayList; import java.util.List; -import java.util.Set; + +import static fr.inria.corese.core.next.query.impl.parser.semantic.support.SemanticValidationUtils.*; /** * Validates that FILTER and HAVING expressions are compatible with SPARQL @@ -16,26 +15,6 @@ */ public final class FilterArgumentsValidationRule extends AbstractSemanticValidationRule { - private static final String BOOLEAN_DATATYPE = XSD.xsdBoolean.getIRI().stringValue(); - private static final String STRING_DATATYPE = XSD.xsdString.getIRI().stringValue(); - private static final Set NUMERIC_DATATYPES = Set.of( - XSD.xsdInteger.getIRI().stringValue(), - XSD.xsdNonNegativeInteger.getIRI().stringValue(), - XSD.xsdNonPositiveInteger.getIRI().stringValue(), - XSD.xsdPositiveInteger.getIRI().stringValue(), - XSD.xsdNegativeInteger.getIRI().stringValue(), - XSD.xsdInt.getIRI().stringValue(), - XSD.xsdUnsignedInt.getIRI().stringValue(), - XSD.xsdLong.getIRI().stringValue(), - XSD.xsdUnsignedLong.getIRI().stringValue(), - XSD.xsdDecimal.getIRI().stringValue(), - XSD.xsdShort.getIRI().stringValue(), - XSD.xsdUnsignedShort.getIRI().stringValue(), - XSD.xsdByte.getIRI().stringValue(), - XSD.xsdUnsignedByte.getIRI().stringValue(), - XSD.xsdFloat.getIRI().stringValue(), - XSD.xsdDouble.getIRI().stringValue()); - @Override protected String getDiagnosticSource() { return FilterArgumentsValidationRule.class.getSimpleName(); @@ -48,52 +27,6 @@ public List validate(QueryAst queryAst) { return visitor.getResult(); } - /** - * Checks whether the given term is statically compatible with SPARQL - * effective boolean value evaluation. - */ - private static boolean isPotentiallyEbvCompatible(TermAst termAst) { - if (termAst instanceof BooleanExpressionAst - || termAst instanceof NumericExpressionAst - || termAst instanceof VarAst - || termAst instanceof FunctionCallAst - || termAst instanceof UnlimitedArgumentsFunctionAst) { - return true; - } - - if (termAst instanceof LiteralAst literalAst) { - return isEbvCompatibleLiteral(literalAst); - } - - if (termAst instanceof IfAst(TermAst condition, TermAst thenExpr, TermAst elseExpr)) { - return isPotentiallyEbvCompatible(condition) - && isPotentiallyEbvCompatible(thenExpr) - && isPotentiallyEbvCompatible(elseExpr); - } - - if (termAst instanceof LiteralExpressionAst literalExpressionAst) { - return !(literalExpressionAst instanceof XsdDateTimeExpressionAst - || literalExpressionAst instanceof XsdDayTimeDurationExpressionAst); - } - - return false; - } - - private static boolean isEbvCompatibleLiteral(LiteralAst literalAst) { - if (literalAst.lang() != null && !literalAst.lang().isBlank()) { - return true; - } - - String datatype = literalAst.datatype(); - if (datatype == null) { - return true; - } - - return BOOLEAN_DATATYPE.equals(datatype) - || STRING_DATATYPE.equals(datatype) - || NUMERIC_DATATYPES.contains(datatype); - } - private class FilterArgumentsValidationVisitor extends AbstractAstVisitor { private final List result = new ArrayList<>(); @@ -103,7 +36,7 @@ public List getResult() { @Override public void visit(PatternAst patternAst) { - if (patternAst instanceof FilterAst(TermAst operator) && !isPotentiallyEbvCompatible(operator)) { + if (patternAst instanceof FilterAst(TermAst operator) && !isPotentialBooleanCompatible(operator)) { result.add(buildIncorrectTypeDiagnostic(operator.getName(), "FILTER", "boolean")); } } @@ -111,7 +44,7 @@ public void visit(PatternAst patternAst) { @Override public void visit(HavingAst havingAst) { for (TermAst condition : havingAst.conditions()) { - if (!isPotentiallyEbvCompatible(condition)) { + if (!isPotentialBooleanCompatible(condition)) { result.add(buildIncorrectTypeDiagnostic(condition.getName(), "HAVING", "boolean")); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentBooleanTypeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentBooleanTypeValidationRule.java new file mode 100644 index 000000000..020a16ae2 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentBooleanTypeValidationRule.java @@ -0,0 +1,67 @@ +package fr.inria.corese.core.next.query.impl.parser.semantic.rule; + +import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +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.BooleanNotAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.OrAst; + +import java.util.ArrayList; +import java.util.List; + +import static fr.inria.corese.core.next.query.impl.parser.semantic.support.SemanticValidationUtils.isPotentialBooleanCompatible; + + +public class OperandArgumentBooleanTypeValidationRule extends AbstractSemanticValidationRule { + private static final String BOOLEAN_TYPE = "boolean"; + + @Override + protected String getDiagnosticSource() { + return OperandArgumentBooleanTypeValidationRule.class.getSimpleName(); + } + + @Override + public List validate(QueryAst queryAst) { + OperandBooleanArgumentTypeVisitor visitor = new OperandBooleanArgumentTypeVisitor(); + queryAst.accept(visitor); + return visitor.getResult(); + } + + private class OperandBooleanArgumentTypeVisitor extends AbstractAstVisitor { + private final List result = new ArrayList<>(); + + public List getResult() { + return result; + } + + @Override + public void visit(TermAst termAst) { + if (termAst instanceof BooleanNotAst booleanNotAst + && !isPotentialBooleanCompatible(booleanNotAst.argument())) { + result.add(buildIncorrectTypeDiagnostic( + booleanNotAst.argument().getName(), + booleanNotAst.getName(), + BOOLEAN_TYPE)); + } + + if (termAst instanceof BinaryConstraintAst binaryConstraintAst + && (binaryConstraintAst instanceof AndAst || binaryConstraintAst instanceof OrAst)) { + if (!isPotentialBooleanCompatible(binaryConstraintAst.getLeftArgument())) { + result.add(buildIncorrectTypeDiagnostic( + binaryConstraintAst.getLeftArgument().getName(), + binaryConstraintAst.getName(), + BOOLEAN_TYPE)); + } + if (!isPotentialBooleanCompatible(binaryConstraintAst.getRightArgument())) { + result.add(buildIncorrectTypeDiagnostic( + binaryConstraintAst.getRightArgument().getName(), + binaryConstraintAst.getName(), + BOOLEAN_TYPE)); + } + } + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentIRITypeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentIRITypeValidationRule.java new file mode 100644 index 000000000..d979f769a --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentIRITypeValidationRule.java @@ -0,0 +1,61 @@ +package fr.inria.corese.core.next.query.impl.parser.semantic.rule; + +import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; + +import java.util.ArrayList; +import java.util.List; + +import static fr.inria.corese.core.next.query.impl.parser.semantic.support.SemanticValidationUtils.isPotentialIri; + +/** + * Check that the comparison operand do not compare IRIs (except for the different != operator) + */ +public class OperandArgumentIRITypeValidationRule extends AbstractSemanticValidationRule { + + @Override + protected String getDiagnosticSource() { + return OperandArgumentIRITypeValidationRule.class.getSimpleName(); + } + + @Override + public List validate(QueryAst queryAst) { + OperandIRIArgumentTypeVisitor visitor = new OperandIRIArgumentTypeVisitor(); + queryAst.accept(visitor); + return visitor.getResult(); + } + + private class OperandIRIArgumentTypeVisitor extends AbstractAstVisitor { + private final List result = new ArrayList<>(); + + public List getResult() { + return result; + } + + @Override + public void visit(TermAst termAst) { + if (termAst instanceof BinaryConstraintAst binaryConstraintAst + && (binaryConstraintAst instanceof LowerThanAst + || binaryConstraintAst instanceof LowerOrEqualThanAst + || binaryConstraintAst instanceof GreaterThanAst + || binaryConstraintAst instanceof GreaterOrEqualThanAst)) { + if (isPotentialIri(binaryConstraintAst.getLeftArgument())) { + result.add(buildIncorrectTypeDiagnostic( + binaryConstraintAst.getLeftArgument().getName(), + binaryConstraintAst.getName(), + "not an IRI")); + } + if (isPotentialIri(binaryConstraintAst.getRightArgument())) { + result.add(buildIncorrectTypeDiagnostic( + binaryConstraintAst.getRightArgument().getName(), + binaryConstraintAst.getName(), + "not an IRI")); + } + } + } + } + +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentNumericTypeValidationRule.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentNumericTypeValidationRule.java new file mode 100644 index 000000000..45d341d6f --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/rule/OperandArgumentNumericTypeValidationRule.java @@ -0,0 +1,75 @@ +package fr.inria.corese.core.next.query.impl.parser.semantic.rule; + +import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; +import fr.inria.corese.core.next.query.impl.parser.semantic.support.AbstractAstVisitor; +import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; +import fr.inria.corese.core.next.query.impl.sparql.ast.constraint.*; + +import java.util.ArrayList; +import java.util.List; + +import static fr.inria.corese.core.next.query.impl.parser.semantic.support.SemanticValidationUtils.isPotentialNumeric; + +/** + * Check that the operands and numeric functions use numeric arguments + */ +public final class OperandArgumentNumericTypeValidationRule extends AbstractSemanticValidationRule { + private static final String NUMERIC_TYPE = "numeric"; + + @Override + protected String getDiagnosticSource() { + return OperandArgumentNumericTypeValidationRule.class.getSimpleName(); + } + + @Override + public List validate(QueryAst queryAst) { + OperandNumericArgumentTypeVisitor visitor = new OperandNumericArgumentTypeVisitor(); + queryAst.accept(visitor); + return visitor.getResult(); + } + + private class OperandNumericArgumentTypeVisitor extends AbstractAstVisitor { + private final List result = new ArrayList<>(); + + public List getResult() { + return result; + } + + @Override + public void visit(TermAst termAst) { + if (termAst instanceof UnaryConstraintAst unaryConstraintAst + && (unaryConstraintAst instanceof AbsAst + || unaryConstraintAst instanceof CeilAst + || unaryConstraintAst instanceof FloorAst + || unaryConstraintAst instanceof RoundAst + || unaryConstraintAst instanceof UnaryMinusAst + || unaryConstraintAst instanceof UnaryPlusAst) + && !isPotentialNumeric(unaryConstraintAst.argument())) { + result.add(buildIncorrectTypeDiagnostic( + unaryConstraintAst.argument().getName(), + unaryConstraintAst.getName(), + NUMERIC_TYPE)); + } + + if (termAst instanceof BinaryConstraintAst binaryConstraintAst + && (binaryConstraintAst instanceof AddAst + || binaryConstraintAst instanceof DivideAst + || binaryConstraintAst instanceof MultiplyAst + || binaryConstraintAst instanceof SubtractAst)) { + if (!isPotentialNumeric(binaryConstraintAst.getLeftArgument())) { + result.add(buildIncorrectTypeDiagnostic( + binaryConstraintAst.getLeftArgument().getName(), + binaryConstraintAst.getName(), + NUMERIC_TYPE)); + } + if (!isPotentialNumeric(binaryConstraintAst.getRightArgument())) { + result.add(buildIncorrectTypeDiagnostic( + binaryConstraintAst.getRightArgument().getName(), + binaryConstraintAst.getName(), + NUMERIC_TYPE)); + } + } + } + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/SemanticValidationUtils.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/SemanticValidationUtils.java new file mode 100644 index 000000000..13d9b7c85 --- /dev/null +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/SemanticValidationUtils.java @@ -0,0 +1,176 @@ +package fr.inria.corese.core.next.query.impl.parser.semantic.support; + +import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; +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 java.util.Set; + +public class SemanticValidationUtils { + + private static final String BOOLEAN_DATATYPE = XSD.xsdBoolean.getIRI().stringValue(); + private static final Set STRING_DATATYPE = Set.of( + XSD.xsdString.getIRI().stringValue(), + RDF.langString.getIRI().stringValue()); + + /** + * Valid numeric datatypes as defined in https://www.w3.org/TR/sparql11-query/#operandDataTypes + */ + private static final Set NUMERIC_DATATYPES = Set.of( + XSD.xsdInteger.getIRI().stringValue(), + XSD.xsdNonNegativeInteger.getIRI().stringValue(), + XSD.xsdNonPositiveInteger.getIRI().stringValue(), + XSD.xsdPositiveInteger.getIRI().stringValue(), + XSD.xsdNegativeInteger.getIRI().stringValue(), + XSD.xsdInt.getIRI().stringValue(), + XSD.xsdUnsignedInt.getIRI().stringValue(), + XSD.xsdLong.getIRI().stringValue(), + XSD.xsdUnsignedLong.getIRI().stringValue(), + XSD.xsdDecimal.getIRI().stringValue(), + XSD.xsdShort.getIRI().stringValue(), + XSD.xsdUnsignedShort.getIRI().stringValue(), + XSD.xsdByte.getIRI().stringValue(), + XSD.xsdUnsignedByte.getIRI().stringValue(), + XSD.xsdFloat.getIRI().stringValue(), + XSD.xsdDouble.getIRI().stringValue()); + + private SemanticValidationUtils() { + } + + public static boolean isBooleanCompatible(LiteralAst literalAst) { + if (literalAst.lang() != null && !literalAst.lang().isBlank()) { + return true; + } + + String datatype = literalAst.datatype(); + if (datatype == null) { + return true; + } + + return BOOLEAN_DATATYPE.equals(datatype) + || STRING_DATATYPE.contains(datatype) + || NUMERIC_DATATYPES.contains(datatype); + } + + /** + * Checks whether the given term is statically compatible with SPARQL + * effective boolean value evaluation. + */ + public static boolean isPotentialBooleanCompatible(TermAst termAst) { + if (termAst instanceof BooleanExpressionAst + || termAst instanceof NumericExpressionAst + || termAst instanceof VarAst + || termAst instanceof FunctionCallAst + || termAst instanceof UnlimitedArgumentsFunctionAst) { + return true; + } + + if (termAst instanceof LiteralAst literalAst) { + return isBooleanCompatible(literalAst); + } + + if (termAst instanceof IfAst(TermAst condition, TermAst thenExpr, TermAst elseExpr)) { + return isPotentialBooleanCompatible(condition) + && isPotentialBooleanCompatible(thenExpr) + && isPotentialBooleanCompatible(elseExpr); + } + + if (termAst instanceof LiteralExpressionAst literalExpressionAst) { + return !(literalExpressionAst instanceof XsdDateTimeExpressionAst + || literalExpressionAst instanceof XsdDayTimeDurationExpressionAst); + } + + return false; + } + + /** + * Check that the given term is either numeric expression, literal expression typed by a standard numeric datatype, a literal that can be parsed to a numeric or a variable + */ + public static boolean isPotentialNumeric(TermAst termAst) { + if (isUnknownType(termAst)) { + return true; + } + if (termAst instanceof IfAst ifAst + && isPotentialNumeric(ifAst.thenExpr()) + && isPotentialNumeric(ifAst.elseExpr())) { // Is an IF that returns potential numerics + return true; + } + if (termAst instanceof NumericExpressionAst) { // Is a literal expression that could be a numeric, we cannot know + return true; + } + if (termAst instanceof LiteralAst(String lexical, String lang, String datatype)) { + if(datatype != null) { + return NUMERIC_DATATYPES.contains(datatype); // Datatype is an URI of an standard numeric datatype + } else { + return isNumeric(lexical); // The string value can be parsed to a numeric value + } + } + + return false; + } + + /** + * Check if the given term type cannot be determined at query parsing, it will be checked at query resolution. + * + */ + public static boolean isUnknownType(TermAst termAst) { + return termAst instanceof VarAst + || termAst instanceof FunctionCallAst; + } + + /** + * Tries to parse the string as a Double + */ + public static boolean isNumeric(String lexical) { + if (lexical == null) { + return false; + } + try { + Double.parseDouble(lexical); + } catch (NumberFormatException nfe) { + return false; + } + return true; + } + + /** + * Check if the term is either a variable or an IRI or an expression that can be resolved to an IRI + */ + public static boolean isPotentialIri(TermAst termAst) { + if(termAst instanceof IriAst) { // Is an IRI + return true; + } + // Is a function that can be resolved to an IRI + return termAst instanceof IriExpressionAst; + } + + /** + * Check if the term is a literal that can be considered as a string argument as defined in the SPARQL 1.1 recommendation + */ + public static boolean isStringLiteral(TermAst termAst) { + if(termAst instanceof LiteralAst(String lexical, String lang, String datatype) && lexical != null) { + if(lang != null) { + return true; + } + if(datatype == null) { + return true; + } + if(STRING_DATATYPE.contains(datatype)) { + return true; + } + } + return false; + } + + + /** + * Check if the term is either a variable or a literal that can be considered as a string argument as defined in the SPARQL 1.1 recommendation + */ + public static boolean isPotentialStringLiteral(TermAst termAst) { + return isUnknownType(termAst) || isStringLiteral(termAst) || termAst instanceof SimpleLiteralExpressionAst; + } +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/validator/SparqlQuerySemanticValidator.java b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/validator/SparqlQuerySemanticValidator.java index 9459d1758..cd311f4b5 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/validator/SparqlQuerySemanticValidator.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/parser/semantic/validator/SparqlQuerySemanticValidator.java @@ -3,15 +3,7 @@ import fr.inria.corese.core.next.query.api.validation.QueryDiagnostic; import fr.inria.corese.core.next.query.api.validation.QueryValidationResult; import fr.inria.corese.core.next.query.api.validation.QueryValidator; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.FilterArgumentsValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.GroupByScopeValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.GroupedHavingValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.GroupedOrderByValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.GroupedSelectProjectionValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.HavingScopeValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.OrderByScopeValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.SelectProjectionScopeValidationRule; -import fr.inria.corese.core.next.query.impl.parser.semantic.rule.SemanticValidationRule; +import fr.inria.corese.core.next.query.impl.parser.semantic.rule.*; import fr.inria.corese.core.next.query.impl.sparql.ast.QueryAst; import java.util.ArrayList; @@ -34,7 +26,10 @@ public SparqlQuerySemanticValidator() { new GroupByScopeValidationRule(), new HavingScopeValidationRule(), new OrderByScopeValidationRule(), - new FilterArgumentsValidationRule())); + new FilterArgumentsValidationRule(), + new OperandArgumentNumericTypeValidationRule(), + new OperandArgumentBooleanTypeValidationRule(), + new OperandArgumentIRITypeValidationRule())); } /** Package-private for tests or future composition of rule sets. */ diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java index 8013ae693..20a8a9ea6 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/UpdateRequestAst.java @@ -20,7 +20,8 @@ public void addQuery(UpdateRequestUnitAst updateQuery) { @Override public void accept(AstVisitor visitor) { - visitor.visit(this.prologue); + visitor.visit(this); + this.prologue.accept(visitor); this.operations.forEach(updateRequestUnitAst -> updateRequestUnitAst.accept(visitor)); } } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ConcatAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ConcatAst.java index 451343b7e..cb7e62219 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ConcatAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/ConcatAst.java @@ -7,7 +7,7 @@ /** * Function {@code CONCAT(expr1, expr2, ...)} in SPARQL 1.1. */ -public class ConcatAst extends AbstractUnlimitedArgumentsFunctionAst { +public class ConcatAst extends AbstractUnlimitedArgumentsFunctionAst implements SimpleLiteralExpressionAst { public ConcatAst(List arguments) { super(arguments); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SameTermAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SameTermAst.java index 0285195ba..b1e686200 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SameTermAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/SameTermAst.java @@ -2,8 +2,6 @@ import fr.inria.corese.core.next.query.api.exception.QuerySyntaxException; import fr.inria.corese.core.next.query.impl.sparql.ast.TermAst; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.List; @@ -11,11 +9,9 @@ * Function {@code sameTerm(term1, term2)} */ public class SameTermAst extends AbstractBinaryFunctionAst implements BooleanExpressionAst { - private static final Logger logger = LoggerFactory.getLogger(SameTermAst.class); public SameTermAst(List args) { super(args); - logger.debug("{}", args); if (args.size() != 2) { throw new QuerySyntaxException("Unexpected number of arguments for sameTerm function"); } diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrLenAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrLenAst.java index 0797c921d..87e9876db 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrLenAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrLenAst.java @@ -8,7 +8,7 @@ * Function {@code STRLEN(string)} in SPARQL 1.1 * Returns the length of a string. */ -public class StrLenAst extends AbstractUnaryConstraintAst { +public class StrLenAst extends AbstractUnaryConstraintAst implements NumericExpressionAst { public StrLenAst(TermAst arg) { super(arg); diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java index a5fd07e3d..ec5a2b7e2 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/StrUuidAst.java @@ -1,13 +1,12 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst; /** * Function {@code STRUUID()} in SPARQL 1.1 * Returns a string form of a UUID. */ -public record StrUuidAst() implements ConstraintAst { +public record StrUuidAst() implements SimpleLiteralExpressionAst { @Override public String getName() { @@ -18,4 +17,4 @@ public String getName() { public void accept(AstVisitor visitor) { visitor.visit(this); } -} \ No newline at end of file +} diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java index 9d9e6b6a5..ced9cc97d 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/ast/constraint/UuidAst.java @@ -1,13 +1,12 @@ package fr.inria.corese.core.next.query.impl.sparql.ast.constraint; import fr.inria.corese.core.next.query.impl.parser.semantic.support.AstVisitor; -import fr.inria.corese.core.next.query.impl.sparql.ast.ConstraintAst; /** * Function {@code UUID()} in SPARQL 1.1 * Returns a fresh IRI from the UUID URN scheme. */ -public record UuidAst() implements ConstraintAst { +public record UuidAst() implements IriExpressionAst { @Override public String getName() { @@ -18,4 +17,4 @@ public String getName() { public void accept(AstVisitor visitor) { visitor.visit(this); } -} \ No newline at end of file +} diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserFilterTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserFilterTest.java index a7cb966d6..59c18b66c 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserFilterTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserFilterTest.java @@ -483,7 +483,7 @@ void shouldParseLowerFilter() { SparqlQueryAst ast = (SparqlQueryAst) parser.parse(""" SELECT * WHERE { ?s ?p ?o . - FILTER(?s < ) + FILTER(?s < 2) } """); @@ -501,11 +501,11 @@ void shouldParseLowerFilter() { LowerThanAst t = (LowerThanAst) filterAst.operator(); - assertInstanceOf(VarAst.class, t.getLeftArgument()); - assertInstanceOf(IriAst.class, t.getRightArgument()); + VarAst varAst = assertInstanceOf(VarAst.class, t.getLeftArgument()); + LiteralAst literalAst = assertInstanceOf(LiteralAst.class, t.getRightArgument()); - assertEquals("s", ((VarAst) t.getLeftArgument()).name()); - assertEquals("", ((IriAst) t.getRightArgument()).raw()); + assertEquals("s", varAst.name()); + assertEquals("2", literalAst.lexical()); } @Test @@ -515,7 +515,7 @@ void shouldParseLowerOrEqualFilter() { SparqlQueryAst ast = (SparqlQueryAst) parser.parse(""" SELECT * WHERE { ?s ?p ?o . - FILTER(?s <= ) + FILTER(?s <= 3) } """); @@ -533,11 +533,11 @@ void shouldParseLowerOrEqualFilter() { LowerOrEqualThanAst t = (LowerOrEqualThanAst) filterAst.operator(); - assertInstanceOf(VarAst.class, t.getLeftArgument()); - assertInstanceOf(IriAst.class, t.getRightArgument()); + VarAst varAst = assertInstanceOf(VarAst.class, t.getLeftArgument()); + LiteralAst literalAst = assertInstanceOf(LiteralAst.class, t.getRightArgument()); - assertEquals("s", ((VarAst) t.getLeftArgument()).name()); - assertEquals("", ((IriAst) t.getRightArgument()).raw()); + assertEquals("s", varAst.name()); + assertEquals("3", literalAst.lexical()); } @Test @@ -547,7 +547,7 @@ void shouldParseGreaterFilter() { SparqlQueryAst ast = (SparqlQueryAst) parser.parse(""" SELECT * WHERE { ?s ?p ?o . - FILTER(?s > ) + FILTER(?s > 4) } """); @@ -565,11 +565,11 @@ void shouldParseGreaterFilter() { GreaterThanAst t = (GreaterThanAst) filterAst.operator(); - assertInstanceOf(VarAst.class, t.getLeftArgument()); - assertInstanceOf(IriAst.class, t.getRightArgument()); + VarAst varAst = assertInstanceOf(VarAst.class, t.getLeftArgument()); + LiteralAst literalAst = assertInstanceOf(LiteralAst.class, t.getRightArgument()); - assertEquals("s", ((VarAst) t.getLeftArgument()).name()); - assertEquals("", ((IriAst) t.getRightArgument()).raw()); + assertEquals("s", varAst.name()); + assertEquals("4", literalAst.lexical()); } @Test @@ -579,7 +579,7 @@ void shouldParseGreaterOrEqualFilter() { SparqlQueryAst ast = (SparqlQueryAst) parser.parse(""" SELECT * WHERE { ?s ?p ?o . - FILTER(?s >= ) + FILTER(?s >= 5) } """); @@ -597,11 +597,11 @@ void shouldParseGreaterOrEqualFilter() { GreaterOrEqualThanAst t = (GreaterOrEqualThanAst) filterAst.operator(); - assertInstanceOf(VarAst.class, t.getLeftArgument()); - assertInstanceOf(IriAst.class, t.getRightArgument()); + VarAst varAst = assertInstanceOf(VarAst.class, t.getLeftArgument()); + LiteralAst literalAst = assertInstanceOf(LiteralAst.class, t.getRightArgument()); - assertEquals("s", ((VarAst) t.getLeftArgument()).name()); - assertEquals("", ((IriAst) t.getRightArgument()).raw()); + assertEquals("s", varAst.name()); + assertEquals("5", literalAst.lexical()); } @Test diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java index acb7c1850..19dcfbe1c 100644 --- a/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/SparqlParserValidationTest.java @@ -146,6 +146,19 @@ void shouldRejectBindVariableAlreadyVisibleFromPreviousBind() { @Nested class FilterValidationTest { + @Test + void shouldRejectConcatUsedAsNumericOperand() { + SparqlParser parser = newParserDefault(); + + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(CONCAT("1", "2") + 1 = ?o) + } + """)); + + assertEquals("CONCAT used in + should be resolvable to a numeric", exception.getMessage()); + } @Test @DisplayName("Should accept FILTER with numeric operator") @@ -345,6 +358,274 @@ void shouldAcceptFilterWithNumericOperatorInNestedBGP() { } } + @Nested + public class OperandTypeTest { + + @Test + @DisplayName("Should accept + operator with numerics") + void shouldAcceptPlusWithNumerics() { + SparqlParser parser = newParserDefault(); + assertDoesNotThrow(() -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(RAND() + 1 = ?o) + } + """); + }); + } + + @Test + @DisplayName("Should accept - operator with numerics") + void shouldAcceptMinusWithNumerics() { + SparqlParser parser = newParserDefault(); + assertDoesNotThrow(() -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(STRLEN("test") - 1 = ?o) + } + """); + }); + } + + @Test + @DisplayName("Should accept * operator with numerics") + void shouldAcceptMultiplyWithNumerics() { + SparqlParser parser = newParserDefault(); + assertDoesNotThrow(() -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(STRLEN("test") * RAND() = ?o) + } + """); + }); + } + + @Test + @DisplayName("Should accept / operator with numerics") + void shouldAcceptDivideWithNumerics() { + SparqlParser parser = newParserDefault(); + assertDoesNotThrow(() -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(DAY(NOW()) / 2 = ?o) + } + """); + }); + } + + @Test + @DisplayName("Should reject + operator with non numerics") + void shouldRefusePlusWithNonNumerics() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(RAND() + "one" = ?o) + } + """); + }); + assertEquals("\"one\" used in + should be resolvable to a numeric", exception.getMessage()); + } + + @Test + @DisplayName("Should reject - operator with non numerics") + void shouldRejectMinusWithNonNumerics() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER((3 - STRENDS("test", "")) = ?o) + } + """); + }); + assertEquals("STRENDS used in - should be resolvable to a numeric", exception.getMessage()); + } + + @Test + @DisplayName("Should reject * operator with non numerics") + void shouldRejectDivideWithNonNumerics() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER( / 2 = ?o) + } + """); + }); + assertEquals(" used in / should be resolvable to a numeric", exception.getMessage()); + } + + @Test + @DisplayName("Should reject / operator with non numerics") + void shouldRejectMultiplyWithNonNumerics() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(STRLEN("test") * NOW() = ?o) + } + """); + }); + assertEquals("NOW used in * should be resolvable to a numeric", exception.getMessage()); + } + + @Test + @DisplayName("Should accept || operator with booleans") + void shouldAcceptOrWithBoolean() { + SparqlParser parser = newParserDefault(); + assertDoesNotThrow(() -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(IsIri(?s) || false) + } + """); + }); + } + + @Test + @DisplayName("Should accept && operator with booleans") + void shouldAcceptAndWithBoolean() { + SparqlParser parser = newParserDefault(); + assertDoesNotThrow(() -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(NOT EXISTS { ?s ?p false } && STRSTARTS("test", "t")) + } + """); + }); + } + + @Test + @DisplayName("Should reject || operator with non booleans") + void shouldRejectOrWithNonBoolean() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(IsIri(?s) || "potato"^^) + } + """); + }); + assertEquals("\"potato\"^^ used in || should be resolvable to a boolean", exception.getMessage()); + } + + @Test + @DisplayName("Should reject && operator with non booleans") + void shouldRejectAndWithNonBoolean() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER( && langMatches("potato", "fr")) + } + """); + }); + assertEquals(" used in && should be resolvable to a boolean", exception.getMessage()); + } + + @Test + @DisplayName("Should accept ! operator with booleans") + void shouldAcceptNotWithBoolean() { + SparqlParser parser = newParserDefault(); + assertDoesNotThrow(() -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(! NOT EXISTS { ?s ?p false } && STRSTARTS("test", "t")) + } + """); + }); + } + + @Test + @DisplayName("Should reject ! operator with non booleans") + void shouldRejectNotWithNonBoolean() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(! ) + } + """); + }); + assertEquals(" used in ! should be resolvable to a boolean", exception.getMessage()); + } + + @Test + @DisplayName("Should reject < operator with IRIs") + void shouldRejectLTWithIRIs() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(2 < ) + } + """); + }); + assertEquals(" used in < should be resolvable to a not an IRI", exception.getMessage()); + } + + @Test + @DisplayName("Should reject <= operator with IRIs") + void shouldRejectLTEWithIRIs() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(STRLEN("test") <= ) + } + """); + }); + assertEquals(" used in <= should be resolvable to a not an IRI", exception.getMessage()); + } + + @Test + @DisplayName("Should reject > operator with IRIs") + void shouldRejectGTWithIRIs() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER("2"^^ > ) + } + """); + }); + assertEquals(" used in > should be resolvable to a not an IRI", exception.getMessage()); + } + + @Test + @DisplayName("Should reject >= operator with IRIs") + void shouldRejectGTEWithIRIs() { + SparqlParser parser = newParserDefault(); + QueryValidationException exception = assertThrows(QueryValidationException.class, () -> { + parser.parse(""" + SELECT * WHERE { + ?x ?p ?o . + FILTER(datatype("2"^^) >= 4) + } + """); + }); + assertEquals("DATATYPE used in >= should be resolvable to a not an IRI", exception.getMessage()); + } + + } + private static Stream invalidSelectQueries() { return Stream.of( Arguments.of( diff --git a/src/test/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/SemanticValidationUtilsTest.java b/src/test/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/SemanticValidationUtilsTest.java new file mode 100644 index 000000000..c63d5fdf5 --- /dev/null +++ b/src/test/java/fr/inria/corese/core/next/query/impl/parser/semantic/support/SemanticValidationUtilsTest.java @@ -0,0 +1,73 @@ +package fr.inria.corese.core.next.query.impl.parser.semantic.support; + +import fr.inria.corese.core.next.data.impl.common.vocabulary.RDF; +import fr.inria.corese.core.next.data.impl.common.vocabulary.XSD; +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.*; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static fr.inria.corese.core.next.query.impl.parser.semantic.support.SemanticValidationUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +class SemanticValidationUtilsTest { + + @Test + void checkTermIsPotentialBooleanTest() { + assertTrue(isPotentialBooleanCompatible(new LiteralAst("true", null, null))); + assertTrue(isPotentialBooleanCompatible(new LiteralAst("potato", null, XSD.xsdBoolean.getIRI().stringValue()))); + assertTrue(isPotentialBooleanCompatible(new AndAst(List.of(new LiteralAst("true", null, null), new LiteralAst("true", null, null))))); + assertTrue(isPotentialBooleanCompatible(new VarAst("test"))); + assertFalse(isPotentialBooleanCompatible(new IriAst(""))); + assertFalse(isPotentialBooleanCompatible(new NowAst())); + } + + @Test + void isPotentialNumericTest() { + assertTrue(isPotentialNumeric(new LiteralAst("1", null, null))); + assertTrue(isPotentialNumeric(new LiteralAst("abc", null, XSD.xsdDouble.getIRI().stringValue()))); + assertTrue(isPotentialNumeric(new VarAst("test"))); + assertFalse(isPotentialNumeric(new IriAst(""))); + assertFalse(isPotentialNumeric(new NowAst())); + assertTrue(isPotentialNumeric(new AddAst(List.of(new LiteralAst("1", null, null), new LiteralAst("2", null, null))))); + } + + @Test + void isUnknownTypeTest() { + assertTrue(isUnknownType(new VarAst("test"))); + assertFalse(isUnknownType(new LiteralAst("true", null, null))); + assertFalse(isUnknownType(new NowAst())); + } + + @Test + void isNumericTest() { + assertTrue(isNumeric("1")); + assertFalse(isNumeric("Potato")); + } + + @Test + void isPotentialIriTest() { + assertTrue(isPotentialIri(new IriAst(""))); + assertTrue(isPotentialIri(new DatatypeAst(List.of(new LiteralAst("1", null, null))))); + assertFalse(isPotentialIri(new VarAst("test"))); + assertFalse(isPotentialIri(new LiteralAst("1", null, null))); + assertFalse(isPotentialIri(new NowAst())); + } + + @Test + void isPotentialStringLiteralTest() { + assertTrue(isPotentialStringLiteral(new LiteralAst("test", null, null))); + assertTrue(isPotentialStringLiteral(new LiteralAst("test", "en", null))); + assertTrue(isPotentialStringLiteral(new LiteralAst("test", null, RDF.langString.getIRI().stringValue()))); + assertTrue(isPotentialStringLiteral(new LiteralAst("test", null, XSD.xsdString.getIRI().stringValue()))); + assertTrue(isPotentialStringLiteral(new VarAst("test"))); + assertTrue(isPotentialStringLiteral(new ConcatAst(List.of()))); + assertFalse(isPotentialStringLiteral(new LiteralAst("test", null, XSD.xsdDouble.getIRI().stringValue()))); + assertFalse(isPotentialStringLiteral(new DatatypeAst(List.of(new LiteralAst("1", null, null))))); + assertFalse(isPotentialStringLiteral(new IriAst(""))); + assertFalse(isPotentialStringLiteral(new NowAst())); + } +} \ No newline at end of file