001/*
002// This software is subject to the terms of the Eclipse Public License v1.0
003// Agreement, available at the following URL:
004// http://www.eclipse.org/legal/epl-v10.html.
005// You must accept the terms of that agreement to use this software.
006//
007// Copyright (C) 2004-2005 TONBELLER AG
008// Copyright (C) 2005-2005 Julian Hyde
009// Copyright (C) 2005-2013 Pentaho and others
010// All Rights Reserved.
011*/
012package mondrian.rolap;
013
014import mondrian.calc.ResultStyle;
015import mondrian.calc.TupleList;
016import mondrian.calc.impl.DelegatingTupleList;
017import mondrian.olap.*;
018import mondrian.rolap.TupleReader.MemberBuilder;
019import mondrian.rolap.aggmatcher.AggStar;
020import mondrian.rolap.cache.*;
021import mondrian.rolap.sql.*;
022
023import org.apache.log4j.Logger;
024
025import java.util.*;
026
027import javax.sql.DataSource;
028
029/**
030 * Analyses set expressions and executes them in SQL if possible.
031 * Supports crossjoin, member.children, level.members and member.descendants -
032 * all in non empty mode, i.e. there is a join to the fact table.<p/>
033 *
034 * <p>TODO: the order of the result is different from the order of the
035 * enumeration. Should sort.
036 *
037 * @author av
038 * @since Nov 12, 2005
039  */
040public abstract class RolapNativeSet extends RolapNative {
041    protected static final Logger LOGGER =
042        Logger.getLogger(RolapNativeSet.class);
043
044    private SmartCache<Object, TupleList> cache =
045        new SoftSmartCache<Object, TupleList>();
046
047    /**
048     * Returns whether certain member types (e.g. calculated members) should
049     * disable native SQL evaluation for expressions containing them.
050     *
051     * <p>If true, expressions containing calculated members will be evaluated
052     * by the interpreter, instead of using SQL.
053     *
054     * <p>If false, calc members will be ignored and the computation will be
055     * done in SQL, returning more members than requested.  This is ok, if
056     * the superflous members are filtered out in java code afterwards.
057     *
058     * @return whether certain member types should disable native SQL evaluation
059     */
060    protected abstract boolean restrictMemberTypes();
061
062    protected CrossJoinArgFactory crossJoinArgFactory() {
063        return new CrossJoinArgFactory(restrictMemberTypes());
064    }
065
066    /**
067     * Constraint for non empty {crossjoin, member.children,
068     * member.descendants, level.members}
069     */
070    protected static abstract class SetConstraint extends SqlContextConstraint {
071        CrossJoinArg[] args;
072
073        SetConstraint(
074            CrossJoinArg[] args,
075            RolapEvaluator evaluator,
076            boolean strict)
077        {
078            super(evaluator, strict);
079            this.args = args;
080        }
081
082        /**
083         * {@inheritDoc}
084         *
085         * <p>If there is a crossjoin, we need to join the fact table - even if
086         * the evaluator context is empty.
087         */
088        protected boolean isJoinRequired() {
089            return args.length > 1 || super.isJoinRequired();
090        }
091
092        public void addConstraint(
093            SqlQuery sqlQuery,
094            RolapCube baseCube,
095            AggStar aggStar)
096        {
097            super.addConstraint(sqlQuery, baseCube, aggStar);
098            for (CrossJoinArg arg : args) {
099                // if the cross join argument has calculated members in its
100                // enumerated set, ignore the constraint since we won't
101                // produce that set through the native sql and instead
102                // will simply enumerate through the members in the set
103                if (!(arg instanceof MemberListCrossJoinArg)
104                    || !((MemberListCrossJoinArg) arg).hasCalcMembers())
105                {
106                    RolapLevel level = arg.getLevel();
107                    if (level == null || levelIsOnBaseCube(baseCube, level)) {
108                        arg.addConstraint(sqlQuery, baseCube, aggStar);
109                    }
110                }
111            }
112        }
113
114        private boolean levelIsOnBaseCube(
115            final RolapCube baseCube, final RolapLevel level)
116        {
117            return baseCube.findBaseCubeHierarchy(level.getHierarchy()) != null;
118        }
119
120        /**
121         * Returns null to prevent the member/childern from being cached. There
122         * exists no valid MemberChildrenConstraint that would fetch those
123         * children that were extracted as a side effect from evaluating a non
124         * empty crossjoin
125         */
126        public MemberChildrenConstraint getMemberChildrenConstraint(
127            RolapMember parent)
128        {
129            return null;
130        }
131
132        /**
133         * returns a key to cache the result
134         */
135        public Object getCacheKey() {
136            List<Object> key = new ArrayList<Object>();
137            key.add(super.getCacheKey());
138            // only add args that will be retrieved through native sql;
139            // args that are sets with calculated members aren't executed
140            // natively
141            for (CrossJoinArg arg : args) {
142                if (!(arg instanceof MemberListCrossJoinArg)
143                    || !((MemberListCrossJoinArg) arg).hasCalcMembers())
144                {
145                    key.add(arg);
146                }
147            }
148            return key;
149        }
150    }
151
152    protected class SetEvaluator implements NativeEvaluator {
153        private final CrossJoinArg[] args;
154        private final SchemaReaderWithMemberReaderAvailable schemaReader;
155        private final TupleConstraint constraint;
156        private int maxRows = 0;
157
158        public SetEvaluator(
159            CrossJoinArg[] args,
160            SchemaReader schemaReader,
161            TupleConstraint constraint)
162        {
163            this.args = args;
164            if (schemaReader instanceof SchemaReaderWithMemberReaderAvailable) {
165                this.schemaReader =
166                    (SchemaReaderWithMemberReaderAvailable) schemaReader;
167            } else {
168                this.schemaReader =
169                    new SchemaReaderWithMemberReaderCache(schemaReader);
170            }
171            this.constraint = constraint;
172        }
173
174        public Object execute(ResultStyle desiredResultStyle) {
175            switch (desiredResultStyle) {
176            case ITERABLE:
177                for (CrossJoinArg arg : this.args) {
178                    if (arg.getLevel().getDimension().isHighCardinality()) {
179                        // If any of the dimensions is a HCD,
180                        // use the proper tuple reader.
181                        return executeList(
182                            new HighCardSqlTupleReader(constraint));
183                    }
184                    // Use the regular tuple reader.
185                    return executeList(
186                        new SqlTupleReader(constraint));
187                }
188            case MUTABLE_LIST:
189            case LIST:
190                return executeList(new SqlTupleReader(constraint));
191            default:
192                throw ResultStyleException.generate(
193                    ResultStyle.ITERABLE_MUTABLELIST_LIST,
194                    Collections.singletonList(desiredResultStyle));
195            }
196        }
197
198        protected TupleList executeList(final SqlTupleReader tr) {
199            tr.setMaxRows(maxRows);
200            for (CrossJoinArg arg : args) {
201                addLevel(tr, arg);
202            }
203
204            // Look up the result in cache; we can't return the cached
205            // result if the tuple reader contains a target with calculated
206            // members because the cached result does not include those
207            // members; so we still need to cross join the cached result
208            // with those enumerated members.
209            //
210            // The key needs to include the arguments (projection) as well as
211            // the constraint, because it's possible (see bug MONDRIAN-902)
212            // that independent axes have identical constraints but different
213            // args (i.e. projections). REVIEW: In this case, should we use the
214            // same cached result and project different columns?
215            List<Object> key = new ArrayList<Object>();
216            key.add(tr.getCacheKey());
217            key.addAll(Arrays.asList(args));
218            key.add(maxRows);
219
220            TupleList result = cache.get(key);
221            boolean hasEnumTargets = (tr.getEnumTargetCount() > 0);
222            if (result != null && !hasEnumTargets) {
223                if (listener != null) {
224                    TupleEvent e = new TupleEvent(this, tr);
225                    listener.foundInCache(e);
226                }
227                return new DelegatingTupleList(
228                    args.length, Util.<List<Member>>cast(result));
229            }
230
231            // execute sql and store the result
232            if (result == null && listener != null) {
233                TupleEvent e = new TupleEvent(this, tr);
234                listener.executingSql(e);
235            }
236
237            // if we don't have a cached result in the case where we have
238            // enumerated targets, then retrieve and cache that partial result
239            TupleList partialResult = result;
240            List<List<RolapMember>> newPartialResult = null;
241            if (hasEnumTargets && partialResult == null) {
242                newPartialResult = new ArrayList<List<RolapMember>>();
243            }
244            DataSource dataSource = schemaReader.getDataSource();
245            if (args.length == 1) {
246                result =
247                    tr.readMembers(
248                        dataSource, partialResult, newPartialResult);
249            } else {
250                result =
251                    tr.readTuples(
252                        dataSource, partialResult, newPartialResult);
253            }
254
255            if (!MondrianProperties.instance().DisableCaching.get()) {
256                if (hasEnumTargets) {
257                    if (newPartialResult != null) {
258                        cache.put(
259                            key,
260                            new DelegatingTupleList(
261                                args.length,
262                                Util.<List<Member>>cast(newPartialResult)));
263                    }
264                } else {
265                    cache.put(key, result);
266                }
267            }
268            return result;
269        }
270
271        private void addLevel(TupleReader tr, CrossJoinArg arg) {
272            RolapLevel level = arg.getLevel();
273            if (level == null) {
274                // Level can be null if the CrossJoinArg represent
275                // an empty set.
276                // This is used to push down the "1 = 0" predicate
277                // into the emerging CJ so that the entire CJ can
278                // be natively evaluated.
279                return;
280            }
281
282            RolapHierarchy hierarchy = level.getHierarchy();
283            MemberReader mr = schemaReader.getMemberReader(hierarchy);
284            MemberBuilder mb = mr.getMemberBuilder();
285            Util.assertTrue(mb != null, "MemberBuilder not found");
286
287            if (arg instanceof MemberListCrossJoinArg
288                && ((MemberListCrossJoinArg) arg).hasCalcMembers())
289            {
290                // only need to keep track of the members in the case
291                // where there are calculated members since in that case,
292                // we produce the values by enumerating through the list
293                // rather than generating the values through native sql
294                tr.addLevelMembers(level, mb, arg.getMembers());
295            } else {
296                tr.addLevelMembers(level, mb, null);
297            }
298        }
299
300        int getMaxRows() {
301            return maxRows;
302        }
303
304        void setMaxRows(int maxRows) {
305            this.maxRows = maxRows;
306        }
307    }
308
309    /**
310     * Tests whether non-native evaluation is preferred for the
311     * given arguments.
312     *
313     * @param joinArg true if evaluating a cross-join; false if
314     * evaluating a single-input expression such as filter
315     *
316     * @return true if <em>all</em> args prefer the interpreter
317     */
318    protected boolean isPreferInterpreter(
319        CrossJoinArg[] args,
320        boolean joinArg)
321    {
322        for (CrossJoinArg arg : args) {
323            if (!arg.isPreferInterpreter(joinArg)) {
324                return false;
325            }
326        }
327        return true;
328    }
329
330    /** disable garbage collection for test */
331    @SuppressWarnings({ "unchecked", "rawtypes" })
332    void useHardCache(boolean hard) {
333        if (hard) {
334            cache = new HardSmartCache();
335        } else {
336            cache = new SoftSmartCache();
337        }
338    }
339
340    /**
341     * Overrides current members in position by default members in
342     * hierarchies which are involved in this filter/topcount.
343     * Stores the RolapStoredMeasure into the context because that is needed to
344     * generate a cell request to constraint the sql.
345     *
346     * <p>The current context may contain a calculated measure, this measure
347     * was translated into an sql condition (filter/topcount). The measure
348     * is not used to constrain the result but only to access the star.
349     *
350     * @param evaluator Evaluation context to modify
351     * @param cargs Cross join arguments
352     * @param storedMeasure Stored measure
353     *
354     * @see RolapAggregationManager#makeRequest(RolapEvaluator)
355     */
356    protected void overrideContext(
357        RolapEvaluator evaluator,
358        CrossJoinArg[] cargs,
359        RolapStoredMeasure storedMeasure)
360    {
361        SchemaReader schemaReader = evaluator.getSchemaReader();
362        for (CrossJoinArg carg : cargs) {
363            RolapLevel level = carg.getLevel();
364            if (level != null) {
365                RolapHierarchy hierarchy = level.getHierarchy();
366
367                final Member contextMember;
368                if (hierarchy.hasAll()
369                    || schemaReader.getRole()
370                    .getAccess(hierarchy) == Access.ALL)
371                {
372                    // The hierarchy may have access restrictions.
373                    // If it does, calling .substitute() will retrieve an
374                    // appropriate LimitedRollupMember.
375                    contextMember =
376                        schemaReader.substitute(hierarchy.getAllMember());
377                } else {
378                    // If there is no All member on a role restricted hierarchy,
379                    // use a restricted member based on the set of accessible
380                    // root members.
381                    contextMember = new RestrictedMemberReader
382                        .MultiCardinalityDefaultMember(
383                            hierarchy.getMemberReader()
384                                .getRootMembers().get(0));
385                }
386                evaluator.setContext(contextMember);
387            }
388        }
389        if (storedMeasure != null) {
390            evaluator.setContext(storedMeasure);
391        }
392    }
393
394
395    public interface SchemaReaderWithMemberReaderAvailable
396        extends SchemaReader
397    {
398        MemberReader getMemberReader(Hierarchy hierarchy);
399    }
400
401    private static class SchemaReaderWithMemberReaderCache
402        extends DelegatingSchemaReader
403        implements SchemaReaderWithMemberReaderAvailable
404    {
405        private final Map<Hierarchy, MemberReader> hierarchyReaders =
406            new HashMap<Hierarchy, MemberReader>();
407
408        SchemaReaderWithMemberReaderCache(SchemaReader schemaReader) {
409            super(schemaReader);
410        }
411
412        public synchronized MemberReader getMemberReader(Hierarchy hierarchy) {
413            MemberReader memberReader = hierarchyReaders.get(hierarchy);
414            if (memberReader == null) {
415                memberReader =
416                    ((RolapHierarchy) hierarchy).createMemberReader(
417                        schemaReader.getRole());
418                hierarchyReaders.put(hierarchy, memberReader);
419            }
420            return memberReader;
421        }
422    }
423}
424
425// End RolapNativeSet.java
426