+ * A PathExpr can be wrapped in a DynamicCardinalityCheck, which is why this has to happen in series. + *
+ * + * @param expr to unwrap + * @return unwrapped expression + */ + private static Expression unwrapSubExpression(Expression expr) { if (expr instanceof Atomize atomize) { expr = atomize.getExpression(); } - if (expr instanceof DynamicCardinalityCheck cardinalityCheck - && expr.getSubExpressionCount() == 1) { + if (expr instanceof DynamicCardinalityCheck cardinalityCheck && expr.getSubExpressionCount() == 1) { expr = cardinalityCheck.getSubExpression(0); } - if (expr instanceof PathExpr pathExpr && - expr.getSubExpressionCount() == 1) { + if (expr instanceof PathExpr pathExpr && expr.getSubExpressionCount() == 1) { expr = pathExpr.getSubExpression(0); } return expr; } - private LocationStep findLocationStep(final Expression expr) { + private static LocationStep findLocationStep(final Expression expr) { if (expr instanceof LocationStep step) { return step; } @@ -320,7 +244,7 @@ private LocationStep findLocationStep(final Expression expr) { return null; } - private AtomicValue findAtomicValue(final Expression expr) { + private static AtomicValue findAtomicValue(final Expression expr) { if (expr instanceof AtomicValue atomic) { return atomic; } @@ -346,14 +270,13 @@ private AtomicValue findAtomicValue(final Expression expr) { if (result instanceof AtomicValue atomic) { return atomic; } - return null; } catch (XPathException e) { RangeIndex.LOG.error(e); - return null; } + return null; } - private Operator invertOrdinalOperator(Operator operator) { + private static Operator invertOrdinalOperator(final Operator operator) { return switch (operator) { case LE -> Operator.GE; case GE -> Operator.LE; @@ -362,4 +285,90 @@ private Operator invertOrdinalOperator(Operator operator) { default -> null; }; } + + @Override + public boolean matches(final Node node) { + return node.getNodeType() == Node.ELEMENT_NODE && indexPredicate.test(((Element) node).getAttribute(attributeName)); + } + + private Double toDouble(final String value) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + RangeIndex.LOG.debug("Non-numeric value encountered for numeric condition on @'{}': {}", attributeName, value); + return (double) 0; + } + } + + private boolean match(final InternalFunctionCall functionCall) { + if (!operator.equals(Operator.MATCH)) { + return false; // nothing to do, everything else but matches is not handled here + } + final Function func = functionCall.getFunction(); + if (!func.isCalledAs("matches")) { + return false; // only calls to fn:matches() are handled here + } + final Expression lhe = unwrapSubExpression(func.getArgument(0)); + final Expression rhe = unwrapSubExpression(func.getArgument(1)); + final LocationStep testStep = findLocationStep(lhe); + final AtomicValue testValue = findAtomicValue(rhe); + + return canTest(testStep, testValue) && queryPredicate.test(testValue); + } + + private boolean compare(final GeneralComparison generalComparison) { + final Expression lhe = generalComparison.getLeft(); + final Expression rhe = generalComparison.getRight(); + + // find the attribute name and value pair from the predicate to check against + // first assume attribute is on the left and value is on the right + Operator currentOperator = RangeQueryRewriter.getOperator(generalComparison); + Operator invertedOperator = invertOrdinalOperator(currentOperator); + + if (!operator.equals(currentOperator) && !operator.equals(invertedOperator)) { + return false; // needless to do more as the operator cannot match + } + + LocationStep testStep = findLocationStep(lhe); + AtomicValue testValue = findAtomicValue(rhe); + + // the equality operators are commutative so if attribute/value pair has not been found, + // check the other way around + if (testStep == null && testValue == null && (commutative || commutativeNegate)) { + testStep = findLocationStep(rhe); + testValue = findAtomicValue(lhe); + // for LT, GT, GE and LE the operation has to be inverted + if (commutativeNegate) { + currentOperator = invertedOperator; + } + } + + return operator.equals(currentOperator) && canTest(testStep, testValue) && queryPredicate.test(testValue); + } + + private boolean canTest (final LocationStep step, final AtomicValue value) { + if (step == null || value == null) { + return false; + } + final QName qname = step.getTest().getName(); + return qname.getNameType() == ElementValue.ATTRIBUTE && qname.equals(attribute); + } + + @Override + public boolean find(final org.exist.xquery.Predicate predicate) { + final Expression inner = getInnerExpression(predicate); + if (inner instanceof InternalFunctionCall functionCall) { + return match(functionCall); + } + if (inner instanceof final GeneralComparison generalComparison) { + return compare(generalComparison); + } + // predicate expression cannot be parsed as condition + return false; + } + + @FunctionalInterface + public interface ThrowingPredicate