001    /*
002    // This software is subject to the terms of the Common Public License
003    // Agreement, available at the following URL:
004    // http://www.opensource.org/licenses/cpl.html.
005    // Copyright (C) 2004-2005 TONBELLER AG
006    // Copyright (C) 2005-2008 Julian Hyde and others
007    // All Rights Reserved.
008    // You must accept the terms of that agreement to use this software.
009    */
010    package mondrian.rolap;
011    
012    import mondrian.olap.*;
013    import mondrian.olap.fun.FunUtil;
014    import mondrian.resource.MondrianResource;
015    import mondrian.rolap.RolapCube.CubeComparator;
016    import mondrian.rolap.sql.MemberChildrenConstraint;
017    import mondrian.rolap.sql.SqlQuery;
018    import mondrian.rolap.sql.TupleConstraint;
019    
020    import javax.sql.DataSource;
021    import java.sql.ResultSet;
022    import java.sql.SQLException;
023    import java.util.*;
024    
025    /**
026     * Reads the members of a single level (level.members) or of multiple levels
027     * (crossjoin).
028     *
029     * <p>Allows the result to be restricted by a {@link TupleConstraint}. So
030     * the SqlTupleReader can also read Member.Descendants (which is level.members
031     * restricted to a common parent) and member.children (which is a special case
032     * of member.descendants). Other constraints, especially for the current slicer
033     * or evaluation context, are possible.
034     *
035     * <h3>Caching</h3>
036     *
037     * <p>When a SqlTupleReader reads level.members, it groups the result into
038     * parent/children pairs and puts them into the cache. In order that these can
039     * be found later when the children of a parent are requested, a matching
040     * constraint must be provided for every parent.
041     *
042     * <ul>
043     *
044     * <li>When reading members from a single level, then the constraint is not
045     * required to join the fact table in
046     * {@link TupleConstraint#addLevelConstraint} although it may do so to restrict
047     * the result. Also it is permitted to cache the parent/children from all
048     * members in MemberCache, so
049     * {@link TupleConstraint#getMemberChildrenConstraint(RolapMember)}
050     * should not return null.</li>
051     *
052     * <li>When reading multiple levels (i.e. we are performing a crossjoin),
053     * then we can not store the parent/child pairs in the MemberCache and
054     * {@link TupleConstraint#getMemberChildrenConstraint(RolapMember)}
055     * must return null. Also
056     * {@link TupleConstraint#addConstraint(mondrian.rolap.sql.SqlQuery, mondrian.rolap.RolapCube)}
057     * is required to join the fact table for the levels table.</li>
058     * </ul>
059     *
060     * @author av
061     * @since Nov 11, 2005
062     * @version $Id: //open/mondrian/src/main/mondrian/rolap/SqlTupleReader.java#49 $
063     */
064    public class SqlTupleReader implements TupleReader {
065        protected final TupleConstraint constraint;
066        List<Target> targets = new ArrayList<Target>();
067        int maxRows = 0;
068    
069        /**
070         * TODO: Document this class.
071         */
072        private class Target {
073            final RolapLevel level;
074            final MemberCache cache;
075            final Object cacheLock;
076    
077            RolapLevel[] levels;
078            List<RolapMember> list;
079            int levelDepth;
080            boolean parentChild;
081            List<RolapMember> members;
082            List<List<RolapMember>> siblings;
083            final MemberBuilder memberBuilder;
084            // if set, the rows for this target come from the array rather
085            // than native sql
086            private final List<RolapMember> srcMembers;
087            // current member within the current result set row
088            // for this target
089            private RolapMember currMember;
090    
091            public Target(
092                RolapLevel level, MemberBuilder memberBuilder,
093                List<RolapMember> srcMembers) {
094                this.level = level;
095                this.cache = memberBuilder.getMemberCache();
096                this.cacheLock = memberBuilder.getMemberCacheLock();
097                this.memberBuilder = memberBuilder;
098                this.srcMembers = srcMembers;
099            }
100    
101            public void open() {
102                levels = (RolapLevel[]) level.getHierarchy().getLevels();
103                list = new ArrayList<RolapMember>();
104                levelDepth = level.getDepth();
105                parentChild = level.isParentChild();
106                // members[i] is the current member of level#i, and siblings[i]
107                // is the current member of level#i plus its siblings
108                members = new ArrayList<RolapMember>();
109                for (int i = 0; i < levels.length; i++) {
110                    members.add(null);
111                }
112                siblings = new ArrayList<List<RolapMember>>();
113                for (int i = 0; i < levels.length + 1; i++) {
114                    siblings.add(new ArrayList<RolapMember>());
115                }
116            }
117    
118            /**
119             * Scans a row of the resultset and creates a member
120             * for the result.
121             *
122             * @param resultSet result set to retrieve rows from
123             * @param column the column index to start with
124             *
125             * @return index of the last column read + 1
126             * @throws SQLException
127             */
128            public int addRow(ResultSet resultSet, int column) throws SQLException {
129                synchronized (cacheLock) {
130                    return internalAddRow(resultSet, column);
131                }
132            }
133    
134            private int internalAddRow(ResultSet resultSet, int column) throws SQLException {
135                RolapMember member = null;
136                if (currMember != null) {
137                    member = currMember;
138                } else {
139                    boolean checkCacheStatus = true;
140                    for (int i = 0; i <= levelDepth; i++) {
141                        RolapLevel childLevel = levels[i];
142                        if (childLevel.isAll()) {
143                            member = level.getHierarchy().getAllMember();
144                            continue;
145                        }
146                        Object value = resultSet.getObject(++column);
147                        if (value == null) {
148                            value = RolapUtil.sqlNullValue;
149                        }
150                        Object captionValue;
151                        if (childLevel.hasCaptionColumn()) {
152                            captionValue = resultSet.getObject(++column);
153                        } else {
154                            captionValue = null;
155                        }
156                        RolapMember parentMember = member;
157                        Object key = cache.makeKey(parentMember, value);
158                        member = cache.getMember(key, checkCacheStatus);
159                        checkCacheStatus = false; /* Only check the first time */
160                        if (member == null) {
161                            member = memberBuilder.makeMember(
162                                parentMember, childLevel, value, captionValue,
163                                parentChild, resultSet, key, column);
164                        }
165    
166                        // Skip over the columns consumed by makeMember
167                        if (!childLevel.getOrdinalExp().equals(
168                            childLevel.getKeyExp()))
169                        {
170                            ++column;
171                        }
172                        column += childLevel.getProperties().length;
173    
174                        if (member != members.get(i)) {
175                            // Flush list we've been building.
176                            List<RolapMember> children = siblings.get(i + 1);
177                            if (children != null) {
178                                MemberChildrenConstraint mcc =
179                                    constraint.getMemberChildrenConstraint(
180                                        members.get(i));
181                                if (mcc != null) {
182                                    cache.putChildren(members.get(i), mcc, children);
183                                }
184                            }
185                            // Start a new list, if the cache needs one. (We don't
186                            // synchronize, so it's possible that the cache will
187                            // have one by the time we complete it.)
188                            MemberChildrenConstraint mcc =
189                                constraint.getMemberChildrenConstraint(member);
190                            // we keep a reference to cachedChildren so they don't
191                            // get garbage-collected
192                            List cachedChildren =
193                                cache.getChildrenFromCache(member, mcc);
194                            if (i < levelDepth && cachedChildren == null) {
195                                siblings.set(i + 1, new ArrayList<RolapMember>());
196                            } else {
197                                // don't bother building up a list
198                                siblings.set(i + 1,  null);
199                            }
200                            // Record new current member of this level.
201                            members.set(i, member);
202                            // If we're building a list of siblings at this level,
203                            // we haven't seen this one before, so add it.
204                            if (siblings.get(i) != null) {
205                                if (value == RolapUtil.sqlNullValue) {
206                                    addAsOldestSibling(siblings.get(i), member);
207                                } else {
208                                    ((List)siblings.get(i)).add(member);
209                                }
210                            }
211                        }
212                    }
213                    currMember = member;
214                }
215                ((List)list).add(member);
216                return column;
217            }
218    
219            public List<RolapMember> close() {
220                synchronized (cacheLock) {
221                    return internalClose();
222                }
223            }
224    
225            /**
226             * Cleans up after all rows have been processed, and returns the list of
227             * members.
228             *
229             * @return list of members
230             */
231            public List<RolapMember> internalClose() {
232                for (int i = 0; i < members.size(); i++) {
233                    RolapMember member = members.get(i);
234                    final List<RolapMember> children = siblings.get(i + 1);
235                    if (member != null && children != null) {
236                        // If we are finding the members of a particular level, and
237                        // we happen to find some of the children of an ancestor of
238                        // that level, we can't be sure that we have found all of
239                        // the children, so don't put them in the cache.
240                        if (member.getDepth() < level.getDepth()) {
241                            continue;
242                        }
243                        MemberChildrenConstraint mcc =
244                            constraint.getMemberChildrenConstraint(member);
245                        if (mcc != null) {
246                            cache.putChildren(member, mcc, children);
247                        }
248                    }
249                }
250                return list;
251            }
252    
253            /**
254             * Adds <code>member</code> just before the first element in
255             * <code>list</code> which has the same parent.
256             */
257            private void addAsOldestSibling(List<RolapMember> list, RolapMember member) {
258                int i = list.size();
259                while (--i >= 0) {
260                    RolapMember sibling = list.get(i);
261                    if (sibling.getParentMember() != member.getParentMember()) {
262                        break;
263                    }
264                }
265                list.add(i + 1, member);
266            }
267    
268            public RolapLevel getLevel() {
269                return level;
270            }
271    
272            public String toString() {
273                return level.getUniqueName();
274            }
275    
276        }
277    
278        public SqlTupleReader(TupleConstraint constraint) {
279            this.constraint = constraint;
280        }
281    
282        public void addLevelMembers(
283            RolapLevel level,
284            MemberBuilder memberBuilder,
285            List<RolapMember> srcMembers)
286        {
287            targets.add(new Target(level, memberBuilder, srcMembers));
288        }
289    
290        public Object getCacheKey() {
291            List<Object> key = new ArrayList<Object>();
292            key.add(constraint.getCacheKey());
293            key.add(SqlTupleReader.class);
294            for (Target target : targets) {
295                // don't include the level in the key if the target isn't
296                // processed through native sql
297                if (target.srcMembers != null) {
298                    key.add(target.getLevel());
299                }
300            }
301            return key;
302        }
303    
304        /**
305         * @return number of targets that contain enumerated sets with calculated
306         * members
307         */
308        public int getEnumTargetCount()
309        {
310            int enumTargetCount = 0;
311            for (Target target : targets) {
312                if (target.srcMembers != null) {
313                    enumTargetCount++;
314                }
315            }
316            return enumTargetCount;
317        }
318    
319        protected void prepareTuples(
320            DataSource dataSource,
321            List<List<RolapMember>> partialResult,
322            List<List<RolapMember>> newPartialResult)
323        {
324            String message = "Populating member cache with members for " + targets;
325            SqlStatement stmt = null;
326            final ResultSet resultSet;
327            boolean execQuery = (partialResult == null);
328            try {
329                if (execQuery) {
330                    // we're only reading tuples from the targets that are
331                    // non-enum targets
332                    List<Target> partialTargets = new ArrayList<Target>();
333                    for (Target target : targets) {
334                        if (target.srcMembers == null) {
335                            partialTargets.add(target);
336                        }
337                    }
338                    String sql = makeLevelMembersSql(dataSource);
339                    assert sql != null && !sql.equals("");
340                    stmt = RolapUtil.executeQuery(
341                        dataSource, sql, maxRows,
342                        "SqlTupleReader.readTuples " + partialTargets,
343                        message,
344                        -1, -1);
345                    resultSet = stmt.getResultSet();
346                } else {
347                    resultSet = null;
348                }
349    
350                for (Target target : targets) {
351                    target.open();
352                }
353    
354                int limit = MondrianProperties.instance().ResultLimit.get();
355                int fetchCount = 0;
356    
357                // determine how many enum targets we have
358                int enumTargetCount = getEnumTargetCount();
359                int[] srcMemberIdxes = null;
360                if (enumTargetCount > 0) {
361                    srcMemberIdxes = new int[enumTargetCount];
362                }
363    
364                boolean moreRows;
365                int currPartialResultIdx = 0;
366                if (execQuery) {
367                    moreRows = resultSet.next();
368                    if (moreRows) {
369                        ++stmt.rowCount;
370                    }
371                } else {
372                    moreRows = currPartialResultIdx < partialResult.size();
373                }
374                while (moreRows) {
375                    if (limit > 0 && limit < ++fetchCount) {
376                        // result limit exceeded, throw an exception
377                        throw MondrianResource.instance().MemberFetchLimitExceeded
378                                .ex((long) limit);
379                    }
380    
381                    if (enumTargetCount == 0) {
382                        int column = 0;
383                        for (Target target : targets) {
384                            target.currMember = null;
385                            column = target.addRow(resultSet, column);
386                        }
387                    } else {
388                        // find the first enum target, then call addTargets()
389                        // to form the cross product of the row from resultSet
390                        // with each of the list of members corresponding to
391                        // the enumerated targets
392                        int firstEnumTarget = 0;
393                        for (; firstEnumTarget < targets.size();
394                            firstEnumTarget++)
395                        {
396                            if (targets.get(firstEnumTarget).srcMembers != null) {
397                                break;
398                            }
399                        }
400                        List<RolapMember> partialRow;
401                        if (execQuery) {
402                            partialRow = null;
403                        } else {
404                            partialRow = partialResult.get(currPartialResultIdx);
405                        }
406                        resetCurrMembers(partialRow);
407                        addTargets(
408                            0, firstEnumTarget, enumTargetCount, srcMemberIdxes,
409                            resultSet, message);
410                        if (newPartialResult != null) {
411                            savePartialResult(newPartialResult);
412                        }
413                    }
414    
415                    if (execQuery) {
416                        moreRows = resultSet.next();
417                        if (moreRows) {
418                            ++stmt.rowCount;
419                        }
420                    } else {
421                        currPartialResultIdx++;
422                        moreRows = currPartialResultIdx < partialResult.size();
423                    }
424                }
425            } catch (SQLException e) {
426                if (stmt == null) {
427                    throw Util.newError(e, message);
428                } else {
429                    stmt.handle(e);
430                }
431            } finally {
432                if (stmt != null) {
433                    stmt.close();
434                }
435            }
436        }
437    
438        public List<RolapMember> readMembers(
439            DataSource dataSource,
440            List<List<RolapMember>> partialResult,
441            List<List<RolapMember>> newPartialResult)
442        {
443            prepareTuples(dataSource, partialResult, newPartialResult);
444            assert targets.size() == 1;
445            return targets.get(0).close();
446        }
447    
448        public List<RolapMember[]> readTuples(
449            DataSource jdbcConnection,
450            List<List<RolapMember>> partialResult,
451            List<List<RolapMember>> newPartialResult)
452        {
453            prepareTuples(jdbcConnection, partialResult, newPartialResult);
454    
455            // List of tuples
456            int n = targets.size();
457            List<RolapMember[]> tupleList = new ArrayList<RolapMember[]>();
458            Iterator<RolapMember>[] iter = new Iterator[n];
459            for (int i = 0; i < n; i++) {
460                Target t = targets.get(i);
461                iter[i] = t.close().iterator();
462            }
463            while (iter[0].hasNext()) {
464                RolapMember[] tuples = new RolapMember[n];
465                for (int i = 0; i < n; i++) {
466                    tuples[i] = iter[i].next();
467                }
468                tupleList.add(tuples);
469            }
470    
471            // need to hierarchize the columns from the enumerated targets
472            // since we didn't necessarily add them in the order in which
473            // they originally appeared in the cross product
474            int enumTargetCount = getEnumTargetCount();
475            if (enumTargetCount > 0) {
476                FunUtil.hierarchize(tupleList, false);
477            }
478            return tupleList;
479        }
480    
481        /**
482         * Sets the current member for those targets that retrieve their column
483         * values from native sql
484         *
485         * @param partialRow if set, previously cached result set
486         */
487        private void resetCurrMembers(List<RolapMember> partialRow) {
488            int nativeTarget = 0;
489            for (Target target : targets) {
490                if (target.srcMembers == null) {
491                    // if we have a previously cached row, use that by picking
492                    // out the column corresponding to this target; otherwise,
493                    // we need to retrieve a new column value from the current
494                    // result set
495                    if (partialRow != null) {
496                        target.currMember = partialRow.get(nativeTarget++);
497                    } else {
498                        target.currMember = null;
499                    }
500                }
501            }
502        }
503    
504        /**
505         * Recursively forms the cross product of a row retrieved through sql
506         * with each of the targets that contains an enumerated set of members.
507         *
508         * @param currEnumTargetIdx current enum target that recursion
509         * is being applied on
510         * @param currTargetIdx index within the list of a targets that
511         * currEnumTargetIdx corresponds to
512         * @param nEnumTargets number of targets that have enumerated members
513         * @param srcMemberIdxes for each enumerated target, the current member
514         * to be retrieved to form the current cross product row
515         * @param resultSet result set corresponding to rows retrieved through
516         * native sql
517         * @param message Message to issue on failure
518         */
519        private void addTargets(
520            int currEnumTargetIdx,
521            int currTargetIdx,
522            int nEnumTargets,
523            int[] srcMemberIdxes,
524            ResultSet resultSet,
525            String message)
526        {
527    
528            // loop through the list of members for the current enum target
529            Target currTarget = targets.get(currTargetIdx);
530            for (int i = 0; i < currTarget.srcMembers.size(); i++) {
531                srcMemberIdxes[currEnumTargetIdx] = i;
532                // if we're not on the last enum target, recursively move
533                // to the next one
534                if (currEnumTargetIdx < nEnumTargets - 1) {
535                    int nextTargetIdx = currTargetIdx + 1;
536                    for (; nextTargetIdx < targets.size(); nextTargetIdx++) {
537                        if (targets.get(nextTargetIdx).srcMembers != null) {
538                            break;
539                        }
540                    }
541                    addTargets(
542                        currEnumTargetIdx + 1, nextTargetIdx, nEnumTargets,
543                        srcMemberIdxes, resultSet, message);
544                } else {
545                    // form a cross product using the columns from the current
546                    // result set row and the current members that recursion
547                    // has reached for the enum targets
548                    int column = 0;
549                    int enumTargetIdx = 0;
550                    for (Target target : targets) {
551                        if (target.srcMembers == null) {
552                            try {
553                                column = target.addRow(resultSet, column);
554                            } catch (Throwable e) {
555                                throw Util.newError(e, message);
556                            }
557                        } else {
558                            RolapMember member =
559                                target.srcMembers.get(srcMemberIdxes[enumTargetIdx++]);
560                            target.list.add(member);
561                        }
562                    }
563                }
564            }
565        }
566    
567        /**
568         * Retrieves the current members fetched from the targets executed
569         * through sql and form tuples, adding them to partialResult
570         *
571         * @param partialResult list containing the columns and rows corresponding
572         * to data fetched through sql
573         */
574        private void savePartialResult(List<List<RolapMember>> partialResult) {
575            List<RolapMember> row = new ArrayList<RolapMember>();
576            for (Target target : targets) {
577                if (target.srcMembers == null) {
578                    row.add(target.currMember);
579                }
580            }
581            partialResult.add(row);
582        }
583    
584        private String makeLevelMembersSql(DataSource dataSource) {
585    
586            // In the case of a virtual cube, if we need to join to the fact
587            // table, we do not necessarily have a single underlying fact table,
588            // as the underlying base cubes in the virtual cube may all reference
589            // different fact tables.
590            //
591            // Therefore, we need to gather the underlying fact tables by going
592            // through the list of measures referenced in the query.  And then
593            // we generate one sub-select per fact table, joining against each
594            // underlying fact table, unioning the sub-selects.
595            RolapCube cube = null;
596            boolean virtualCube = false;
597            if (constraint instanceof SqlContextConstraint) {
598                SqlContextConstraint sqlConstraint =
599                    (SqlContextConstraint) constraint;
600                if (sqlConstraint.isJoinRequired()) {
601                    Query query = constraint.getEvaluator().getQuery();
602                    cube = (RolapCube) query.getCube();
603                    virtualCube = cube.isVirtual();
604                }
605            }
606    
607            if (virtualCube) {
608                String selectString = "";
609                Query query = constraint.getEvaluator().getQuery();
610    
611                // Make fact table appear in fixed sequence
612                RolapCube.CubeComparator cubeComparator = new RolapCube.CubeComparator();
613                TreeSet<RolapCube> baseCubes = new TreeSet<RolapCube>(cubeComparator);
614                baseCubes.addAll(query.getBaseCubes());
615    
616                // generate sub-selects, each one joining with one of
617                // the fact table referenced
618                int k = -1;
619                // Save the original measure in the context
620                Member originalMeasure = constraint.getEvaluator().getMembers()[0];
621                for (RolapCube baseCube : baseCubes) {
622                    // Use the measure from the corresponding base cube in the
623                    // context to find the correct join path to the base fact
624                    // table.
625                    //
626                    // Any measure is fine since the constraint logic only uses it
627                    // to find the correct fact table to join to.
628                    Member measureInCurrentbaseCube = baseCube.getMeasures().get(0);
629                    constraint.getEvaluator().setContext(measureInCurrentbaseCube);
630    
631                    boolean finalSelect = (++k == baseCubes.size() - 1);
632                    WhichSelect whichSelect =
633                        finalSelect ? WhichSelect.LAST : WhichSelect.NOT_LAST;
634                    selectString +=
635                        generateSelectForLevels(dataSource, baseCube, whichSelect);
636                    if (!finalSelect) {
637                        selectString += " union ";
638                    }
639                }
640                // Restore the original measure member
641                constraint.getEvaluator().setContext(originalMeasure);
642                return selectString;
643            } else {
644                return generateSelectForLevels(dataSource, cube, WhichSelect.ONLY);
645            }
646        }
647    
648        /**
649         * Generates the SQL string corresponding to the levels referenced.
650         *
651         * @param dataSource jdbc connection that they query will execute against
652         * @param baseCube this is the cube object for regular cubes, and the
653         *   underlying base cube for virtual cubes
654         * @param whichSelect Position of this select statement in a union
655         * @return SQL statement string
656         */
657        private String generateSelectForLevels(
658            DataSource dataSource,
659            RolapCube baseCube,
660            WhichSelect whichSelect) {
661    
662            String s = "while generating query to retrieve members of level(s) " + targets;
663            SqlQuery sqlQuery = SqlQuery.newQuery(dataSource, s);
664    
665            // add the selects for all levels to fetch
666            for (Target target : targets) {
667                // if we're going to be enumerating the values for this target,
668                // then we don't need to generate sql for it
669                if (target.srcMembers == null) {
670                    addLevelMemberSql(
671                        sqlQuery,
672                        target.getLevel(),
673                        baseCube,
674                        whichSelect);
675                }
676            }
677    
678            constraint.addConstraint(sqlQuery, baseCube);
679    
680            return sqlQuery.toString();
681        }
682    
683        /**
684         * Generates the SQL statement to access members of <code>level</code>. For
685         * example, <blockquote>
686         * <pre>SELECT "country", "state_province", "city"
687         * FROM "customer"
688         * GROUP BY "country", "state_province", "city", "init", "bar"
689         * ORDER BY "country", "state_province", "city"</pre>
690         * </blockquote> accesses the "City" level of the "Customers"
691         * hierarchy. Note that:<ul>
692         *
693         * <li><code>"country", "state_province"</code> are the parent keys;</li>
694         *
695         * <li><code>"city"</code> is the level key;</li>
696         *
697         * <li><code>"init", "bar"</code> are member properties.</li>
698         * </ul>
699         *
700         * @param sqlQuery the query object being constructed
701         * @param level level to be added to the sql query
702         * @param baseCube this is the cube object for regular cubes, and the
703         *   underlying base cube for virtual cubes
704         * @param whichSelect describes whether this select belongs to a larger
705         * select containing unions or this is a non-union select
706         */
707        private void addLevelMemberSql(
708            SqlQuery sqlQuery,
709            RolapLevel level,
710            RolapCube baseCube,
711            WhichSelect whichSelect)
712        {
713            RolapHierarchy hierarchy = level.getHierarchy();
714    
715            // lookup RolapHierarchy of base cube that matches this hierarchy
716    
717            if (hierarchy instanceof RolapCubeHierarchy) {
718                RolapCubeHierarchy cubeHierarchy = (RolapCubeHierarchy)hierarchy;
719                if (baseCube != null && !cubeHierarchy.getDimension().getCube().equals(baseCube)) {
720                    // replace the hierarchy with the underlying base cube hierarchy
721                    // in the case of virtual cubes
722                    hierarchy = baseCube.findBaseCubeHierarchy(hierarchy);
723                }
724            }
725    
726            RolapLevel[] levels = (RolapLevel[]) hierarchy.getLevels();
727            int levelDepth = level.getDepth();
728            for (int i = 0; i <= levelDepth; i++) {
729                RolapLevel currLevel = levels[i];
730                if (currLevel.isAll()) {
731                    continue;
732                }
733    
734                MondrianDef.Expression keyExp = currLevel.getKeyExp();
735                MondrianDef.Expression ordinalExp = currLevel.getOrdinalExp();
736                MondrianDef.Expression captionExp = currLevel.getCaptionExp();
737    
738                String keySql = keyExp.getExpression(sqlQuery);
739                String ordinalSql = ordinalExp.getExpression(sqlQuery);
740    
741                hierarchy.addToFrom(sqlQuery, keyExp);
742                hierarchy.addToFrom(sqlQuery, ordinalExp);
743    
744                String captionSql = null;
745                if (captionExp != null) {
746                    captionSql = captionExp.getExpression(sqlQuery);
747                    hierarchy.addToFrom(sqlQuery, captionExp);
748                }
749    
750                sqlQuery.addSelect(keySql);
751                sqlQuery.addGroupBy(keySql);
752    
753                if (!ordinalSql.equals(keySql)) {
754                    sqlQuery.addSelect(ordinalSql);
755                    sqlQuery.addGroupBy(ordinalSql);
756                }
757    
758                if (captionSql != null) {
759                    sqlQuery.addSelect(captionSql);
760                    sqlQuery.addGroupBy(captionSql);
761                }
762    
763                constraint.addLevelConstraint(sqlQuery, baseCube, null, currLevel);
764    
765                // If this is a select on a virtual cube, the query will be
766                // a union, so the order by columns need to be numbers,
767                // not column name strings or expressions.
768                switch (whichSelect) {
769                case LAST:
770                    sqlQuery.addOrderBy(
771                        Integer.toString(
772                            sqlQuery.getCurrentSelectListSize()),
773                            true, false, true);
774                    break;
775                case ONLY:
776                    sqlQuery.addOrderBy(ordinalSql, true, false, true);
777                    break;
778                }
779    
780                RolapProperty[] properties = currLevel.getProperties();
781                for (RolapProperty property : properties) {
782                    String propSql = property.getExp().getExpression(sqlQuery);
783                    sqlQuery.addSelect(propSql);
784                    sqlQuery.addGroupBy(propSql);
785                }
786            }
787        }
788    
789        int getMaxRows() {
790            return maxRows;
791        }
792    
793        void setMaxRows(int maxRows) {
794            this.maxRows = maxRows;
795        }
796    
797        /**
798         * Description of the position of a SELECT statement in a UNION. Queries
799         * on virtual cubes tend to generate unions.
800         */
801        enum WhichSelect {
802            /**
803             * Select statement does not belong to a union.
804             */
805            ONLY,
806            /**
807             * Select statement belongs to a UNION, but is not the last. Typically
808             * this occurs when querying a virtual cube.
809             */
810            NOT_LAST,
811            /**
812             * Select statement is the last in a UNION. Typically
813             * this occurs when querying a virtual cube.
814             */
815            LAST
816        }
817    }
818    
819    // End SqlTupleReader.java