diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g index f5f3cb0209a..345fadfa657 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQuery.g @@ -103,6 +103,10 @@ options { return buf.toString(); } + public Exception getLastException() { + return (Exception) exceptions.get(exceptions.size() - 1); + } + public String getXQDoc() { return lexer.getXQDoc(); } @@ -802,7 +806,7 @@ initialClause throws XPathException intermediateClause throws XPathException : - ( initialClause | whereClause | groupByClause | orderByClause ) + ( initialClause | whereClause | groupByClause | orderByClause | countClause ) ; whereClause throws XPathException @@ -810,6 +814,13 @@ whereClause throws XPathException "where"^ exprSingle ; +countClause throws XPathException +{ String varName; } +: + "count"^ DOLLAR! varName=varName! + { #countClause = #(#countClause, #[VARIABLE_BINDING, varName]); } + ; + forClause throws XPathException : "for"^ inVarBinding ( COMMA! inVarBinding )* @@ -2223,6 +2234,8 @@ reservedKeywords returns [String name] | "array" { name = "array"; } | + "count" { name = "count"; } + | "copy-namespaces" { name = "copy-namespaces"; } | "empty-sequence" { name = "empty-sequence"; } diff --git a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g index e965f13c295..5c74f31de50 100644 --- a/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g +++ b/exist-core/src/main/antlr/org/exist/xquery/parser/XQueryTree.g @@ -128,9 +128,9 @@ options { private static class ForLetClause { XQueryAST ast; - String varName; + QName varName; SequenceType sequenceType= null; - String posVar= null; + QName posVar = null; Expression inputSequence; Expression action; FLWORClause.ClauseType type = FLWORClause.ClauseType.FOR; @@ -1398,7 +1398,11 @@ throws PermissionDeniedException, EXistException, XPathException )? step=expr[inputSequence] { - clause.varName= someVarName.getText(); + try { + clause.varName = QName.parse(staticContext, someVarName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(someVarName.getLine(), someVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + someVarName.getText()); + } clause.inputSequence= inputSequence; clauses.add(clause); } @@ -1449,7 +1453,11 @@ throws PermissionDeniedException, EXistException, XPathException )? step=expr[inputSequence] { - clause.varName= everyVarName.getText(); + try { + clause.varName = QName.parse(staticContext, everyVarName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(everyVarName.getLine(), everyVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + everyVarName.getText()); + } clause.inputSequence= inputSequence; clauses.add(clause); } @@ -1585,11 +1593,21 @@ throws PermissionDeniedException, EXistException, XPathException )? ( posVar:POSITIONAL_VAR - { clause.posVar= posVar.getText(); } + { + try { + clause.posVar = QName.parse(staticContext, posVar.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(posVar.getLine(), posVar.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + posVar.getText()); + } + } )? step=expr [inputSequence] { - clause.varName= varName.getText(); + try { + clause.varName = QName.parse(staticContext, varName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(varName.getLine(), varName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + varName.getText()); + } clause.inputSequence= inputSequence; clauses.add(clause); } @@ -1618,7 +1636,11 @@ throws PermissionDeniedException, EXistException, XPathException )? step=expr [inputSequence] { - clause.varName= letVarName.getText(); + try { + clause.varName = QName.parse(staticContext, letVarName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(letVarName.getLine(), letVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + letVarName.getText()); + } clause.inputSequence= inputSequence; clauses.add(clause); } @@ -1752,29 +1774,49 @@ throws PermissionDeniedException, EXistException, XPathException clauses.add(clause); } ) - )+ - step=expr [(PathExpr) action] - { + | + #( + co:"count" + countVarName:VARIABLE_BINDING + { + ForLetClause clause = new ForLetClause(); + clause.ast = co; + try { + clause.varName = QName.parse(staticContext, countVarName.getText(), null); + } catch (final IllegalQNameException iqe) { + throw new XPathException(countVarName.getLine(), countVarName.getColumn(), ErrorCodes.XPST0081, "No namespace defined for prefix " + countVarName.getText()); + } + clause.type = FLWORClause.ClauseType.COUNT; + clause.inputSequence = null; + clauses.add(clause); + } + ) + )+ + step=expr [(PathExpr) action] + { for (int i= clauses.size() - 1; i >= 0; i--) { ForLetClause clause= (ForLetClause) clauses.get(i); FLWORClause expr; switch (clause.type) { case LET: - expr= new LetExpr(context); + expr = new LetExpr(context); expr.setASTNode(expr_AST_in); break; case GROUPBY: - expr = new GroupByClause(context); - break; - case ORDERBY: - expr = new OrderByClause(context, clause.orderSpecs); - break; - case WHERE: - expr = new WhereClause(context, new DebuggableExpression(clause.inputSequence)); - break; - default: - expr= new ForExpr(context, clause.allowEmpty); - break; + expr = new GroupByClause(context); + break; + case ORDERBY: + expr = new OrderByClause(context, clause.orderSpecs); + break; + case WHERE: + expr = new WhereClause(context, new DebuggableExpression(clause.inputSequence)); + break; + case COUNT: + expr = new CountClause(context, clause.varName); + break; + default: + expr = new ForExpr(context, clause.allowEmpty); + break; } expr.setASTNode(clause.ast); if (clause.type == FLWORClause.ClauseType.FOR || clause.type == FLWORClause.ClauseType.LET) { diff --git a/exist-core/src/main/java/org/exist/xquery/AbstractFLWORClause.java b/exist-core/src/main/java/org/exist/xquery/AbstractFLWORClause.java index 99775bc4c40..8e987e78f72 100644 --- a/exist-core/src/main/java/org/exist/xquery/AbstractFLWORClause.java +++ b/exist-core/src/main/java/org/exist/xquery/AbstractFLWORClause.java @@ -41,14 +41,10 @@ public AbstractFLWORClause(XQueryContext context) { } @Override - public LocalVariable createVariable(final String name) throws XPathException { - try { - final LocalVariable var = new LocalVariable(QName.parse(context, name, null)); - firstVar = var; - return var; - } catch (final IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + name); - } + public LocalVariable createVariable(final QName name) throws XPathException { + final LocalVariable var = new LocalVariable(name); + firstVar = var; + return var; } @Override diff --git a/exist-core/src/main/java/org/exist/xquery/BasicExpressionVisitor.java b/exist-core/src/main/java/org/exist/xquery/BasicExpressionVisitor.java index 2975f22c470..ed64ab74b5c 100644 --- a/exist-core/src/main/java/org/exist/xquery/BasicExpressionVisitor.java +++ b/exist-core/src/main/java/org/exist/xquery/BasicExpressionVisitor.java @@ -180,6 +180,11 @@ public void visitOrderByClause(final OrderByClause orderBy) { // Nothing to do } + @Override + public void visitCountClause(final CountClause count) { + // Nothing to do + } + @Override public void visitGroupByClause(final GroupByClause groupBy) { // Nothing to do diff --git a/exist-core/src/main/java/org/exist/xquery/BindingExpression.java b/exist-core/src/main/java/org/exist/xquery/BindingExpression.java index c75d6d1f151..57ea725e6c3 100644 --- a/exist-core/src/main/java/org/exist/xquery/BindingExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/BindingExpression.java @@ -23,13 +23,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.exist.dom.QName; import org.exist.dom.persistent.*; import org.exist.numbering.NodeId; import org.exist.storage.UpdateListener; import org.exist.xquery.value.*; /** - * Abstract superclass for the variable binding expressions "for" and "let". + * Abstract superclass for the variable binding expressions "for", "let", and "count". * * @author Wolfgang Meier */ @@ -41,22 +42,20 @@ public abstract class BindingExpression extends AbstractFLWORClause implements R protected final static SequenceType POSITIONAL_VAR_TYPE = new SequenceType(Type.INTEGER, Cardinality.EXACTLY_ONE); - protected String varName; + protected QName varName; protected SequenceType sequenceType = null; protected Expression inputSequence; - private ExprUpdateListener listener; - - public BindingExpression(XQueryContext context) { + public BindingExpression(final XQueryContext context) { super(context); } - public void setVariable(String qname) { - varName = qname; + public void setVariable(final QName varName) { + this.varName = varName; } - public String getVariable() { + public QName getVariable() { return this.varName; } @@ -77,52 +76,45 @@ public Expression getInputSequence() { return this.inputSequence; } - /* (non-Javadoc) - * @see org.exist.xquery.Expression#analyze(org.exist.xquery.Expression, int) - */ - public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { unordered = (contextInfo.getFlags() & UNORDERED) > 0; } @Override public Sequence postEval(Sequence seq) throws XPathException { - if (returnExpr instanceof FLWORClause) { - seq = ((FLWORClause)returnExpr).postEval(seq); + if (returnExpr instanceof FLWORClause flworClause) { + seq = flworClause.postEval(seq); } return super.postEval(seq); } - /* (non-Javadoc) - * @see org.exist.xquery.Expression#preselect(org.exist.dom.persistent.DocumentSet, org.exist.xquery.StaticContext) - */ - public DocumentSet preselect(DocumentSet in_docs) throws XPathException { - return in_docs; + public DocumentSet preselect(final DocumentSet docs) throws XPathException { + return docs; } - /* (non-Javadoc) - * @see org.exist.xquery.AbstractExpression#resetState() - */ - public void resetState(boolean postOptimization) { + @Override + public void resetState(final boolean postOptimization) { super.resetState(postOptimization); inputSequence.resetState(postOptimization); returnExpr.resetState(postOptimization); } - public final static void setContext(int contextId, Sequence seq) throws XPathException { + public static void setContext(final int contextId, final Sequence seq) throws XPathException { if (seq instanceof VirtualNodeSet) { ((VirtualNodeSet)seq).setInPredicate(true); ((VirtualNodeSet)seq).setSelfIsContext(); } else { - Item next; - for (final SequenceIterator i = seq.unorderedIterator(); i.hasNext();) { - next = i.nextItem(); - if (next instanceof NodeProxy) - {((NodeProxy) next).addContextNode(contextId, (NodeProxy) next);} + for (final SequenceIterator i = seq.unorderedIterator(); i.hasNext(); ) { + final Item next = i.nextItem(); + if (next instanceof NodeProxy) { + ((NodeProxy) next).addContextNode(contextId, (NodeProxy) next); + } } } } - public final static void clearContext(int contextId, Sequence seq) throws XPathException { + public final static void clearContext(final int contextId, final Sequence seq) throws XPathException { if (seq != null && !(seq instanceof VirtualNodeSet)) { seq.clearContext(contextId); } @@ -132,27 +124,29 @@ protected void registerUpdateListener(final Sequence sequence) { if (listener == null) { listener = new ExprUpdateListener(sequence); context.registerUpdateListener(listener); - } else - {listener.setSequence(sequence);} + } else { + listener.setSequence(sequence); + } } private class ExprUpdateListener implements UpdateListener { private Sequence sequence; - public ExprUpdateListener(Sequence sequence) { + public ExprUpdateListener(final Sequence sequence) { this.sequence = sequence; } - public void setSequence(Sequence sequence) { + public void setSequence(final Sequence sequence) { this.sequence = sequence; } @Override - public void documentUpdated(DocumentImpl document, int event) { + public void documentUpdated(final DocumentImpl document, final int event) { + // no-op } @Override - public void nodeMoved(NodeId oldNodeId, NodeHandle newNode) { + public void nodeMoved(final NodeId oldNodeId, final NodeHandle newNode) { sequence.nodeMoved(oldNodeId, newNode); } @@ -163,6 +157,7 @@ public void unsubscribe() { @Override public void debug() { + // no-op } } @@ -178,11 +173,12 @@ public int returnsType() { /* RewritableExpression API */ @Override - public void replace(Expression oldExpr, Expression newExpr) { - if (inputSequence == oldExpr) - {inputSequence = newExpr;} - else if (returnExpr == oldExpr) - {returnExpr = newExpr;} + public void replace(final Expression oldExpr, final Expression newExpr) { + if (inputSequence == oldExpr) { + inputSequence = newExpr; + } else if (returnExpr == oldExpr) { + returnExpr = newExpr; + } } @Override @@ -196,7 +192,8 @@ public Expression getFirst() { } @Override - public void remove(Expression oldExpr) throws XPathException { + public void remove(final Expression oldExpr) throws XPathException { + // no-op } /* END RewritableExpression API */ diff --git a/exist-core/src/main/java/org/exist/xquery/CountClause.java b/exist-core/src/main/java/org/exist/xquery/CountClause.java new file mode 100644 index 00000000000..4ea6dc4a7f1 --- /dev/null +++ b/exist-core/src/main/java/org/exist/xquery/CountClause.java @@ -0,0 +1,211 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import org.exist.dom.QName; +import org.exist.xquery.util.ExpressionDumper; +import org.exist.xquery.value.IntegerValue; +import org.exist.xquery.value.Item; +import org.exist.xquery.value.Sequence; +import org.exist.xquery.value.SequenceType; +import org.exist.xquery.value.Type; +import org.exist.xquery.value.ValueSequence; + +/** + * Implements a count clause inside a FLWOR expressions. + * + * @author Adam Retter + * @author Gabriele Tomassetti + */ +public class CountClause extends AbstractFLWORClause { + + private static final SequenceType countVarType = new SequenceType(Type.INTEGER, Cardinality.EXACTLY_ONE); + + final QName varName; + + // the count itself + private long count = 0; + private int step = 1; + + public CountClause(final XQueryContext context, final QName varName) { + super(context); + this.varName = varName; + } + + @Override + public ClauseType getType() { + return ClauseType.COUNT; + } + + public QName getVarName() { + return varName; + } + + @Override + public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException { + contextInfo.setParent(this); + unordered = (contextInfo.getFlags() & UNORDERED) > 0; + + // Save the local variable stack + final LocalVariable mark = context.markLocalVariables(false); + try { + final AnalyzeContextInfo varContextInfo = new AnalyzeContextInfo(contextInfo); + + // Declare the count variable + final LocalVariable countVar = new LocalVariable(varName); + countVar.setSequenceType(countVarType); + countVar.setStaticType(varContextInfo.getStaticReturnType()); + context.declareVariableBinding(countVar); + + // analyze the return expression + final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); + returnExpr.analyze(newContextInfo); + + } finally { + // restore the local variable stack + context.popLocalVariables(mark); + } + } + + @Override + public Sequence preEval(final Sequence seq) throws XPathException { + // determine whether to count down or up + this.step = hasPreviousOrderByDescending() ? -1 : 1; + + // get the count start position + if (this.step == 1) { + this.count = 0; + } else { + this.count = seq.getItemCountLong() + 1; + } + + return super.preEval(seq); + } + + private boolean hasPreviousOrderByDescending() { + FLWORClause prev = getPreviousClause(); + while (prev != null) { + switch (prev.getType()) { + case LET, GROUPBY, FOR -> { + return false; + } + case ORDERBY -> { + return isDescending(((OrderByClause) prev).getOrderSpecs()); + } + default -> prev = prev.getPreviousClause(); + } + } + return true; + } + private boolean isDescending(final OrderSpec[] orderSpecs) { + for (final OrderSpec orderSpec : orderSpecs) { + if ((orderSpec.getModifiers() & OrderSpec.DESCENDING_ORDER) == OrderSpec.DESCENDING_ORDER) { + return true; + } + } + return false; + } + + @Override + public Sequence eval(final Sequence contextSequence, final Item contextItem) throws XPathException { + if (context.getProfiler().isEnabled()) { + context.getProfiler().start(this); + context.getProfiler().message(this, Profiler.DEPENDENCIES, + "DEPENDENCIES", Dependency.getDependenciesName(this.getDependencies())); + if (contextSequence != null) { + context.getProfiler().message(this, Profiler.START_SEQUENCES, + "CONTEXT SEQUENCE", contextSequence); + } + if (contextItem != null) { + context.getProfiler().message(this, Profiler.START_SEQUENCES, + "CONTEXT ITEM", contextItem.toSequence()); + } + } + + context.expressionStart(this); + + final Sequence resultSequence = new ValueSequence(unordered); + + // update the count + count = count + step; + + // Save the local variable stack + final LocalVariable mark = context.markLocalVariables(false); + try { + + // Declare the count variable + final LocalVariable countVar = createVariable(varName); + countVar.setSequenceType(countVarType); + context.declareVariableBinding(countVar); + + // set the binding for the count + countVar.setValue(new IntegerValue(count)); + + // eval the return expression on the window binding + resultSequence.addAll(returnExpr.eval(null, null)); + + // free resources + countVar.destroy(context, resultSequence); + + } finally { + // restore the local variable stack + context.popLocalVariables(mark, resultSequence); + } + + setActualReturnType(resultSequence.getItemType()); + + context.expressionEnd(this); + if (context.getProfiler().isEnabled()) { + context.getProfiler().end(this, "", resultSequence); + } + + return resultSequence; + } + + @Override + public Sequence postEval(Sequence seq) throws XPathException { + if (returnExpr instanceof FLWORClause flworClause) { + seq = flworClause.postEval(seq); + } + return super.postEval(seq); + } + + @Override + public void dump(final ExpressionDumper dumper) { + dumper.display("count", this.getLine()); + dumper.startIndent(); + dumper.display(this.varName); + dumper.endIndent().nl(); + } + + public String toString() { + final StringBuilder result = new StringBuilder(); + result.append("count "); + result.append("$").append(this.varName); + return result.toString(); + } + + @Override + public void accept(final ExpressionVisitor visitor) { + visitor.visitCountClause(this); + } +} diff --git a/exist-core/src/main/java/org/exist/xquery/ExpressionVisitor.java b/exist-core/src/main/java/org/exist/xquery/ExpressionVisitor.java index b3e2ff04321..343a4a2a60f 100644 --- a/exist-core/src/main/java/org/exist/xquery/ExpressionVisitor.java +++ b/exist-core/src/main/java/org/exist/xquery/ExpressionVisitor.java @@ -74,6 +74,8 @@ public interface ExpressionVisitor { void visitLetExpression(LetExpr letExpr); + void visitCountClause(CountClause count); + void visitOrderByClause(OrderByClause orderBy); void visitGroupByClause(GroupByClause groupBy); diff --git a/exist-core/src/main/java/org/exist/xquery/FLWORClause.java b/exist-core/src/main/java/org/exist/xquery/FLWORClause.java index 93623749530..60a665579f0 100644 --- a/exist-core/src/main/java/org/exist/xquery/FLWORClause.java +++ b/exist-core/src/main/java/org/exist/xquery/FLWORClause.java @@ -21,6 +21,7 @@ */ package org.exist.xquery; +import org.exist.dom.QName; import org.exist.xquery.value.Sequence; /** @@ -31,7 +32,7 @@ public interface FLWORClause extends Expression { enum ClauseType { - FOR, LET, GROUPBY, ORDERBY, WHERE, SOME, EVERY + FOR, LET, GROUPBY, ORDERBY, WHERE, SOME, EVERY, COUNT } /** @@ -102,7 +103,7 @@ enum ClauseType { * @return a new local variable, registered in the context * @throws XPathException if an error occurs whilst creating the variable */ - LocalVariable createVariable(String name) throws XPathException; + LocalVariable createVariable(QName name) throws XPathException; /** * Returns the first variable created by this FLWOR clause for reference diff --git a/exist-core/src/main/java/org/exist/xquery/ForExpr.java b/exist-core/src/main/java/org/exist/xquery/ForExpr.java index bb1fceec5ac..47a3f278e97 100644 --- a/exist-core/src/main/java/org/exist/xquery/ForExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/ForExpr.java @@ -33,7 +33,7 @@ */ public class ForExpr extends BindingExpression { - private String positionalVariable = null; + private QName positionalVariable = null; private boolean allowEmpty = false; private boolean isOuterFor = true; @@ -53,7 +53,7 @@ public ClauseType getType() { * * @param var the name of the variable to set */ - public void setPositionalVariable(String var) { + public void setPositionalVariable(final QName var) { positionalVariable = var; } @@ -69,7 +69,7 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { final AnalyzeContextInfo varContextInfo = new AnalyzeContextInfo(contextInfo); inputSequence.analyze(varContextInfo); // Declare the iteration variable - final LocalVariable inVar = new LocalVariable(QName.parse(context, varName, null)); + final LocalVariable inVar = new LocalVariable(varName); inVar.setSequenceType(sequenceType); inVar.setStaticType(varContextInfo.getStaticReturnType()); context.declareVariableBinding(inVar); @@ -80,7 +80,7 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { throw new XPathException(this, ErrorCodes.XQST0089, "bound variable and positional variable have the same name"); } - final LocalVariable posVar = new LocalVariable(QName.parse(context, positionalVariable, null)); + final LocalVariable posVar = new LocalVariable(positionalVariable); posVar.setSequenceType(POSITIONAL_VAR_TYPE); posVar.setStaticType(Type.INTEGER); context.declareVariableBinding(posVar); @@ -89,8 +89,6 @@ public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo); newContextInfo.addFlag(SINGLE_STEP_EXECUTION); returnExpr.analyze(newContextInfo); - } catch (final QName.IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix"); } finally { // restore the local variable stack context.popLocalVariables(mark); @@ -135,7 +133,7 @@ public Sequence eval(Sequence contextSequence, Item contextItem) // Declare positional variable LocalVariable at = null; if (positionalVariable != null) { - at = new LocalVariable(QName.parse(context, positionalVariable, null)); + at = new LocalVariable(positionalVariable); at.setSequenceType(POSITIONAL_VAR_TYPE); context.declareVariableBinding(at); } @@ -187,8 +185,6 @@ public Sequence eval(Sequence contextSequence, Item contextItem) processItem(var, i.nextItem(), in, resultSequence, at, p); } } - } catch (final QName.IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + positionalVariable); } finally { // restore the local variable stack context.popLocalVariables(mark, resultSequence); diff --git a/exist-core/src/main/java/org/exist/xquery/GroupByClause.java b/exist-core/src/main/java/org/exist/xquery/GroupByClause.java index 1d22c8c277f..5ea83ae2ff6 100644 --- a/exist-core/src/main/java/org/exist/xquery/GroupByClause.java +++ b/exist-core/src/main/java/org/exist/xquery/GroupByClause.java @@ -172,8 +172,8 @@ public Sequence postEval(final Sequence seq) throws XPathException { context.popLocalVariables(mark, result); } - if (returnExpr instanceof FLWORClause) { - result = ((FLWORClause) returnExpr).postEval(result); + if (returnExpr instanceof FLWORClause flworClause) { + result = flworClause.postEval(result); } result = super.postEval(result); return result; diff --git a/exist-core/src/main/java/org/exist/xquery/LetExpr.java b/exist-core/src/main/java/org/exist/xquery/LetExpr.java index 05dae3376c6..0ba839b3fc8 100644 --- a/exist-core/src/main/java/org/exist/xquery/LetExpr.java +++ b/exist-core/src/main/java/org/exist/xquery/LetExpr.java @@ -53,7 +53,7 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException final AnalyzeContextInfo varContextInfo = new AnalyzeContextInfo(contextInfo); inputSequence.analyze(varContextInfo); //Declare the iteration variable - final LocalVariable inVar = new LocalVariable(QName.parse(context, varName, null)); + final LocalVariable inVar = new LocalVariable(varName); inVar.setSequenceType(sequenceType); inVar.setStaticType(varContextInfo.getStaticReturnType()); context.declareVariableBinding(inVar); @@ -61,8 +61,6 @@ public void analyze(final AnalyzeContextInfo contextInfo) throws XPathException context.setContextSequencePosition(0, null); returnExpr.analyze(contextInfo); - } catch (final QName.IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + varName); } finally { // restore the local variable stack context.popLocalVariables(mark); diff --git a/exist-core/src/main/java/org/exist/xquery/OrderByClause.java b/exist-core/src/main/java/org/exist/xquery/OrderByClause.java index 2abb6b7038f..84bd07c5a1e 100644 --- a/exist-core/src/main/java/org/exist/xquery/OrderByClause.java +++ b/exist-core/src/main/java/org/exist/xquery/OrderByClause.java @@ -95,8 +95,8 @@ public Sequence postEval(Sequence seq) throws XPathException { orderedResult.sort(); Sequence result = orderedResult; - if (getReturnExpression() instanceof FLWORClause) { - result = ((FLWORClause) getReturnExpression()).postEval(result); + if (getReturnExpression() instanceof FLWORClause flworClause) { + result = flworClause.postEval(result); } return super.postEval(result); } @@ -112,6 +112,19 @@ public void dump(ExpressionDumper dumper) { dumper.nl(); } + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("order by "); + for (int i = 0; i < orderSpecs.length; i++) { + if (i > 0) { + builder.append(", "); + } + builder.append(orderSpecs[i]); + } + return builder.toString(); + } + @Override public void accept(ExpressionVisitor visitor) { visitor.visitOrderByClause(this); diff --git a/exist-core/src/main/java/org/exist/xquery/QuantifiedExpression.java b/exist-core/src/main/java/org/exist/xquery/QuantifiedExpression.java index 39d06b3f602..34950d32b97 100644 --- a/exist-core/src/main/java/org/exist/xquery/QuantifiedExpression.java +++ b/exist-core/src/main/java/org/exist/xquery/QuantifiedExpression.java @@ -21,7 +21,6 @@ */ package org.exist.xquery; -import org.exist.dom.QName; import org.exist.xquery.util.ExpressionDumper; import org.exist.xquery.value.BooleanValue; import org.exist.xquery.value.Item; @@ -63,13 +62,11 @@ public ClauseType getType() { public void analyze(AnalyzeContextInfo contextInfo) throws XPathException { final LocalVariable mark = context.markLocalVariables(false); try { - context.declareVariableBinding(new LocalVariable(QName.parse(context, varName, null))); + context.declareVariableBinding(new LocalVariable(varName)); contextInfo.setParent(this); inputSequence.analyze(contextInfo); returnExpr.analyze(contextInfo); - } catch (final QName.IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + varName); } finally { context.popLocalVariables(mark); } @@ -87,12 +84,7 @@ public Sequence eval(Sequence contextSequence, Item contextItem) {context.getProfiler().message(this, Profiler.START_SEQUENCES, "CONTEXT ITEM", contextItem.toSequence());} } - final LocalVariable var; - try { - var = new LocalVariable(QName.parse(context, varName, null)); - } catch (final QName.IllegalQNameException e) { - throw new XPathException(this, ErrorCodes.XPST0081, "No namespace defined for prefix " + varName); - } + final LocalVariable var = new LocalVariable(varName); final Sequence inSeq = inputSequence.eval(contextSequence, contextItem); if (sequenceType != null) { diff --git a/exist-core/src/main/java/org/exist/xquery/WhereClause.java b/exist-core/src/main/java/org/exist/xquery/WhereClause.java index 031838963d6..480781c9ce4 100644 --- a/exist-core/src/main/java/org/exist/xquery/WhereClause.java +++ b/exist-core/src/main/java/org/exist/xquery/WhereClause.java @@ -133,8 +133,8 @@ public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathExc @Override public Sequence postEval(Sequence seq) throws XPathException { fastTrack = false; - if (returnExpr instanceof FLWORClause) { - seq = ((FLWORClause) returnExpr).postEval(seq); + if (returnExpr instanceof FLWORClause flworClause) { + seq = flworClause.postEval(seq); } return super.postEval(seq); } diff --git a/exist-core/src/main/java/org/exist/xquery/XQuery.java b/exist-core/src/main/java/org/exist/xquery/XQuery.java index 2009777436e..c3908470605 100644 --- a/exist-core/src/main/java/org/exist/xquery/XQuery.java +++ b/exist-core/src/main/java/org/exist/xquery/XQuery.java @@ -224,10 +224,18 @@ private CompiledXQuery compile(final XQueryContext context, final Reader reader, } else { parser.xpath(); } - - if(parser.foundErrors()) { - LOG.debug(parser.getErrorMessage()); - throw new StaticXQueryException(context.getRootExpression(), parser.getErrorMessage()); + + if (parser.foundErrors()) { + if (LOG.isDebugEnabled()) { + LOG.debug(parser.getErrorMessage()); + } + final Exception lastException = parser.getLastException(); + if (lastException != null && lastException instanceof XPathException) { + final XPathException xpe = (XPathException) lastException; + throw new StaticXQueryException(xpe.getColumn(), xpe.getLine(), parser.getErrorMessage(), xpe); + } else { + throw new StaticXQueryException(context.getRootExpression(), parser.getErrorMessage()); + } } final AST ast = parser.getAST(); diff --git a/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java b/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java index 47cf110194b..e888892da82 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/DoubleValue.java @@ -146,19 +146,25 @@ public boolean isPositive() { @Override protected @Nullable IntSupplier createComparisonWith(final NumericValue other) { - if (other instanceof IntegerValue) { - return () -> BigDecimal.valueOf(value).compareTo(new BigDecimal(((IntegerValue) other).value)); - } - if (other instanceof DecimalValue) { - return () -> BigDecimal.valueOf(value).compareTo(((DecimalValue) other).value); - } - if (other instanceof DoubleValue) { - return () -> Double.compare(value, ((DoubleValue) other).value); - } - if (other instanceof FloatValue) { - return () -> Double.compare(value, ((FloatValue) other).value); - } - return null; + final IntSupplier comparison; + if (isNaN()) { + comparison = () -> Constants.INFERIOR; + } else if (other.isNaN()) { + comparison = () -> Constants.SUPERIOR; + } else if (isInfinite() && other.isInfinite() && isPositive() == other.isPositive()) { + comparison = () -> Constants.EQUAL; + } else if (other instanceof IntegerValue iv) { + comparison = () -> BigDecimal.valueOf(value).compareTo(new BigDecimal(iv.value)); + } else if (other instanceof DecimalValue dv) { + comparison = () -> BigDecimal.valueOf(value).compareTo(dv.value); + } else if (other instanceof DoubleValue dv) { + comparison = () -> Double.compare(value, dv.value); + } else if (other instanceof FloatValue fv) { + comparison = () -> Double.compare(value, fv.value); + } else { + comparison = null; + } + return comparison; } @Override diff --git a/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java b/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java index b9538b24fc3..1d4b7847c2c 100644 --- a/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java +++ b/exist-core/src/main/java/org/exist/xquery/value/FloatValue.java @@ -150,7 +150,13 @@ public boolean isPositive() { @Override protected @Nullable IntSupplier createComparisonWith(final NumericValue other) { final IntSupplier comparison; - if (other instanceof IntegerValue) { + if (isNaN()) { + comparison = () -> Constants.INFERIOR; + } else if (other.isNaN()) { + comparison = () -> Constants.SUPERIOR; + } else if (isInfinite() && other.isInfinite() && isPositive() == other.isPositive()) { + comparison = () -> Constants.EQUAL; + } else if (other instanceof IntegerValue) { comparison = () -> BigDecimal.valueOf(value).compareTo(new BigDecimal(((IntegerValue)other).value)); } else if (other instanceof DecimalValue) { final BigDecimal promoted = new BigDecimal(Float.toString(value)); diff --git a/exist-core/src/test/java/org/exist/xquery/CountExpressionTest.java b/exist-core/src/test/java/org/exist/xquery/CountExpressionTest.java new file mode 100644 index 00000000000..6798961fc92 --- /dev/null +++ b/exist-core/src/test/java/org/exist/xquery/CountExpressionTest.java @@ -0,0 +1,85 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.xquery; + +import antlr.RecognitionException; +import antlr.TokenStreamException; +import org.exist.dom.QName; +import org.exist.xquery.parser.XQueryAST; +import org.exist.xquery.parser.XQueryLexer; +import org.exist.xquery.parser.XQueryParser; +import org.exist.xquery.parser.XQueryTreeParser; +import org.junit.jupiter.api.Test; + +import java.io.StringReader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Adam Retter + * @author Gabriele Tomassetti + */ +public class CountExpressionTest { + + @Test + public void countTest() throws RecognitionException, XPathException, TokenStreamException, QName.IllegalQNameException { + final String query = "xquery version \"3.1\";\n" + + "for $p in $products\n" + + "order by $p/sales descending\n" + + "count $rank\n" + + "where $rank <= 3\n" + + "return\n" + + " \n" + + " {$p/name, $p/sales}\n" + + " "; + + // parse the query into the internal syntax tree + final XQueryContext context = new XQueryContext(); + final XQueryLexer lexer = new XQueryLexer(context, new StringReader(query)); + final XQueryParser xparser = new XQueryParser(lexer); + xparser.xpath(); + if (xparser.foundErrors()) { + fail(xparser.getErrorMessage()); + return; + } + + final XQueryAST ast = (XQueryAST) xparser.getAST(); + + final XQueryTreeParser treeParser = new XQueryTreeParser(context); + final PathExpr expr = new PathExpr(context); + treeParser.xpath(ast, expr); + if (treeParser.foundErrors()) { + fail(treeParser.getErrorMessage()); + return; + } + + // count keyword + assertEquals(XQueryParser.LITERAL_count, ast.getNextSibling().getFirstChild().getNextSibling().getNextSibling().getType()); + // rank variable binding + assertEquals(XQueryParser.VARIABLE_BINDING, ast.getNextSibling().getFirstChild().getNextSibling().getNextSibling().getFirstChild().getType()); + assertTrue(((ForExpr)expr.getFirst()).returnExpr instanceof OrderByClause); + assertTrue(((OrderByClause)(((ForExpr)expr.getFirst()).returnExpr)).returnExpr instanceof CountClause); + assertEquals(new QName("rank"), ((CountClause)((OrderByClause)(((ForExpr)expr.getFirst()).returnExpr)).returnExpr).varName); + } +} diff --git a/exist-core/src/test/xquery/count.xql b/exist-core/src/test/xquery/count.xql index 14548feba94..ea2b9e33b13 100644 --- a/exist-core/src/test/xquery/count.xql +++ b/exist-core/src/test/xquery/count.xql @@ -22,43 +22,43 @@ xquery version "3.0"; (:~ Additional tests for the fn:count function :) -module namespace count="http://exist-db.org/xquery/test/count"; +module namespace cnt="http://exist-db.org/xquery/test/count"; declare namespace test="http://exist-db.org/xquery/xqsuite"; import module namespace xmldb="http://exist-db.org/xquery/xmldb"; -declare variable $count:TEST_COLLECTION_NAME := "test-count"; -declare variable $count:TEST_COLLECTION := "/db/" || $count:TEST_COLLECTION_NAME; -declare variable $count:COLLECTION1_NAME := "test-count-1"; -declare variable $count:COLLECTION2_NAME := "test-count-2"; -declare variable $count:COLLECTION1 := $count:TEST_COLLECTION || "/" || $count:COLLECTION1_NAME; -declare variable $count:COLLECTION2 := $count:TEST_COLLECTION || "/" || $count:COLLECTION2_NAME; +declare variable $cnt:TEST_COLLECTION_NAME := "test-count"; +declare variable $cnt:TEST_COLLECTION := "/db/" || $cnt:TEST_COLLECTION_NAME; +declare variable $cnt:COLLECTION1_NAME := "test-count-1"; +declare variable $cnt:COLLECTION2_NAME := "test-count-2"; +declare variable $cnt:COLLECTION1 := $cnt:TEST_COLLECTION || "/" || $cnt:COLLECTION1_NAME; +declare variable $cnt:COLLECTION2 := $cnt:TEST_COLLECTION || "/" || $cnt:COLLECTION2_NAME; declare %test:setUp -function count:setup() { - xmldb:create-collection("/db", $count:TEST_COLLECTION_NAME), - xmldb:create-collection($count:TEST_COLLECTION, $count:COLLECTION1_NAME), - xmldb:store($count:COLLECTION1, "test1.xml", ), - xmldb:create-collection($count:TEST_COLLECTION, $count:COLLECTION2_NAME), - xmldb:store($count:COLLECTION2, "test2xml", ) +function cnt:setup() { + xmldb:create-collection("/db", $cnt:TEST_COLLECTION_NAME), + xmldb:create-collection($cnt:TEST_COLLECTION, $cnt:COLLECTION1_NAME), + xmldb:store($cnt:COLLECTION1, "test1.xml", ), + xmldb:create-collection($cnt:TEST_COLLECTION, $cnt:COLLECTION2_NAME), + xmldb:store($cnt:COLLECTION2, "test2xml", ) }; declare %test:tearDown -function count:cleanup() { - xmldb:remove($count:TEST_COLLECTION) +function cnt:cleanup() { + xmldb:remove($cnt:TEST_COLLECTION) }; declare %test:assertEquals(1, 1) -function count:arg-self-on-stored() { - (collection($count:COLLECTION1)/*, collection($count:COLLECTION2)/*)/count(.) +function cnt:arg-self-on-stored() { + (collection($cnt:COLLECTION1)/*, collection($cnt:COLLECTION2)/*)/count(.) }; declare %test:assertEquals(1, 1, 1) -function count:arg-self-on-constructed() { +function cnt:arg-self-on-constructed() { (, , )/count(.) }; diff --git a/exist-core/src/test/xquery/xquery3/count.xqm b/exist-core/src/test/xquery/xquery3/count.xqm new file mode 100644 index 00000000000..d3a7d063bf6 --- /dev/null +++ b/exist-core/src/test/xquery/xquery3/count.xqm @@ -0,0 +1,262 @@ +(: + : eXist-db Open Source Native XML Database + : Copyright (C) 2001 The eXist-db Authors + : + : info@exist-db.org + : http://www.exist-db.org + : + : This library is free software; you can redistribute it and/or + : modify it under the terms of the GNU Lesser General Public + : License as published by the Free Software Foundation; either + : version 2.1 of the License, or (at your option) any later version. + : + : This library is distributed in the hope that it will be useful, + : but WITHOUT ANY WARRANTY; without even the implied warranty of + : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + : Lesser General Public License for more details. + : + : You should have received a copy of the GNU Lesser General Public + : License along with this library; if not, write to the Free Software + : Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + :) +xquery version "3.0"; + +module namespace ct = "http://exist-db.org/xquery/test/count"; + +declare namespace test = "http://exist-db.org/xquery/xqsuite"; + + +declare + %test:assertEquals( + '1', + '2', + '3', + '4' + ) +function ct:simple() { + for $x in 1 to 4 + count $index1 + return + {$x} +}; + +declare + %test:assertEquals( + '1', + '2', + '3', + '4' + ) +function ct:order-ascending-index1-before() { + for $x in 1 to 4 + order by $x ascending + count $index1 + return + {$x} +}; + +declare + %test:assertEquals( + '1', + '2', + '3', + '4' + ) +function ct:order-ascending-index1-after() { + for $x in 1 to 4 + count $index1 + order by $x ascending + return + {$x} +}; + +declare + %test:assertEquals( + '4', + '3', + '2', + '1' + ) +function ct:order-descending-index1-before() { + for $x in 1 to 4 + order by $x descending + count $index1 + return + {$x} +}; + +declare + %test:assertEquals( + '4', + '3', + '2', + '1' + ) +function ct:order-descending-index1-after() { + for $x in 1 to 4 + count $index1 + order by $x descending + return + {$x} +}; + +declare +%test:pending('Related to failing XQTS test prod-CountClause/count-009, see: https://github.com/eXist-db/exist/pull/4530#issue-1356325345') +%test:assertEquals( + 'b', + 'a' + ) +function ct:order-alpha-ascending-indexes() { + for $x in ('a', 'b') + count $index1 + let $remainder := $index1 mod 2 + order by $remainder, $index1 + count $index2 + return + {$x} +}; + +declare + %test:assertEquals( + '1', + '2', + '3', + '4' + ) +function ct:order-ascending-indexes() { + for $x in 1 to 4 + count $index1 + order by $x ascending + count $index2 + return + {$x} +}; + +declare + %test:assertEquals( + '4', + '3', + '2', + '1' + ) +function ct:order-descending-indexes() { + for $x in 1 to 4 + count $index1 + order by $x descending + count $index2 + return + {$x} +}; + +declare + %test:pending('Related to failing XQTS test prod-CountClause/count-009, see: https://github.com/eXist-db/exist/pull/4530#issue-1356325345') + %test:assertEquals( + '3', + '1', + '4', + '2' + ) +function ct:order-non-linear-ascending-indexes() { + for $x in 1 to 4 + count $index1 + let $remainder := $index1 mod 3 + order by $remainder ascending + count $index2 + return + {$x} +}; + +declare + %test:pending('Related to failing XQTS test prod-CountClause/count-009, see: https://github.com/eXist-db/exist/pull/4530#issue-1356325345') + %test:assertEquals( + '2', + '1', + '4', + '3' + ) +function ct:order-non-linear-descending-indexes() { + for $x in 1 to 4 + count $index1 + let $remainder := $index1 mod 3 + order by $remainder descending + count $index2 + return + {$x} +}; + +declare + %test:pending('Related to failing XQTS test prod-CountClause/count-009, see: https://github.com/eXist-db/exist/pull/4530#issue-1356325345') + %test:assertEquals( + '21', + '11', + '22', + '12' + ) +function ct:order-ascending-indexes-two-keys() { + for $x in 1 to 2 + for $y in 1 to 2 + count $index1 + let $remainder := $index1 mod 3 + order by $remainder, $index1 + count $index2 + return + {$x}{$y} +}; + +declare + %test:pending('Related to failing XQTS test prod-CountClause/count-009, see: https://github.com/eXist-db/exist/pull/4530#issue-1356325345') + %test:assertEquals( + '12', + '22', + '11', + '21' + ) +function ct:order-descending-indexes-two-keys() { + for $x in 1 to 2 + for $y in 1 to 2 + count $index1 + let $remainder := $index1 mod 3 + order by $remainder descending, $index1 descending + count $index2 + return + {$x}{$y} +}; + +declare + %test:pending('Related to failing XQTS test prod-CountClause/count-009, see: https://github.com/eXist-db/exist/pull/4530#issue-1356325345') + %test:assertEquals( + '21', + '22', + '11', + '12' + ) +function ct:order-ascending-descending-indexes-two-keys() { + for $x in 1 to 2 + for $y in 1 to 2 + count $index1 + let $remainder := $index1 mod 3 + order by $remainder ascending, $index1 descending + count $index2 + return + {$x}{$y} +}; + + +declare + %test:pending('Related to failing XQTS test prod-CountClause/count-009, see: https://github.com/eXist-db/exist/pull/4530#issue-1356325345') + %test:assertEquals( + '12', + '11', + '22', + '21' + ) +function ct:order-descending-ascending-indexes-two-keys() { + for $x in 1 to 2 + for $y in 1 to 2 + count $index1 + let $remainder := $index1 mod 3 + order by $remainder descending, $index1 ascending + count $index2 + return + {$x}{$y} +}; \ No newline at end of file