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) 2006-2013 Pentaho and others
009// All Rights Reserved.
010*/
011package mondrian.rolap;
012
013import mondrian.mdx.MemberExpr;
014import mondrian.mdx.ResolvedFunCall;
015import mondrian.olap.*;
016import mondrian.rolap.RestrictedMemberReader.MultiCardinalityDefaultMember;
017import mondrian.rolap.RolapHierarchy.LimitedRollupMember;
018import mondrian.rolap.aggmatcher.AggStar;
019import mondrian.rolap.sql.*;
020
021import java.util.*;
022
023/**
024 * limits the result of a Member SQL query to the current evaluation context.
025 * All Members of the current context are joined against the fact table and only
026 * those rows are returned, that have an entry in the fact table.
027 *
028 * <p>For example, if you have two dimensions, "invoice" and "time", and the
029 * current context (e.g. the slicer) contains a day from the "time" dimension,
030 * then only the invoices of that day are found. Used to optimize NON EMPTY.
031 *
032 * <p> The {@link TupleConstraint} methods may silently ignore calculated
033 * members (depends on the <code>strict</code> c'tor argument), so these may
034 * return more members than the current context restricts to. The
035 * MemberChildren methods will never accept calculated members as parents,
036 * these will cause an exception.
037 *
038 * @author av
039 * @since Nov 2, 2005
040 */
041public class SqlContextConstraint
042    implements MemberChildrenConstraint, TupleConstraint
043{
044    private final List<Object> cacheKey;
045    private Evaluator evaluator;
046    private boolean strict;
047
048    /**
049     * @param context evaluation context
050     * @param strict false if more rows than requested may be returned
051     * (i.e. the constraint is incomplete)
052     *
053     * @return false if this contstraint will not work for the current context
054     */
055    public static boolean isValidContext(Evaluator context, boolean strict) {
056        return isValidContext(context, true, null, strict);
057    }
058
059    /**
060     * @param context evaluation context
061     * @param disallowVirtualCube if true, check for virtual cubes
062     * @param levels levels being referenced in the current context
063     * @param strict false if more rows than requested may be returned
064     * (i.e. the constraint is incomplete)
065     *
066     * @return false if constraint will not work for current context
067     */
068    public static boolean isValidContext(
069        Evaluator context,
070        boolean disallowVirtualCube,
071        Level [] levels,
072        boolean strict)
073    {
074        if (context == null) {
075            return false;
076        }
077        RolapCube cube = (RolapCube) context.getCube();
078        if (disallowVirtualCube) {
079            if (cube.isVirtual()) {
080                return false;
081            }
082        }
083        if (cube.isVirtual()) {
084            Query query = context.getQuery();
085            Set<RolapCube> baseCubes = new HashSet<RolapCube>();
086            List<RolapCube> baseCubeList = new ArrayList<RolapCube>();
087            if (!findVirtualCubeBaseCubes(query, baseCubes, baseCubeList)) {
088                return false;
089            }
090            assert levels != null;
091            query.setBaseCubes(baseCubeList);
092        }
093
094        // may return more rows than requested?
095        if (!strict) {
096            return true;
097        }
098
099        // we can not handle all calc members in slicer. Calc measure and some
100        // like aggregates are exceptions
101        Member[] members = context.getMembers();
102        for (int i = 1; i < members.length; i++) {
103            if (members[i].isCalculated()
104                && !SqlConstraintUtils.isSupportedCalculatedMember(members[i]))
105            {
106                return false;
107            }
108        }
109        return true;
110    }
111
112    /**
113     * Locates base cubes related to the measures referenced in the query.
114     *
115     * @param query query referencing the virtual cube
116     * @param baseCubes set of base cubes
117     *
118     * @return true if valid measures exist
119     */
120    private static boolean findVirtualCubeBaseCubes(
121        Query query,
122        Set<RolapCube> baseCubes,
123        List<RolapCube> baseCubeList)
124    {
125        // Gather the unique set of level-to-column maps corresponding
126        // to the underlying star/cube where the measure column
127        // originates from.
128        Set<Member> measureMembers = query.getMeasuresMembers();
129        // if no measures are explicitly referenced, just use the default
130        // measure
131        if (measureMembers.isEmpty()) {
132            Cube cube = query.getCube();
133            Dimension dimension = cube.getDimensions()[0];
134            query.addMeasuresMembers(
135                dimension.getHierarchy().getDefaultMember());
136        }
137        for (Member member : query.getMeasuresMembers()) {
138            if (member instanceof RolapStoredMeasure) {
139                addMeasure(
140                    (RolapStoredMeasure) member, baseCubes, baseCubeList);
141            } else if (member instanceof RolapCalculatedMember) {
142                findMeasures(member.getExpression(), baseCubes, baseCubeList);
143            }
144        }
145        if (baseCubes.isEmpty()) {
146            return false;
147        }
148        return true;
149    }
150
151    /**
152     * Adds information regarding a stored measure to maps
153     *
154     * @param measure the stored measure
155     * @param baseCubes set of base cubes
156     */
157    private static void addMeasure(
158        RolapStoredMeasure measure,
159        Set<RolapCube> baseCubes,
160        List<RolapCube> baseCubeList)
161    {
162        RolapCube baseCube = measure.getCube();
163        if (baseCubes.add(baseCube)) {
164            baseCubeList.add(baseCube);
165        }
166    }
167
168    /**
169     * Extracts the stored measures referenced in an expression
170     *
171     * @param exp expression
172     * @param baseCubes set of base cubes
173     */
174    private static void findMeasures(
175        Exp exp,
176        Set<RolapCube> baseCubes,
177        List<RolapCube> baseCubeList)
178    {
179        if (exp instanceof MemberExpr) {
180            MemberExpr memberExpr = (MemberExpr) exp;
181            Member member = memberExpr.getMember();
182            if (member instanceof RolapStoredMeasure) {
183                addMeasure(
184                    (RolapStoredMeasure) member, baseCubes, baseCubeList);
185            } else if (member instanceof RolapCalculatedMember) {
186                findMeasures(member.getExpression(), baseCubes, baseCubeList);
187            }
188        } else if (exp instanceof ResolvedFunCall) {
189            ResolvedFunCall funCall = (ResolvedFunCall) exp;
190            Exp [] args = funCall.getArgs();
191            for (Exp arg : args) {
192                findMeasures(arg, baseCubes, baseCubeList);
193            }
194        }
195    }
196
197    /**
198    * Creates a SqlContextConstraint.
199    *
200    * @param evaluator Evaluator
201    * @param strict defines the behaviour if the evaluator context
202    * contains calculated members. If true, an exception is thrown,
203    * otherwise calculated members are silently ignored. The
204    * methods {@link mondrian.rolap.sql.MemberChildrenConstraint#addMemberConstraint(mondrian.rolap.sql.SqlQuery, mondrian.rolap.RolapCube, mondrian.rolap.aggmatcher.AggStar, RolapMember)} and
205    * {@link mondrian.rolap.sql.MemberChildrenConstraint#addMemberConstraint(mondrian.rolap.sql.SqlQuery, mondrian.rolap.RolapCube, mondrian.rolap.aggmatcher.AggStar, java.util.List)} will
206    * never accept a calculated member as parent.
207    */
208    SqlContextConstraint(RolapEvaluator evaluator, boolean strict) {
209        this.evaluator = evaluator.push();
210        this.strict = strict;
211        cacheKey = new ArrayList<Object>();
212        cacheKey.add(getClass());
213        cacheKey.add(strict);
214
215        List<Member> members = new ArrayList<Member>();
216        List<Member> expandedMembers = new ArrayList<Member>();
217
218        members.addAll(
219            Arrays.asList(
220                SqlConstraintUtils.removeMultiPositionSlicerMembers(
221                    evaluator.getMembers(), evaluator)));
222
223        // Now we'll need to expand the aggregated members
224        expandedMembers.addAll(
225            Arrays.asList(
226                SqlConstraintUtils.expandSupportedCalculatedMembers(
227                    members,
228                    evaluator)));
229        cacheKey.add(expandedMembers);
230
231        // Add restrictions imposed by Role based access filtering
232        Map<Level, List<RolapMember>> roleMembers =
233            SqlConstraintUtils.getRoleConstraintMembers(
234                this.getEvaluator().getSchemaReader(),
235                this.getEvaluator().getMembers());
236        for (List<RolapMember> list : roleMembers.values()) {
237            cacheKey.addAll(list);
238        }
239
240        // For virtual cubes, context constraint should be evaluated in the
241        // query's context, because the query might reference different base
242        // cubes.
243        //
244        // Note: we could avoid adding base cubes to the key if the evaluator
245        // contains measure members referenced in the query, rather than
246        // just the default measure for the entire virtual cube. The commented
247        // code in RolapResult() that replaces the default measure seems to
248        // do that.
249        if (evaluator.getCube().isVirtual()) {
250            cacheKey.addAll(evaluator.getQuery().getBaseCubes());
251        }
252    }
253
254    /**
255     * Called from MemberChildren: adds <code>parent</code> to the current
256     * context and restricts the SQL resultset to that new context.
257     */
258    public void addMemberConstraint(
259        SqlQuery sqlQuery,
260        RolapCube baseCube,
261        AggStar aggStar,
262        RolapMember parent)
263    {
264        if (parent.isCalculated()) {
265            throw Util.newInternal("cannot restrict SQL to calculated member");
266        }
267        final int savepoint = evaluator.savepoint();
268        try {
269            evaluator.setContext(parent);
270            SqlConstraintUtils.addContextConstraint(
271                sqlQuery, aggStar, evaluator, baseCube, strict);
272        } finally {
273            evaluator.restore(savepoint);
274        }
275
276        // comment out addMemberConstraint here since constraint
277        // is already added by addContextConstraint
278        // SqlConstraintUtils.addMemberConstraint(
279        //        sqlQuery, baseCube, aggStar, parent, true);
280    }
281
282    /**
283     * Adds <code>parents</code> to the current
284     * context and restricts the SQL resultset to that new context.
285     */
286    public void addMemberConstraint(
287        SqlQuery sqlQuery,
288        RolapCube baseCube,
289        AggStar aggStar,
290        List<RolapMember> parents)
291    {
292        SqlConstraintUtils.addContextConstraint(
293            sqlQuery, aggStar, evaluator, baseCube, strict);
294        boolean exclude = false;
295        SqlConstraintUtils.addMemberConstraint(
296            sqlQuery, baseCube, aggStar, parents, true, false, exclude);
297    }
298
299    /**
300     * Called from LevelMembers: restricts the SQL resultset to the current
301     * context.
302     */
303    public void addConstraint(
304        SqlQuery sqlQuery,
305        RolapCube baseCube,
306        AggStar aggStar)
307    {
308        SqlConstraintUtils.addContextConstraint(
309            sqlQuery, aggStar, evaluator, baseCube, strict);
310    }
311
312    /**
313     * Returns whether a join with the fact table is required. A join is
314     * required if the context contains members from dimensions other than
315     * level. If we are interested in the members of a level or a members
316     * children then it does not make sense to join only one dimension (the one
317     * that contains the requested members) with the fact table for NON EMPTY
318     * optimization.
319     */
320    protected boolean isJoinRequired() {
321        Member[] members = evaluator.getMembers();
322        // members[0] is the Measure, so loop starts at 1
323        for (int i = 1; i < members.length; i++) {
324            if (!members[i].isAll()
325                || members[i] instanceof LimitedRollupMember
326                || members[i] instanceof MultiCardinalityDefaultMember)
327            {
328                return true;
329            }
330        }
331        return false;
332    }
333
334    public void addLevelConstraint(
335        SqlQuery sqlQuery,
336        RolapCube baseCube,
337        AggStar aggStar,
338        RolapLevel level)
339    {
340        if (!isJoinRequired()) {
341            return;
342        }
343        SqlConstraintUtils.joinLevelTableToFactTable(
344            sqlQuery, baseCube, aggStar, evaluator, (RolapCubeLevel)level);
345    }
346
347    public MemberChildrenConstraint getMemberChildrenConstraint(
348        RolapMember parent)
349    {
350        return this;
351    }
352
353    public Object getCacheKey() {
354        return cacheKey;
355    }
356
357    public Evaluator getEvaluator() {
358        return evaluator;
359    }
360}
361
362// End SqlContextConstraint.java
363