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