/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.kernel;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.datacache.DataCache;
import org.apache.openjpa.kernel.exps.AbstractExpressionVisitor;
import org.apache.openjpa.kernel.exps.AggregateListener;
import org.apache.openjpa.kernel.exps.Constant;
import org.apache.openjpa.kernel.exps.ExpressionFactory;
import org.apache.openjpa.kernel.exps.ExpressionParser;
import org.apache.openjpa.kernel.exps.FilterListener;
import org.apache.openjpa.kernel.exps.InMemoryExpressionFactory;
import org.apache.openjpa.kernel.exps.Path;
import org.apache.openjpa.kernel.exps.QueryExpressions;
import org.apache.openjpa.kernel.exps.Resolver;
import org.apache.openjpa.kernel.exps.StringContains;
import org.apache.openjpa.kernel.exps.Subquery;
import org.apache.openjpa.kernel.exps.Val;
import org.apache.openjpa.kernel.exps.Value;
import org.apache.openjpa.kernel.exps.WildcardMatch;
import org.apache.openjpa.lib.rop.ListResultObjectProvider;
import org.apache.openjpa.lib.rop.RangeResultObjectProvider;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.OrderedMap;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InvalidStateException;
import org.apache.openjpa.util.UnsupportedException;
import org.apache.openjpa.util.UserException;

/**
 * Implementation of an expression-based query, which can handle
 * String-based query expressions such as JPQL and JDOQL.
 * This implementation is suitable for in-memory operation.
 * Override the following methods to also support datastore operation:
 * <ul>
 * <li>Override {@link #supportsDataStoreExecution} to return
 * <code>true</code>.</li>
 * <li>Override {@link #executeQuery}, {@link #executeDelete}, and
 * {@link #executeUpdate} to execute the query against the data store.
 * Keep in mind that the parameters passed to this method might be in use
 * by several threads in different query instances. Thus components like
 * the expression factory must either be thread safe, or this method must
 * synchronize on them.</li>
 * <li>Override {@link #getDataStoreActions} to return a representation of
 * the actions that will be taken on the data store. For use in visual
 * tools.</li>
 * <li>Override {@link #getExpressionFactory} to return a factory for creating
 * expressions in the datastore's language. The factory must be cachable.</li>
 * </ul>
 *
 * @author Abe White
 */
public class ExpressionStoreQuery
    extends AbstractStoreQuery {

    
    private static final long serialVersionUID = 1L;

    private static final Localizer _loc = Localizer.forPackage
        (ExpressionStoreQuery.class);

    // maintain support for a couple of deprecated extensions
    private static final FilterListener[] _listeners = new FilterListener[]{
        new StringContains(), new WildcardMatch(),
    };

    protected final ExpressionParser _parser;
    protected transient Object _parsed;

    /**
     * Construct a query with a parser for the language.
     */
    public ExpressionStoreQuery(ExpressionParser parser) {
        _parser = parser;
    }

    /**
     * Resolver used in parsing.
     */
    public Resolver getResolver() {
        return new Resolver() {
            @Override
            public Class classForName(String name, String[] imports) {
                return ctx.classForName(name, imports);
            }

            @Override
            public FilterListener getFilterListener(String tag) {
                return ctx.getFilterListener(tag);
            }

            @Override
            public AggregateListener getAggregateListener(String tag) {
                return ctx.getAggregateListener(tag);
            }

            @Override
            public OpenJPAConfiguration getConfiguration() {
                return ctx.getStoreContext().getConfiguration();
            }

            @Override
            public QueryContext getQueryContext() {
                return ctx;
            }
        };
    }

    /**
     * Allow direct setting of parsed state for facades that do parsing.
     * The facade should call this method twice: once with the query string,
     * and again with the parsed state.
     */
    @Override
    public boolean setQuery(Object query) {
        _parsed = query;
        return true;
    }

    @Override
    public FilterListener getFilterListener(String tag) {
        for (FilterListener listener : _listeners)
            if (listener.getTag().equals(tag))
                return listener;
        return null;
    }

    @Override
    public Object newCompilation() {
        if (_parsed != null)
            return _parsed;
        return _parser.parse(ctx.getQueryString(), this);
    }

    @Override
    public Object getCompilation() {
        return _parsed;
    }

    @Override
    public void populateFromCompilation(Object comp) {
        _parser.populate(comp, this);
    }

    @Override
    public void invalidateCompilation() {
        _parsed = null;
    }

    @Override
    public boolean supportsInMemoryExecution() {
        return true;
    }

    @Override
    public Executor newInMemoryExecutor(ClassMetaData meta, boolean subs) {
        return new InMemoryExecutor(this, meta, subs, _parser,
        		ctx.getCompilation(), new InMemoryExpressionFactory());
    }

    @Override
    public Executor newDataStoreExecutor(ClassMetaData meta, boolean subs) {
        return new DataStoreExecutor(this, meta, subs, _parser,
            ctx.getCompilation());
    }

    ////////////////////////
    // Methods for Override
    ////////////////////////

    /**
     * Execute the given expression against the given candidate extent.
     *
     * @param ex current executor
     * @param base the base type the query should match
     * @param types the independent candidate types
     * @param subclasses true if subclasses should be included in the results
     * @param facts the expression factory used to build the query for
     * each base type
     * @param parsed the parsed query values
     * @param params parameter values, or empty array
     * @param range result range
     * @return a provider for matching objects
     */
    protected ResultObjectProvider executeQuery(Executor ex,
        ClassMetaData base, ClassMetaData[] types, boolean subclasses,
        ExpressionFactory[] facts, QueryExpressions[] parsed, Object[] params,
        Range range) {
        throw new UnsupportedException();
    }

    /**
     * Execute the given expression against the given candidate extent
     * and delete the instances.
     *
     * @param ex current executor
     * @param base the base type the query should match
     * @param types the independent candidate types
     * @param subclasses true if subclasses should be included in the results
     * @param facts the expression factory used to build the query for
     * each base type
     * @param parsed the parsed query values
     * @param params parameter values, or empty array
     * @return a number indicating the number of instances deleted,
     * or null to execute the delete in memory
     */
    protected Number executeDelete(Executor ex, ClassMetaData base,
        ClassMetaData[] types, boolean subclasses, ExpressionFactory[] facts,
        QueryExpressions[] parsed, Object[] params) {
        return null;
    }

    /**
     * Execute the given expression against the given candidate extent
     * and updates the instances.
     *
     * @param ex current executor
     * @param base the base type the query should match
     * @param types the independent candidate types
     * @param subclasses true if subclasses should be included in the results
     * @param facts the expression factory used to build the query for
     * each base type
     * @param parsed the parsed query values
     * @param params parameter values, or empty array
     * @return a number indicating the number of instances updated,
     * or null to execute the update in memory.
     */
    protected Number executeUpdate(Executor ex, ClassMetaData base,
        ClassMetaData[] types, boolean subclasses, ExpressionFactory[] facts,
        QueryExpressions[] parsed, Object[] params) {
        return null;
    }

    /**
     * Return the commands that will be sent to the datastore in order
     * to execute the query, typically in the database's native language.
     *
     * @param base the base type the query should match
     * @param types the independent candidate types
     * @param subclasses true if subclasses should be included in the results
     * @param facts the expression factory used to build the query for
     * each base type
     * @param parsed the parsed query values
     * @param params parameter values, or empty array
     * @param range result range
     * @return a textual description of the query to execute
     */
    protected String[] getDataStoreActions(ClassMetaData base,
        ClassMetaData[] types, boolean subclasses, ExpressionFactory[] facts,
        QueryExpressions[] parsed, Object[] params, Range range) {
        return StoreQuery.EMPTY_STRINGS;
    }

    /**
     * Return the assignable types for the given metadata whose expression
     * trees must be compiled independently.
     */
    protected ClassMetaData[] getIndependentExpressionCandidates
        (ClassMetaData type, boolean subclasses) {
        return new ClassMetaData[]{ type };
    }

    /**
     * Return an {@link ExpressionFactory} to use to create an expression to
     * be executed against an extent. Each factory will be used to compile
     * one filter only. The factory must be cachable.
     */
    protected ExpressionFactory getExpressionFactory(ClassMetaData type) {
        throw new UnsupportedException();
    }

    /**
     * Provides support for queries that hold query information
     * in a {@link QueryExpressions} instance.
     *
     * @author Marc Prud'hommeaux
     */
    public static abstract class AbstractExpressionExecutor
        extends AbstractExecutor
        implements Executor {

        /**
         * Return the query expressions for one candidate type, or die if none.
         */
        private QueryExpressions assertQueryExpression() {
            QueryExpressions[] exp = getQueryExpressions();
            if (exp == null || exp.length < 1)
                throw new InvalidStateException(_loc.get("no-expressions"));

            return exp[0];
        }

        /**
         * Throw proper exception if given value is a collection/map/array.
         */
        protected void assertNotContainer(Value val, StoreQuery q) {
            // variables represent container elements, not the container itself
            if (val.isVariable())
                return;

            Class<?> type;
            if (val instanceof Path) {
                FieldMetaData fmd = ((Path) val).last();
                type = (fmd == null) ? val.getType() : fmd.getDeclaredType();
            } else
                type = val.getType();

            switch (JavaTypes.getTypeCode(type)) {
                case JavaTypes.ARRAY:
                case JavaTypes.COLLECTION:
                case JavaTypes.MAP:
                    throw new UserException(_loc.get("container-projection",
                        q.getContext().getQueryString()));
            }
        }

        @Override
        public final void validate(StoreQuery q) {
            QueryExpressions exps = assertQueryExpression();
            ValidateGroupingExpressionVisitor.validate(q.getContext(), exps);
        }


        @Override
        public void getRange(StoreQuery q, Object[] params, Range range) {
            QueryExpressions exps = assertQueryExpression();
            if (exps.range.length == 0)
                return;

            if (exps.range.length == 2
                && exps.range[0] instanceof Constant
                && exps.range[1] instanceof Constant) {
                try {
                    range.start = ((Number) ((Constant) exps.range[0]).
                        getValue(params)).longValue();
                    range.end = ((Number) ((Constant) exps.range[1]).
                        getValue(params)).longValue();
                    return;
                } catch (ClassCastException | NullPointerException cce) {
                    // fall through to exception below
                }
            }
            throw new UserException(_loc.get("only-range-constants",
                q.getContext().getQueryString()));
        }

        @Override
        public final Class<?> getResultClass(StoreQuery q) {
            return assertQueryExpression().resultClass;
        }

        @Override
        public final ResultShape<?> getResultShape(StoreQuery q) {
            return assertQueryExpression().shape;
        }

        @Override
        public final boolean[] getAscending(StoreQuery q) {
            return assertQueryExpression().ascending;
        }

        @Override
        public final String getAlias(StoreQuery q) {
            return assertQueryExpression().alias;
        }

        @Override
        public final String[] getProjectionAliases(StoreQuery q) {
            return assertQueryExpression().projectionAliases;
        }

        @Override
        public Class<?>[] getProjectionTypes(StoreQuery q) {
            return null;
        }

        @Override
        public final int getOperation(StoreQuery q) {
            return assertQueryExpression().operation;
        }

        @Override
        public final boolean isAggregate(StoreQuery q) {
            return assertQueryExpression().isAggregate();
        }

        @Override
        public final boolean isDistinct(StoreQuery q) {
            return assertQueryExpression().isDistinct();
        }

        @Override
        public final boolean hasGrouping(StoreQuery q) {
            return assertQueryExpression().grouping.length > 0;
        }

        @Override
        public final OrderedMap<Object,Class<?>> getOrderedParameterTypes(StoreQuery q) {
            return assertQueryExpression().parameterTypes;
        }

        /**
         * Creates a Object[] from the values of the given user parameters.
         */
        @Override
        public Object[] toParameterArray(StoreQuery q, Map<?,?> userParams) {
            if (userParams == null || userParams.isEmpty())
                return StoreQuery.EMPTY_OBJECTS;

            OrderedMap<?,Class<?>> paramTypes = getOrderedParameterTypes(q);
            Object[] arr = new Object[userParams.size()];
            int base = positionalParameterBase(userParams.keySet());
            for(Entry<?, Class<?>> entry : paramTypes.entrySet()){
                Object key = entry.getKey();
                int idx = (key instanceof Integer)
                    ? (Integer) key - base
                    : paramTypes.indexOf(key);
                if (idx >= arr.length || idx < 0)
                        throw new UserException(_loc.get("gap-query-param",
                            new Object[]{q.getContext().getQueryString(), key,
                            userParams.size(), userParams}));
                Object value = userParams.get(key);
                validateParameterValue(key, value, (Class)entry.getValue());
                arr[idx] = value;
            }
            return arr;
        }

        /**
         * Return the base (generally 0 or 1) to use for positional parameters.
         */
        private static int positionalParameterBase(Collection params) {
            int low = Integer.MAX_VALUE;
            Object obj;
            int val;
            for (Object param : params) {
                obj = param;
                if (!(obj instanceof Number))
                    return 0; // use 0 base when params are mixed types

                val = ((Number) obj).intValue();
                if (val == 0)
                    return val;
                if (val < low)
                    low = val;
            }
            return low;
        }

        private static void validateParameterValue(Object key, Object value,
            Class expected) {
            if (expected == null)
                return;

            if (value == null) {
                if (expected.isPrimitive())
                    throw new UserException(_loc.get("null-primitive-param",
                        key, expected));
            } else {
                Class actual = value.getClass();
                boolean strict = true;
                if (!Filters.canConvert(actual, expected, strict))
                    throw new UserException(_loc.get("param-value-mismatch",
                        new Object[]{key, expected, value, actual}));
            }
        }

        @Override
        public final Map getUpdates(StoreQuery q) {
            return assertQueryExpression().updates;
        }

        @Override
        public final ClassMetaData[] getAccessPathMetaDatas(StoreQuery q) {
            QueryExpressions[] exps = getQueryExpressions();
            if (exps.length == 1)
                return exps[0].accessPath;

            List<ClassMetaData> metas = null;
            for (QueryExpressions exp : exps)
                metas = Filters.addAccessPathMetaDatas(metas,
                        exp.accessPath);
            if (metas == null)
                return StoreQuery.EMPTY_METAS;
            return (ClassMetaData[]) metas.toArray
                (new ClassMetaData[metas.size()]);
        }

        @Override
        public boolean isPacking(StoreQuery q) {
            return false;
        }

        /**
         * Throws an exception if select or having clauses contain
         * non-aggregate, non-grouped paths.
         */
        private static class ValidateGroupingExpressionVisitor
            extends AbstractExpressionVisitor {

            private final QueryContext _ctx;
            private boolean _grouping = false;
            private Set _grouped = null;
            private Value _agg = null;

            /**
             * Throw proper exception if query does not meet validation.
             */
            public static void validate(QueryContext ctx,
                QueryExpressions exps) {
                if (exps.grouping.length == 0)
                    return;

                ValidateGroupingExpressionVisitor visitor =
                    new ValidateGroupingExpressionVisitor(ctx);
                visitor._grouping = true;
                for (int i = 0; i < exps.grouping.length; i++)
                    exps.grouping[i].acceptVisit(visitor);
                visitor._grouping = false;
                if (exps.having != null) {
                    Class cls = exps.having.getClass();
                    if (cls.getName().endsWith("Expression"))
                        cls = cls.getSuperclass();
                    Object value2 = null;
                    Method getValue2 = null;
                    try {
                        getValue2 = cls.getMethod("getValue2");
                        getValue2.setAccessible(true);
                        value2 = getValue2.invoke(exps.having, (Object[]) null);
                    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException name) {
                        // skip
                    }
                    if (value2 != null && value2 instanceof Subquery)
                        ;  // complex having with subquery, validation is performed by DBMS
                    else
                        exps.having.acceptVisit(visitor);
                }
                for (int i = 0; i < exps.projections.length; i++)
                    exps.projections[i].acceptVisit(visitor);
            }

            public ValidateGroupingExpressionVisitor(QueryContext ctx) {
                _ctx = ctx;
            }

            @Override
            public void enter(Value val) {
                if (_grouping) {
                    if (val instanceof Path) {
                        if (_grouped == null)
                            _grouped = new HashSet();
                        _grouped.add(val);
                    }
                } else if (_agg == null) {
                    if (val.isAggregate())
                        _agg = val;
                    else if (val instanceof Path
                        && (_grouped == null || !_grouped.contains(val))) {
                        throw new UserException(_loc.get("bad-grouping",
                            _ctx.getCandidateType(), _ctx.getQueryString()));
                    }
                }
            }

            @Override
            public void exit(Value val) {
                if (val == _agg)
                    _agg = null;
            }
        }
    }

    /**
     * Runs the expression query in memory.
     */
    public static class InMemoryExecutor
        extends AbstractExpressionExecutor
        implements Executor, Serializable {

        
        private static final long serialVersionUID = 1L;
        private final ClassMetaData _meta;
        private final boolean _subs;
        private final InMemoryExpressionFactory _factory;
        private final QueryExpressions[] _exps;
        private final Class[] _projTypes;

        public InMemoryExecutor(ExpressionStoreQuery q,
            ClassMetaData candidate, boolean subclasses,
            ExpressionParser parser, Object parsed, InMemoryExpressionFactory factory) {
            _meta = candidate;
            _subs = subclasses;
            _factory = factory;

            _exps = new QueryExpressions[] {
                parser.eval(parsed, q, _factory, _meta)
            };
            if (_exps[0].projections.length == 0)
                _projTypes = StoreQuery.EMPTY_CLASSES;
            else {
                AssertNoVariablesExpressionVisitor novars = new
                    AssertNoVariablesExpressionVisitor(q.getContext());
                _projTypes = new Class[_exps[0].projections.length];
                for (int i = 0; i < _exps[0].projections.length; i++) {
                    _projTypes[i] = _exps[0].projections[i].getType();
                    assertNotContainer(_exps[0].projections[i], q);
                    _exps[0].projections[i].acceptVisit(novars);
                }
                for (int i = 0; i < _exps[0].grouping.length; i++)
                    _exps[0].grouping[i].acceptVisit(novars);
            }
        }

        @Override
        public QueryExpressions[] getQueryExpressions() {
            return _exps;
        }

        @Override
        public ResultObjectProvider executeQuery(StoreQuery q,
            Object[] params, Range range) {
            // execute in memory for candidate collection;
            // also execute in memory for transactional extents
            Collection coll = q.getContext().getCandidateCollection();
            Iterator itr;
            if (coll != null)
                itr = coll.iterator();
            else
                itr = q.getContext().getStoreContext().
                    extentIterator(_meta.getDescribedType(), _subs,
                        q.getContext().getFetchConfiguration(),
                        q.getContext().getIgnoreChanges());

            // find matching objects
            List results = new ArrayList();
            StoreContext ctx = q.getContext().getStoreContext();
            try {
                Object obj;
                while (itr.hasNext()) {
                    obj = itr.next();
                    if (_factory.matches(_exps[0], _meta, _subs, obj, ctx,
                        params))
                        results.add(obj);
                }
            }
            finally {
                ImplHelper.close(itr);
            }

            // group results
            results = _factory.group(_exps[0], results, ctx, params);

            // apply having to filter groups
            if (_exps[0].having != null) {
                List matches = new ArrayList(results.size());
                Collection c;
                itr = results.iterator();
                while (itr.hasNext()) {
                    c = (Collection) itr.next();
                    if (_factory.matches(_exps[0], c, ctx, params))
                        matches.add(c);
                }
                results = matches;
            }

            // apply projections, order results, and filter duplicates
            results = _factory.project(_exps[0], results, ctx, params);
            results = _factory.order(_exps[0], results, ctx, params);
            results = _factory.distinct(_exps[0], coll == null, results);

            ResultObjectProvider rop = new ListResultObjectProvider(results);
            if (range.start != 0 || range.end != Long.MAX_VALUE)
                rop = new RangeResultObjectProvider(rop, range.start,range.end);
            return rop;
        }

        @Override
        public String[] getDataStoreActions(StoreQuery q, Object[] params,
            Range range) {
            // in memory queries have no datastore actions to perform
            return StoreQuery.EMPTY_STRINGS;
        }

        @Override
        public Object getOrderingValue(StoreQuery q, Object[] params,
            Object resultObject, int orderIndex) {
            // if this is a projection, then we have to order on something
            // we selected
            if (_exps[0].projections.length > 0) {
                String ordering = _exps[0].orderingClauses[orderIndex];
                for (int i = 0; i < _exps[0].projectionClauses.length; i++)
                    if (ordering.equals(_exps[0].projectionClauses[i]))
                        return ((Object[]) resultObject)[i];

                throw new InvalidStateException(_loc.get
                    ("merged-order-with-result", q.getContext().getLanguage(),
                        q.getContext().getQueryString(), ordering));
            }

            // use the parsed ordering expression to extract the ordering value
            Val val = (Val) _exps[0].ordering[orderIndex];
            return val.evaluate(resultObject, resultObject, q.getContext().
                getStoreContext(), params);
        }

        @Override
        public Class[] getProjectionTypes(StoreQuery q) {
            return _projTypes;
        }

        /**
         * Throws an exception if a variable is found.
         */
        private static class AssertNoVariablesExpressionVisitor
            extends AbstractExpressionVisitor {

            private final QueryContext _ctx;

            public AssertNoVariablesExpressionVisitor(QueryContext ctx) {
                _ctx = ctx;
            }

            @Override
            public void enter(Value val) {
                if (!val.isVariable())
                    return;
                throw new UnsupportedException(_loc.get("inmem-agg-proj-var",
                    _ctx.getCandidateType(), _ctx.getQueryString()));
            }
        }
    }

    /**
     * The DataStoreExecutor executes the query against the
     * implementation's overridden {@link #executeQuery} method.
     *
     * @author Marc Prud'hommeaux
     */
    public static class DataStoreExecutor
        extends AbstractExpressionExecutor
        implements Executor, Serializable {

        
        private static final long serialVersionUID = 1L;
        private ClassMetaData _meta;
        private ClassMetaData[] _metas;
        private boolean _subs;
        private ExpressionParser _parser;
        private ExpressionFactory[] _facts;
        private QueryExpressions[] _exps;
        private Class[] _projTypes;
        private Value[] _inMemOrdering;

        public DataStoreExecutor(ExpressionStoreQuery q,
            ClassMetaData meta, boolean subclasses,
            ExpressionParser parser, Object parsed) {
            _metas = q.getIndependentExpressionCandidates(meta, subclasses);
            if (_metas.length == 0)
                throw new UserException(_loc.get("query-unmapped", meta));
            _meta = meta;
            _subs = subclasses;
            _parser = parser;

            _facts = new ExpressionFactory[_metas.length];
            for (int i = 0; i < _facts.length; i++)
                _facts[i] = q.getExpressionFactory(_metas[i]);

            _exps = new QueryExpressions[_metas.length];
            for (int i = 0; i < _exps.length; i++)
                _exps[i] = parser.eval(parsed, q, _facts[i], _metas[i]);

            if (_exps[0].projections.length == 0)
                _projTypes = StoreQuery.EMPTY_CLASSES;
            else {
                _projTypes = new Class[_exps[0].projections.length];
                for (int i = 0; i < _exps[0].projections.length; i++) {
                    assertNotContainer(_exps[0].projections[i], q);
                    _projTypes[i] = _exps[0].projections[i].getType();
                }
            }
        }

        @Override
        public QueryExpressions[] getQueryExpressions() {
            return _exps;
        }

        @Override
        public ResultObjectProvider executeQuery(StoreQuery q,
            Object[] params, Range range) {
            range.lrs &= !isAggregate(q) && !hasGrouping(q);
            return ((ExpressionStoreQuery) q).executeQuery(this, _meta, _metas,
                _subs, _facts, _exps, params, range);
        }

        @Override
        public Number executeDelete(StoreQuery q, Object[] params) {
            try {
                Number num =
                    ((ExpressionStoreQuery) q).executeDelete(this, _meta, _metas, _subs, _facts, _exps, params);
                if (num == null)
                    return q.getContext().deleteInMemory(q, this, params);
                return num;
            } finally {
                for (ClassMetaData cmd : getAccessPathMetaDatas(q)) {
                    DataCache cache = cmd.getDataCache();
                    if (cache != null && cache.getEvictOnBulkUpdate()) {
                        cache.removeAll(cmd.getDescribedType(), true);
                    }
                }
            }
        }

        @Override
        public Number executeUpdate(StoreQuery q, Object[] params) {
            try {
                Number num =
                    ((ExpressionStoreQuery) q).executeUpdate(this, _meta, _metas, _subs, _facts, _exps, params);
                if (num == null)
                    return q.getContext().updateInMemory(q, this, params);
                return num;
            } finally {
                for (ClassMetaData cmd : getAccessPathMetaDatas(q)) {
                    DataCache cache = cmd.getDataCache();
                    if (cache != null && cache.getEvictOnBulkUpdate()) {
                        cache.removeAll(cmd.getDescribedType(), true);
                    }
                }
            }
        }

        @Override
        public String[] getDataStoreActions(StoreQuery q, Object[] params,
            Range range) {
            return ((ExpressionStoreQuery) q).getDataStoreActions(_meta,
                _metas, _subs, _facts, _exps, params, range);
        }

        @Override
        public Object getOrderingValue(StoreQuery q, Object[] params,
            Object resultObject, int orderIndex) {
            // if this is a projection, then we have to order on something
            // we selected
            if (_exps[0].projections.length > 0) {
                String ordering = _exps[0].orderingClauses[orderIndex];
                for (int i = 0; i < _exps[0].projectionClauses.length; i++)
                    if (ordering.equals(_exps[0].projectionClauses[i]))
                        return ((Object[]) resultObject)[i];

                throw new InvalidStateException(_loc.get
                    ("merged-order-with-result", q.getContext().getLanguage(),
                        q.getContext().getQueryString(), ordering));
            }

            // need to parse orderings?
            synchronized (this) {
                if (_inMemOrdering == null) {
                    ExpressionFactory factory = new InMemoryExpressionFactory();
                    _inMemOrdering = _parser.eval(_exps[0].orderingClauses,
                        (ExpressionStoreQuery) q, factory, _meta);
                }
                if (_inMemOrdering == null)
                    _inMemOrdering = _exps[0].ordering;
            }

            // use the parsed ordering expression to extract the ordering value
            Val val = (Val) _inMemOrdering[orderIndex];
            return val.evaluate(resultObject, resultObject,
                q.getContext().getStoreContext(), params);
        }

        @Override
        public Class[] getProjectionTypes(StoreQuery q) {
            return _projTypes;
		}
	}
}
