All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.exist.xquery.GroupByClause Maven / Gradle / Ivy

There is a newer version: 6.3.0
Show newest version
package org.exist.xquery;

import com.ibm.icu.text.Collator;
import org.exist.dom.QName;
import org.exist.xquery.util.ExpressionDumper;
import org.exist.xquery.value.*;

import java.util.*;
import java.util.stream.Stream;

/**
 * Implements a "group by" clause inside a FLWOR.
 *
 * @author wolf
 */
public class GroupByClause extends AbstractFLWORClause {

    protected FLWORClause rootClause = null;
    private GroupSpec[] groupSpecs;
    private final Deque stack = new ArrayDeque<>();

    /**
     * Collect tuples and grouping vars. Because GroupByClause needs to keep
     * state across calls to preEval/eval/postEval, we have to track state data
     * in a separate object and push it to a stack, otherwise recursive calls
     * would overwrite data.
     */
    private class GroupByData {

        private Map, Tuple> groupedMap = null;
        private Map variables = null;
        private List groupingVars = null;

        private boolean initialized = false;

        public GroupByData() {
            // check if we can use a hash map
            if (usesDefaultCollator()) {
                groupedMap = new HashMap<>();
            } else {
                // non-default collation: must use tree map
                groupedMap = new TreeMap<>(GroupByClause.this::compareKeys);
            }
            variables = new HashMap<>();
            groupingVars = new ArrayList<>();
        }
    }

    public GroupByClause(XQueryContext context) {
        super(context);
    }

    @Override
    public ClauseType getType() {
        return ClauseType.GROUPBY;
    }

    @Override
    public Sequence preEval(Sequence seq) throws XPathException {
        stack.push(new GroupByData());
        return super.preEval(seq);
    }

    @Override
    public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException {
        final GroupByData data = stack.peek();

        // Evaluate group spec to create grouping key sequence
        final List groupingValues = new ArrayList<>();
        final List groupingKeys = new ArrayList<>();
        for (GroupSpec spec: groupSpecs) {
            final Sequence groupingSeq = spec.getGroupExpression().eval(null);
            if (groupingSeq.getItemCount() > 1) {
                throw new XPathException(this, ErrorCodes.XPTY0004, "Grouping variable " + spec.getKeyVarName() + " " +
                        "evaluates to more than one item");
            }
            final AtomicValue groupingValue = groupingSeq.isEmpty() ? AtomicValue.EMPTY_VALUE : groupingSeq.itemAt(0)
                    .atomize();
            if (!data.initialized) {
                final LocalVariable groupingVar = new LocalVariable(spec.getKeyVarName());
                groupingVar.setSequenceType(new SequenceType(Type.ATOMIC, groupingValue.isEmpty() ? Cardinality
                        .EMPTY : Cardinality.EXACTLY_ONE));
                groupingVar.setStaticType(groupingValue.getType());
                data.groupingVars.add(groupingVar);
            }
            groupingValues.add(groupingSeq);
            groupingKeys.add(groupingValue);
        }

        // collect the current tuples into the grouping map
        final Tuple tuple = data.groupedMap.computeIfAbsent(groupingKeys, ks -> new Tuple(groupingValues));

        // scan in-scope variables to collect tuples
        LocalVariable nextVar = rootClause.getStartVariable();
        Objects.requireNonNull(nextVar);
        while(nextVar != null) {
            tuple.add(nextVar.getQName(), nextVar.getValue());
            if (!data.initialized) {
                // on first call: initialize non-grouping variable for later use
                final LocalVariable var = new LocalVariable(nextVar.getQName());
                var.setSequenceType(nextVar.getSequenceType());
                var.setStaticType(nextVar.getStaticType());
                var.setContextDocs(nextVar.getContextDocs());
                data.variables.put(var.getQName(), var);
            }
            nextVar = nextVar.after;
        }

        data.initialized = true;
        return contextSequence;
    }

    @Override
    public Sequence postEval(final Sequence seq) throws XPathException {
        if (!stack.isEmpty()) {
            final GroupByData data = stack.peek();
            Sequence result = new ValueSequence();
            final LocalVariable mark = context.markLocalVariables(false);
            try {
                // declare non-grouping variables
                for (LocalVariable var : data.variables.values()) {
                    context.declareVariableBinding(var);
                }
                // declare grouping variables
                for (LocalVariable var : data.groupingVars) {
                    context.declareVariableBinding(var);
                }
                // iterate over each group
                for (Tuple tuple : data.groupedMap.values()) {
                    context.proceed();

                    // set grouping variable values
                    final Iterator siter = tuple.groupingValues.iterator();
                    for (LocalVariable var : data.groupingVars) {
                        if (siter.hasNext()) {
                            Sequence val = siter.next();
                            var.setValue(val);
                        } else {
                            throw new XPathException(this, "Internal error: missing grouping value");
                        }
                    }
                    // set values of non-grouping variables
                    for (Map.Entry entry : tuple.entrySet()) {
                        final LocalVariable var = data.variables.get(entry.getKey());
                        var.setValue(entry.getValue());
                    }
                    final Sequence r = returnExpr.eval(null);
                    result.addAll(r);
                }
            } finally {
                stack.pop();
                context.popLocalVariables(mark, result);
            }

            if (returnExpr instanceof FLWORClause) {
                result = ((FLWORClause) returnExpr).postEval(result);
            }
            result = super.postEval(result);
            return result;
        }
        return seq;
    }

    @Override
    public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
        contextInfo.setParent(this);
        unordered = (contextInfo.getFlags() & UNORDERED) > 0;
        final LocalVariable mark = context.markLocalVariables(false);
        try {
            if (groupSpecs != null) {
                for (final GroupSpec spec : groupSpecs) {
                    final LocalVariable groupKeyVar = new LocalVariable(spec.getKeyVarName());
                    context.declareVariableBinding(groupKeyVar);
                }
            }

            final AnalyzeContextInfo newContextInfo = new AnalyzeContextInfo(contextInfo);
            newContextInfo.addFlag(SINGLE_STEP_EXECUTION);
            for (final GroupSpec spec : groupSpecs) {
                spec.analyze(newContextInfo);
            }

            returnExpr.analyze(newContextInfo);
        } finally {
            // restore the local variable stack
            context.popLocalVariables(mark);
        }

        FLWORClause prevClause = getPreviousClause();
        while (prevClause != null) {
            rootClause = prevClause;
            prevClause = prevClause.getPreviousClause();
        }
    }

    public void setGroupSpecs(final GroupSpec specs[]) {
        final List distinctSpecs = new ArrayList<>(specs.length);
        for (int i = 0; i < specs.length; i++) {
            boolean duplicate = false;
            for (int j = i + 1; j < specs.length; j++) {
                if (specs[i].equals(specs[j])) {
                    duplicate = true;
                    break;
                }
            }
            if (!duplicate) {
                distinctSpecs.add(specs[i]);
            }
        }
        this.groupSpecs = distinctSpecs.toArray(new GroupSpec[distinctSpecs.size()]);
    }

    public GroupSpec[] getGroupSpecs() {
        return groupSpecs == null ? new GroupSpec[0] : groupSpecs;
    }

    @Override
    public void dump(ExpressionDumper dumper) {
        if (groupSpecs != null) {
            dumper.display("group by ");
            for (int i = 0; i < groupSpecs.length; i++) {
                if (i > 0)
                {dumper.display(", ");}
                dumper.display(groupSpecs[i].getGroupExpression().toString());
                dumper.display(" as ");
                dumper.display("$").display(groupSpecs[i].getKeyVarName());
            }
            dumper.nl();
        }
    }

    @Override
    public void resetState(boolean postOptimization) {
        super.resetState(postOptimization);
        stack.clear();
        returnExpr.resetState(postOptimization);
        for (GroupSpec spec: groupSpecs) {
            spec.resetState(postOptimization);
        }
    }

    @Override
    public void accept(ExpressionVisitor visitor) {
        visitor.visitGroupByClause(this);
    }

    /**
     * Compare keys using the collator given in the group spec. Used to
     * sort keys into the grouping map.
     */
    private int compareKeys(List s1, List s2) {
        final int c1 = s1.size();
        final int c2 = s2.size();
        if (c1 == c2) {
            try {
                for (int i = 0; i < c1; i++) {
                    final AtomicValue v1 = s1.get(i);
                    final AtomicValue v2 = s2.get(i);
                    final Collator collator = groupSpecs[i].getCollator();
                    final int r = v1.compareTo(collator, v2);
                    if (r != Constants.EQUAL) {
                        return r;
                    }
                }
                return Constants.EQUAL;
            } catch (XPathException e) {
                return Constants.INFERIOR;
            }
        }
        return c1 < c2 ? Constants.INFERIOR : Constants.SUPERIOR;
    }

    private boolean usesDefaultCollator() {
        return Stream.of(groupSpecs).allMatch(spec -> spec.getCollator() == null);
    }

    static class Tuple extends HashMap {

        private final List groupingValues;

        public Tuple(final List groupingValues) {
            super();
            this.groupingValues = groupingValues;
        }

        public void add(final QName name, final Sequence val) throws XPathException {
            Sequence seq = get(name);
            if (seq == null) {
                final ValueSequence temp = new ValueSequence(val.getItemCount());
                temp.addAll(val);
                put(name, temp);
            } else {
                seq.addAll(val);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy