001 /* 002 // $Id: //open/mondrian/src/main/mondrian/rolap/RolapStar.java#98 $ 003 // This software is subject to the terms of the Common Public License 004 // Agreement, available at the following URL: 005 // http://www.opensource.org/licenses/cpl.html. 006 // Copyright (C) 2001-2002 Kana Software, Inc. 007 // Copyright (C) 2001-2008 Julian Hyde and others 008 // All Rights Reserved. 009 // You must accept the terms of that agreement to use this software. 010 // 011 // jhyde, 12 August, 2001 012 */ 013 014 package mondrian.rolap; 015 016 import mondrian.olap.*; 017 import mondrian.rolap.agg.Aggregation; 018 import mondrian.rolap.agg.AggregationKey; 019 import mondrian.rolap.aggmatcher.AggStar; 020 import mondrian.rolap.sql.SqlQuery; 021 import mondrian.spi.DataSourceChangeListener; 022 import mondrian.util.Bug; 023 import org.apache.log4j.Logger; 024 import org.eigenbase.util.property.Property; 025 import org.eigenbase.util.property.TriggerBase; 026 027 import javax.sql.DataSource; 028 import java.io.PrintWriter; 029 import java.io.StringWriter; 030 import java.sql.Connection; 031 import java.sql.*; 032 import java.util.*; 033 034 /** 035 * A <code>RolapStar</code> is a star schema. It is the means to read cell 036 * values. 037 * 038 * <p>todo: put this in package which specicializes in relational aggregation, 039 * doesn't know anything about hierarchies etc. 040 * 041 * @author jhyde 042 * @since 12 August, 2001 043 * @version $Id: //open/mondrian/src/main/mondrian/rolap/RolapStar.java#98 $ 044 */ 045 public class RolapStar { 046 private static final Logger LOGGER = Logger.getLogger(RolapStar.class); 047 048 /** 049 * Controls the aggregate data cache for all RolapStars. 050 * An administrator or tester might selectively enable or 051 * disable in memory caching to allow direct measurement of database 052 * performance. 053 */ 054 private static boolean disableCaching = 055 MondrianProperties.instance().DisableCaching.get(); 056 057 static { 058 // Trigger is used to lookup and change the value of the 059 // variable that controls aggregate data caching 060 // Using a trigger means we don't have to look up the property eveytime. 061 MondrianProperties.instance().DisableCaching.addTrigger( 062 new TriggerBase(true) { 063 public void execute(Property property, String value) { 064 disableCaching = property.booleanValue(); 065 // must flush all caches 066 if (disableCaching) { 067 // REVIEW: could replace following code with call to 068 // CacheControl.flush(CellRegion) 069 for (Iterator<RolapSchema> itSchemas = 070 RolapSchema.getRolapSchemas(); 071 itSchemas.hasNext();) 072 { 073 RolapSchema schema1 = itSchemas.next(); 074 for (RolapStar star : schema1.getStars()) { 075 star.clearCachedAggregations(true); 076 } 077 } 078 } 079 } 080 } 081 ); 082 } 083 084 085 private final RolapSchema schema; 086 087 // not final for test purposes 088 private DataSource dataSource; 089 090 private final Table factTable; 091 092 /** Holds all global aggregations of this star. */ 093 private final Map<AggregationKey,Aggregation> sharedAggregations; 094 095 /** Holds all thread-local aggregations of this star. */ 096 private final ThreadLocal<Map<AggregationKey, Aggregation>> 097 localAggregations = 098 new ThreadLocal<Map<AggregationKey, Aggregation>>() { 099 protected Map<AggregationKey, Aggregation> initialValue() { 100 return new HashMap<AggregationKey, Aggregation>(); 101 } 102 }; 103 104 /** 105 * Holds all pending aggregations of this star that are waiting to 106 * be pushed into the global cache. They cannot be pushed yet, because 107 * the aggregates in question are currently in use by other threads. 108 */ 109 private final Map<AggregationKey, Aggregation> pendingAggregations; 110 111 /** 112 * Holds all requests for aggregations. 113 */ 114 private final List<AggregationKey> aggregationRequests; 115 116 /** 117 * Holds all requests of aggregations per thread. 118 */ 119 private final ThreadLocal<List<AggregationKey>> 120 localAggregationRequests = 121 new ThreadLocal<List<AggregationKey>>() { 122 protected List<AggregationKey> initialValue() { 123 return new ArrayList<AggregationKey>(); 124 } 125 }; 126 127 /** 128 * Number of columns (column and columnName). 129 */ 130 private int columnCount; 131 132 private final SqlQuery.Dialect sqlQueryDialect; 133 134 /** 135 * If true, then database aggregation information is cached, otherwise 136 * it is flushed after each query. 137 */ 138 private boolean cacheAggregations; 139 140 /** 141 * Partially ordered list of AggStars associated with this RolapStar's fact 142 * table 143 */ 144 private List<AggStar> aggStars; 145 146 private DataSourceChangeListener changeListener; 147 148 // temporary model, should eventually use RolapStar.Table and RolapStar.Column 149 private StarNetworkNode factNode; 150 private Map<String, StarNetworkNode> nodeLookup = 151 new HashMap<String, StarNetworkNode>(); 152 153 /** 154 * Creates a RolapStar. Please use 155 * {@link RolapSchema.RolapStarRegistry#getOrCreateStar} to create a 156 * {@link RolapStar}. 157 */ 158 RolapStar( 159 final RolapSchema schema, 160 final DataSource dataSource, 161 final MondrianDef.Relation fact) 162 { 163 this.cacheAggregations = true; 164 this.schema = schema; 165 this.dataSource = dataSource; 166 this.factTable = new RolapStar.Table(this, fact, null, null); 167 168 // phase out and replace with Table, Column network 169 this.factNode = new StarNetworkNode(null, factTable.alias, null, null, null); 170 171 this.sharedAggregations = new HashMap<AggregationKey, Aggregation>(); 172 173 this.pendingAggregations = new HashMap<AggregationKey, Aggregation>(); 174 175 this.aggregationRequests = new ArrayList<AggregationKey>(); 176 177 clearAggStarList(); 178 179 this.sqlQueryDialect = schema.getDialect(); 180 181 this.changeListener = schema.getDataSourceChangeListener(); 182 } 183 184 private static class StarNetworkNode { 185 private StarNetworkNode parent; 186 private MondrianDef.Relation origRel; 187 private String foreignKey; 188 private String joinKey; 189 190 private StarNetworkNode( 191 StarNetworkNode parent, 192 String alias, 193 MondrianDef.Relation origRel, 194 String foreignKey, 195 String joinKey) 196 { 197 this.parent = parent; 198 this.origRel = origRel; 199 this.foreignKey = foreignKey; 200 this.joinKey = joinKey; 201 } 202 203 private boolean isCompatible( 204 StarNetworkNode compatibleParent, 205 MondrianDef.Relation rel, 206 String compatibleForeignKey, 207 String compatibleJoinKey) 208 { 209 return (parent == compatibleParent && 210 origRel.getClass().equals(rel.getClass()) && 211 foreignKey.equals(compatibleForeignKey) && 212 joinKey.equals(compatibleJoinKey)); 213 } 214 } 215 216 private MondrianDef.RelationOrJoin cloneRelation( 217 MondrianDef.Relation rel, 218 String possibleName) 219 { 220 if (rel instanceof MondrianDef.Table) { 221 MondrianDef.Table tbl = (MondrianDef.Table)rel; 222 return new MondrianDef.Table(tbl.schema, tbl.name, possibleName); 223 } else if (rel instanceof MondrianDef.View) { 224 MondrianDef.View view = (MondrianDef.View)rel; 225 MondrianDef.View newView = new MondrianDef.View(view); 226 newView.alias = possibleName; 227 return newView; 228 } else if (rel instanceof MondrianDef.InlineTable) { 229 MondrianDef.InlineTable inlineTable = 230 (MondrianDef.InlineTable) rel; 231 MondrianDef.InlineTable newInlineTable = 232 new MondrianDef.InlineTable(inlineTable); 233 newInlineTable.alias = possibleName; 234 return newInlineTable; 235 } else { 236 throw new UnsupportedOperationException(); 237 } 238 } 239 240 /** 241 * Generates a unique relational join to the fact table via re-aliasing 242 * MondrianDef.Relations 243 * 244 * currently called in the RolapCubeHierarchy constructor. This should 245 * eventually be phased out and replaced with RolapStar.Table and 246 * RolapStar.Column references 247 * 248 * @param rel the relation needing uniqueness 249 * @param factForeignKey the foreign key of the fact table 250 * @param primaryKey the join key of the relation 251 * @param primaryKeyTable the join table of the relation 252 * @return if necessary a new relation that has been re-aliased 253 */ 254 public MondrianDef.RelationOrJoin getUniqueRelation( 255 MondrianDef.RelationOrJoin rel, 256 String factForeignKey, 257 String primaryKey, 258 String primaryKeyTable) 259 { 260 return getUniqueRelation( 261 factNode, rel, factForeignKey, primaryKey, primaryKeyTable); 262 } 263 264 private MondrianDef.RelationOrJoin getUniqueRelation( 265 StarNetworkNode parent, 266 MondrianDef.RelationOrJoin relOrJoin, 267 String foreignKey, 268 String joinKey, 269 String joinKeyTable) 270 { 271 if (relOrJoin == null) { 272 return null; 273 } else if (relOrJoin instanceof MondrianDef.Relation) { 274 int val = 0; 275 MondrianDef.Relation rel = 276 (MondrianDef.Relation) relOrJoin; 277 String newAlias = 278 joinKeyTable != null ? joinKeyTable : rel.getAlias(); 279 while (true) { 280 StarNetworkNode node = nodeLookup.get(newAlias); 281 if (node == null) { 282 if (val != 0) { 283 rel = (MondrianDef.Relation) 284 cloneRelation(rel, newAlias); 285 } 286 node = 287 new StarNetworkNode( 288 parent, newAlias, rel, foreignKey, joinKey); 289 nodeLookup.put(newAlias, node); 290 return rel; 291 } else if (node.isCompatible( 292 parent, rel, foreignKey, joinKey)) 293 { 294 return node.origRel; 295 } 296 newAlias = rel.getAlias() + "_" + (++val); 297 } 298 } else if (relOrJoin instanceof MondrianDef.Join) { 299 // determine if the join starts from the left or right side 300 MondrianDef.Join join = (MondrianDef.Join)relOrJoin; 301 MondrianDef.RelationOrJoin left = null; 302 MondrianDef.RelationOrJoin right = null; 303 if (join.getLeftAlias().equals(joinKeyTable)) { 304 // first manage left then right 305 left = 306 getUniqueRelation( 307 parent, join.left, foreignKey, 308 joinKey, joinKeyTable); 309 parent = nodeLookup.get( 310 ((MondrianDef.Relation) left).getAlias()); 311 right = 312 getUniqueRelation( 313 parent, join.right, join.leftKey, 314 join.rightKey, join.getRightAlias()); 315 } else if (join.getRightAlias().equals(joinKeyTable)) { 316 // right side must equal 317 right = 318 getUniqueRelation( 319 parent, join.right, foreignKey, 320 joinKey, joinKeyTable); 321 parent = nodeLookup.get( 322 ((MondrianDef.Relation) right).getAlias()); 323 left = 324 getUniqueRelation( 325 parent, join.left, join.rightKey, 326 join.leftKey, join.getLeftAlias()); 327 } else { 328 new MondrianException( 329 "failed to match primary key table to join tables"); 330 } 331 332 if (join.left != left || join.right != right) { 333 join = 334 new MondrianDef.Join( 335 left instanceof MondrianDef.Relation 336 ? ((MondrianDef.Relation) left).getAlias() 337 : null, 338 join.leftKey, 339 left, 340 right instanceof MondrianDef.Relation 341 ? ((MondrianDef.Relation) right).getAlias() 342 : null, 343 join.rightKey, 344 right); 345 } 346 return join; 347 } 348 return null; 349 } 350 351 /** 352 * Returns this RolapStar's column count. After a star has been created with 353 * all of its columns, this is the number of columns in the star. 354 */ 355 public int getColumnCount() { 356 return columnCount; 357 } 358 359 /** 360 * This is used by the {@link Column} constructor to get a unique id (per 361 * its parent {@link RolapStar}). 362 */ 363 private int nextColumnCount() { 364 return columnCount++; 365 } 366 367 /** 368 * This is used to decrement the column counter and is used if a newly 369 * created column is found to already exist. 370 */ 371 private int decrementColumnCount() { 372 return columnCount--; 373 } 374 375 /** 376 * This is a place holder in case in the future we wish to be able to 377 * reload aggregates. In that case, if aggregates had already been loaded, 378 * i.e., this star has some aggstars, then those aggstars are cleared. 379 */ 380 public void prepareToLoadAggregates() { 381 aggStars = Collections.emptyList(); 382 } 383 384 /** 385 * Adds an {@link AggStar} to this star. 386 * 387 * <p>Internally the AggStars are added in sort order, smallest row count 388 * to biggest, so that the most efficient AggStar is encountered first; 389 * ties do not matter. 390 */ 391 public void addAggStar(AggStar aggStar) { 392 if (aggStars == Collections.EMPTY_LIST) { 393 // if this is NOT a LinkedList, then the insertion time is longer. 394 aggStars = new LinkedList<AggStar>(); 395 } 396 397 // Add it before the first AggStar which is larger, if there is one. 398 int size = aggStar.getSize(); 399 ListIterator<AggStar> lit = aggStars.listIterator(); 400 while (lit.hasNext()) { 401 AggStar as = lit.next(); 402 if (as.getSize() >= size) { 403 lit.previous(); 404 lit.add(aggStar); 405 return; 406 } 407 } 408 409 // There is no larger star. Add at the end of the list. 410 aggStars.add(aggStar); 411 } 412 413 /** 414 * Set the agg star list to empty. 415 */ 416 void clearAggStarList() { 417 aggStars = Collections.emptyList(); 418 } 419 420 /** 421 * Reorder the list of aggregate stars. This should be called if the 422 * algorithm used to order the AggStars has been changed. 423 */ 424 public void reOrderAggStarList() { 425 // the order of these two lines is important 426 List<AggStar> l = aggStars; 427 clearAggStarList(); 428 429 for (AggStar aggStar : l) { 430 addAggStar(aggStar); 431 } 432 } 433 434 /** 435 * Returns this RolapStar's aggregate table AggStars, ordered in ascending 436 * order of size. 437 */ 438 public List<AggStar> getAggStars() { 439 return aggStars; 440 } 441 442 /** 443 * Returns the fact table at the center of this RolapStar. 444 * 445 * @return fact table 446 */ 447 public Table getFactTable() { 448 return factTable; 449 } 450 451 /** 452 * Clones an existing SqlQuery to create a new one (this cloning creates one 453 * with an empty sql query). 454 */ 455 public SqlQuery getSqlQuery() { 456 return new SqlQuery(getSqlQueryDialect()); 457 } 458 459 /** 460 * Returns this RolapStar's SQL dialect. 461 */ 462 public SqlQuery.Dialect getSqlQueryDialect() { 463 return sqlQueryDialect; 464 } 465 466 /** 467 * Sets whether to cache database aggregation information; if false, cache 468 * is flushed after each query. 469 * 470 * <p>This method is called only by the RolapCube and is only called if 471 * caching is to be turned off. Note that the same RolapStar can be 472 * associated with more than on RolapCube. If any one of those cubes has 473 * caching turned off, then caching is turned off for all of them. 474 * 475 * @param cacheAggregations Whether to cache database aggregation 476 */ 477 void setCacheAggregations(boolean cacheAggregations) { 478 // this can only change from true to false 479 this.cacheAggregations = cacheAggregations; 480 clearCachedAggregations(false); 481 } 482 483 /** 484 * Returns whether the this RolapStar cache aggregates. 485 * 486 * @see #setCacheAggregations(boolean) 487 */ 488 boolean isCacheAggregations() { 489 return this.cacheAggregations; 490 } 491 492 /** 493 * Clears the aggregate cache. This only does something if aggregate caching 494 * is disabled (see {@link #setCacheAggregations(boolean)}). 495 * 496 * @param forced If true, clears cached aggregations regardless of any other 497 * settings. If false, clears only cache from the current thread 498 */ 499 void clearCachedAggregations(boolean forced) { 500 if (forced || !cacheAggregations || RolapStar.disableCaching) { 501 if (LOGGER.isDebugEnabled()) { 502 StringBuilder buf = new StringBuilder(100); 503 buf.append("RolapStar.clearCachedAggregations: schema="); 504 buf.append(schema.getName()); 505 buf.append(", star="); 506 buf.append(getFactTable().getAlias()); 507 LOGGER.debug(buf.toString()); 508 } 509 510 if (forced) { 511 synchronized (sharedAggregations) { 512 sharedAggregations.clear(); 513 } 514 localAggregations.get().clear(); 515 } else { 516 // Only clear aggregation cache for the currect thread context. 517 localAggregations.get().clear(); 518 } 519 } 520 521 } 522 523 /** 524 * Looks up an aggregation or creates one if it does not exist in an 525 * atomic (synchronized) operation. 526 * 527 * <p>When a new aggregation is created, it is marked as thread local. 528 * 529 * @param aggregationKey this is the contrained column bitkey 530 */ 531 public Aggregation lookupOrCreateAggregation(AggregationKey aggregationKey) { 532 533 Aggregation aggregation = lookupAggregation(aggregationKey); 534 535 if (aggregation == null) { 536 aggregation = new Aggregation(aggregationKey); 537 538 this.localAggregations.get().put(aggregationKey, aggregation); 539 540 // Let the change listener get the opportunity to register the 541 // first time the aggregation is used 542 if ((this.cacheAggregations) && (!RolapStar.disableCaching)) { 543 if (changeListener != null) { 544 Util.discard(changeListener.isAggregationChanged(aggregation)); 545 } 546 } 547 } 548 return aggregation; 549 } 550 551 /** 552 * Looks for an existing aggregation over a given set of columns, or 553 * returns <code>null</code> if there is none. 554 * 555 * <p>Thread local cache is taken first. 556 * 557 * <p>Must be called from synchronized context. 558 */ 559 public Aggregation lookupAggregation(AggregationKey aggregationKey) { 560 // First try thread local cache 561 Aggregation aggregation = localAggregations.get().get(aggregationKey); 562 if (aggregation != null) { 563 return aggregation; 564 } 565 566 if (cacheAggregations && !RolapStar.disableCaching) { 567 // Look in global cache 568 synchronized (sharedAggregations) { 569 aggregation = sharedAggregations.get(aggregationKey); 570 if (aggregation != null) { 571 // Keep track of global aggregates that a query is using 572 recordAggregationRequest(aggregationKey); 573 } 574 } 575 } 576 577 return aggregation; 578 } 579 580 /** 581 * Checks whether an aggregation has changed since the last the time 582 * loaded. 583 * 584 * <p>If so, a new thread local aggregation will be made and added after 585 * the query has finished. 586 * 587 * <p>This method should be called before a query is executed and afterwards 588 * the function {@link #pushAggregateModificationsToGlobalCache()} should 589 * be called. 590 */ 591 public void checkAggregateModifications() { 592 593 // Clear own aggregation requests at the beginning of a query 594 // made by request to materialize results after RolapResult constructor 595 // is finished 596 clearAggregationRequests(); 597 598 if (changeListener != null) { 599 if (cacheAggregations && !RolapStar.disableCaching) { 600 synchronized (sharedAggregations) { 601 for (Map.Entry<AggregationKey, Aggregation> e : 602 sharedAggregations.entrySet()) 603 { 604 AggregationKey aggregationKey = e.getKey(); 605 606 Aggregation aggregation = e.getValue(); 607 if (changeListener.isAggregationChanged(aggregation)) { 608 // Create new thread local aggregation 609 // This thread will renew aggregations 610 // And these will be checked in if all queries 611 // that are currently using these aggregates 612 // are finished 613 aggregation = new Aggregation(aggregationKey); 614 615 localAggregations.get().put(aggregationKey, aggregation); 616 } 617 } 618 } 619 } 620 } 621 } 622 623 /** 624 * Checks whether changed modifications may be pushed into global cache. 625 * 626 * <p>The method checks whether there are other running queries that are 627 * using the requested modifications. If this is the case, modifications 628 * are not pushed yet. 629 */ 630 public void pushAggregateModificationsToGlobalCache() { 631 // Need synchronized access to both aggregationRequests as to 632 // aggregations, synchronize this instead 633 synchronized (this) { 634 if (cacheAggregations && !RolapStar.disableCaching) { 635 636 // Push pending modifications other thread could not push 637 // to global cache, because it was in use 638 Iterator<Map.Entry<AggregationKey, Aggregation>> 639 it = pendingAggregations.entrySet().iterator(); 640 while (it.hasNext()) { 641 Map.Entry<AggregationKey, Aggregation> e = it.next(); 642 AggregationKey aggregationKey = e.getKey(); 643 Aggregation aggregation = e.getValue(); 644 // In case this aggregation is not requested by anyone 645 // this aggregation may be pushed into global cache 646 // otherwise put it in pending cache, that will be pushed 647 // when another query finishes 648 if (!isAggregationRequested(aggregationKey)) { 649 pushAggregateModification( 650 aggregationKey, aggregation,sharedAggregations); 651 it.remove(); 652 } 653 } 654 // Push thread local modifications 655 it = localAggregations.get().entrySet().iterator(); 656 while (it.hasNext()) { 657 Map.Entry<AggregationKey, Aggregation> e = it.next(); 658 AggregationKey aggregationKey = e.getKey(); 659 Aggregation aggregation = e.getValue(); 660 // In case this aggregation is not requested by anyone 661 // this aggregation may be pushed into global cache 662 // otherwise put it in pending cache, that will be pushed 663 // when another query finishes 664 if (!isAggregationRequested(aggregationKey)) { 665 pushAggregateModification( 666 aggregationKey, aggregation, sharedAggregations); 667 } else { 668 pushAggregateModification( 669 aggregationKey, aggregation, pendingAggregations); 670 } 671 } 672 localAggregations.get().clear(); 673 } 674 // Clear own aggregation requests 675 clearAggregationRequests(); 676 } 677 } 678 679 /** 680 * Pushes aggregations in destination aggregations, replacing older 681 * entries. 682 */ 683 private void pushAggregateModification( 684 AggregationKey localAggregationKey, 685 Aggregation localAggregation, 686 Map<AggregationKey,Aggregation> destAggregations) 687 { 688 if (cacheAggregations && !RolapStar.disableCaching) { 689 synchronized (destAggregations) { 690 691 boolean found = false; 692 Iterator<Map.Entry<AggregationKey, Aggregation>> 693 it = destAggregations.entrySet().iterator(); 694 while (it.hasNext()) { 695 Map.Entry<AggregationKey, Aggregation> e = 696 it.next(); 697 AggregationKey aggregationKey = e.getKey(); 698 Aggregation aggregation = e.getValue(); 699 700 if (localAggregationKey.equals(aggregationKey)) { 701 702 if (localAggregation.getCreationTimestamp().after( 703 aggregation.getCreationTimestamp())) { 704 it.remove(); 705 } else { 706 // Entry is newer, do not replace 707 found = true; 708 } 709 break; 710 } 711 } 712 if (!found) { 713 destAggregations.put(localAggregationKey, localAggregation); 714 } 715 } 716 } 717 } 718 719 /** 720 * Records global cache requests per thread. 721 */ 722 private void recordAggregationRequest(AggregationKey aggregationKey) { 723 if (!localAggregationRequests.get().contains(aggregationKey)) { 724 synchronized (aggregationRequests) { 725 aggregationRequests.add(aggregationKey); 726 } 727 // Store own request for cleanup afterwards 728 localAggregationRequests.get().add(aggregationKey); 729 } 730 } 731 732 /** 733 * Checks whether an aggregation is requested by another thread. 734 */ 735 private boolean isAggregationRequested(AggregationKey aggregationKey) { 736 synchronized (aggregationRequests) { 737 return aggregationRequests.contains(aggregationKey); 738 } 739 } 740 741 /** 742 * Clears the aggregation requests created by the current thread. 743 */ 744 private void clearAggregationRequests() { 745 synchronized (aggregationRequests) { 746 if (localAggregationRequests.get().isEmpty()) { 747 return; 748 } 749 // Build a set of requests for efficient probing. Negligible cost 750 // if this thread's localAggregationRequests is small, but avoids a 751 // quadratic algorithm if it is large. 752 Set<AggregationKey> localAggregationRequestSet = 753 new HashSet<AggregationKey>(localAggregationRequests.get()); 754 Iterator<AggregationKey> iter = aggregationRequests.iterator(); 755 while (iter.hasNext()) { 756 AggregationKey aggregationKey = iter.next(); 757 if (localAggregationRequestSet.contains(aggregationKey)) { 758 iter.remove(); 759 // Make sure that bitKey is not removed more than once: 760 // other occurrences might exist for other threads. 761 localAggregationRequestSet.remove(aggregationKey); 762 if (localAggregationRequestSet.isEmpty()) { 763 // Nothing further to do 764 break; 765 } 766 } 767 } 768 localAggregationRequests.get().clear(); 769 } 770 } 771 772 /** For testing purposes only. */ 773 public void setDataSource(DataSource dataSource) { 774 this.dataSource = dataSource; 775 } 776 777 /** 778 * Returns the DataSource used to connect to the underlying DBMS. 779 * 780 * @return DataSource 781 */ 782 public DataSource getDataSource() { 783 return dataSource; 784 } 785 786 /** 787 * Retrieves the {@link RolapStar.Measure} in which a measure is stored. 788 */ 789 public static Measure getStarMeasure(Member member) { 790 return (Measure) ((RolapStoredMeasure) member).getStarMeasure(); 791 } 792 793 /** 794 * Retrieves a named column, returns null if not found. 795 */ 796 public Column[] lookupColumns(String tableAlias, String columnName) { 797 final Table table = factTable.findDescendant(tableAlias); 798 return (table == null) ? null : table.lookupColumns(columnName); 799 } 800 801 /** 802 * This is used by TestAggregationManager only. 803 */ 804 public Column lookupColumn(String tableAlias, String columnName) { 805 final Table table = factTable.findDescendant(tableAlias); 806 return (table == null) ? null : table.lookupColumn(columnName); 807 } 808 809 public BitKey getBitKey(String[] tableAlias, String[] columnName) { 810 BitKey bitKey = BitKey.Factory.makeBitKey(getColumnCount()); 811 Column starColumn; 812 for (int i = 0; i < tableAlias.length; i ++) { 813 starColumn = lookupColumn(tableAlias[i], columnName[i]); 814 if (starColumn != null) { 815 bitKey.set(starColumn.getBitPosition()); 816 } 817 } 818 return bitKey; 819 } 820 821 /** 822 * Returns a list of all aliases used in this star. 823 */ 824 public List<String> getAliasList() { 825 List<String> aliasList = new ArrayList<String>(); 826 if (factTable != null) { 827 collectAliases(aliasList, factTable); 828 } 829 return aliasList; 830 } 831 832 /** 833 * Finds all of the table aliases in a table and its children. 834 */ 835 private static void collectAliases(List<String> aliasList, Table table) { 836 aliasList.add(table.getAlias()); 837 for (Table child : table.children) { 838 collectAliases(aliasList, child); 839 } 840 } 841 842 /** 843 * Collects all columns in this table and its children. 844 * If <code>joinColumn</code> is specified, only considers child tables 845 * joined by the given column. 846 */ 847 public static void collectColumns( 848 Collection<Column> columnList, 849 Table table, 850 MondrianDef.Column joinColumn) 851 { 852 if (joinColumn == null) { 853 columnList.addAll(table.columnList); 854 } 855 for (Table child : table.children) { 856 if (joinColumn == null || 857 child.getJoinCondition().left.equals(joinColumn)) { 858 collectColumns(columnList, child, null); 859 } 860 } 861 } 862 863 private boolean containsColumn(String tableName, String columnName) { 864 Connection jdbcConnection; 865 try { 866 jdbcConnection = dataSource.getConnection(); 867 } catch (SQLException e1) { 868 throw Util.newInternal( 869 e1, "Error while creating connection from data source"); 870 } 871 try { 872 final DatabaseMetaData metaData = jdbcConnection.getMetaData(); 873 final ResultSet columns = 874 metaData.getColumns(null, null, tableName, columnName); 875 return columns.next(); 876 } catch (SQLException e) { 877 throw Util.newInternal("Error while retrieving metadata for table '" + 878 tableName + "', column '" + columnName + "'"); 879 } finally { 880 try { 881 jdbcConnection.close(); 882 } catch (SQLException e) { 883 // ignore 884 } 885 } 886 } 887 888 public RolapSchema getSchema() { 889 return schema; 890 } 891 892 /** 893 * Generates a SQL statement to read all instances of the given attributes. 894 * 895 * <p>The SQL statement is of the form {@code SELECT ... FROM ... JOIN ... 896 * GROUP BY ...}. It is useful for populating an aggregate table. 897 * 898 * @param columnList List of columns (attributes and measures) 899 * @param columnNameList List of column names (must have same cardinality 900 * as {@code columnList}) 901 * @return SQL SELECT statement 902 */ 903 public String generateSql( 904 List<Column> columnList, 905 List<String> columnNameList) 906 { 907 final SqlQuery query = new SqlQuery(sqlQueryDialect, true); 908 query.addFrom( 909 factTable.relation, 910 factTable.relation.getAlias(), 911 false); 912 int k = -1; 913 for (Column column : columnList) { 914 ++k; 915 column.table.addToFrom(query, false, true); 916 String columnExpr = column.generateExprString(query); 917 if (column instanceof Measure) { 918 Measure measure = (Measure) column; 919 columnExpr = measure.getAggregator().getExpression(columnExpr); 920 } 921 final String columnName = columnNameList.get(k); 922 query.addSelect(columnExpr, columnName); 923 if (!(column instanceof Measure)) { 924 query.addGroupBy(columnExpr); 925 } 926 } 927 // remove whitespace from query - in particular, the trailing newline 928 return query.toString().trim(); 929 } 930 931 public String toString() { 932 StringWriter sw = new StringWriter(256); 933 PrintWriter pw = new PrintWriter(sw); 934 print(pw, "", true); 935 pw.flush(); 936 return sw.toString(); 937 } 938 939 /** 940 * Prints the state of this <code>RolapStar</code> 941 * 942 * @param pw Writer 943 * @param prefix Prefix to print at the start of each line 944 * @param structure Whether to print the structure of the star 945 */ 946 public void print(PrintWriter pw, String prefix, boolean structure) { 947 if (structure) { 948 pw.print(prefix); 949 pw.println("RolapStar:"); 950 String subprefix = prefix + " "; 951 factTable.print(pw, subprefix); 952 953 for (AggStar aggStar : getAggStars()) { 954 aggStar.print(pw, subprefix); 955 } 956 } 957 958 List<Aggregation> aggregationList = 959 new ArrayList<Aggregation>(sharedAggregations.values()); 960 Collections.sort( 961 aggregationList, 962 new Comparator<Aggregation>() { 963 public int compare(Aggregation o1, Aggregation o2) { 964 return o1.getConstrainedColumnsBitKey().compareTo( 965 o2.getConstrainedColumnsBitKey()); 966 } 967 } 968 ); 969 970 for (Aggregation aggregation : aggregationList) { 971 aggregation.print(pw); 972 } 973 } 974 975 /** 976 * Flushes the contents of a given region of cells from this star. 977 * 978 * @param cacheControl Cache control API 979 * @param region Predicate defining a region of cells 980 */ 981 public void flush( 982 CacheControl cacheControl, 983 CacheControl.CellRegion region) 984 { 985 // Translate the region into a set of (column, value) constraints. 986 final RolapCacheRegion cacheRegion = 987 RolapAggregationManager.makeCacheRegion(this, region); 988 for (Aggregation aggregation : sharedAggregations.values()) { 989 aggregation.flush(cacheControl, cacheRegion); 990 } 991 } 992 993 994 /** 995 * Returns the listener for changes to this star's underlying database. 996 * 997 * @return Returns the Data source change listener. 998 */ 999 public DataSourceChangeListener getChangeListener() { 1000 return changeListener; 1001 } 1002 1003 /** 1004 * Sets the listener for changes to this star's underlying database. 1005 * 1006 * @param changeListener The Data source change listener to set 1007 */ 1008 public void setChangeListener(DataSourceChangeListener changeListener) { 1009 this.changeListener = changeListener; 1010 } 1011 1012 // -- Inner classes -------------------------------------------------------- 1013 1014 /** 1015 * A column in a star schema. 1016 */ 1017 public static class Column { 1018 private final Table table; 1019 private final MondrianDef.Expression expression; 1020 private final SqlQuery.Datatype datatype; 1021 private final String name; 1022 /** 1023 * When a Column is a column, and not a Measure, the parent column 1024 * is the coloumn associated with next highest Level. 1025 */ 1026 private final Column parentColumn; 1027 1028 /** 1029 * This is used during both aggregate table recognition and aggregate 1030 * table generation. For multiple dimension usages, multiple shared 1031 * dimension or unshared dimension with the same column names, 1032 * this is used to disambiguate aggregate column names. 1033 */ 1034 private final String usagePrefix; 1035 /** 1036 * This is only used in RolapAggregationManager and adds 1037 * non-constraining columns making the drill-through queries easier for 1038 * humans to understand. 1039 */ 1040 private final Column nameColumn; 1041 private boolean isNameColumn; 1042 1043 /** this has a unique value per star */ 1044 private final int bitPosition; 1045 1046 private int cardinality = -1; 1047 1048 private Column( 1049 String name, 1050 Table table, 1051 MondrianDef.Expression expression, 1052 SqlQuery.Datatype datatype) 1053 { 1054 this(name, table, expression, datatype, null, null, null); 1055 } 1056 1057 private Column( 1058 String name, 1059 Table table, 1060 MondrianDef.Expression expression, 1061 SqlQuery.Datatype datatype, 1062 Column nameColumn, 1063 Column parentColumn, 1064 String usagePrefix) 1065 { 1066 this.name = name; 1067 this.table = table; 1068 this.expression = expression; 1069 this.datatype = datatype; 1070 this.bitPosition = table.star.nextColumnCount(); 1071 this.nameColumn = nameColumn; 1072 this.parentColumn = parentColumn; 1073 this.usagePrefix = usagePrefix; 1074 if (nameColumn != null) { 1075 nameColumn.isNameColumn = true; 1076 } 1077 } 1078 1079 /** 1080 * Fake column. 1081 * 1082 * @param datatype Datatype 1083 */ 1084 protected Column(SqlQuery.Datatype datatype) 1085 { 1086 this.table = null; 1087 this.expression = null; 1088 this.datatype = datatype; 1089 this.name = null; 1090 this.parentColumn = null; 1091 this.nameColumn = null; 1092 this.usagePrefix = null; 1093 this.bitPosition = 0; 1094 } 1095 1096 public boolean equals(Object obj) { 1097 if (! (obj instanceof RolapStar.Column)) { 1098 return false; 1099 } 1100 RolapStar.Column other = (RolapStar.Column) obj; 1101 // Note: both columns have to be from the same table 1102 return (other.table == this.table) && 1103 other.expression.equals(this.expression) && 1104 (other.datatype == this.datatype) && 1105 other.name.equals(this.name); 1106 } 1107 1108 public int hashCode() { 1109 int h = name.hashCode(); 1110 h = Util.hash(h, table); 1111 return h; 1112 } 1113 1114 public String getName() { 1115 return name; 1116 } 1117 1118 public int getBitPosition() { 1119 return bitPosition; 1120 } 1121 1122 public RolapStar getStar() { 1123 return table.star; 1124 } 1125 1126 public RolapStar.Table getTable() { 1127 return table; 1128 } 1129 1130 public SqlQuery getSqlQuery() { 1131 return getTable().getStar().getSqlQuery(); 1132 } 1133 1134 public RolapStar.Column getNameColumn() { 1135 return nameColumn; 1136 } 1137 1138 public RolapStar.Column getParentColumn() { 1139 return parentColumn; 1140 } 1141 1142 public String getUsagePrefix() { 1143 return usagePrefix; 1144 } 1145 1146 public boolean isNameColumn() { 1147 return isNameColumn; 1148 } 1149 1150 public MondrianDef.Expression getExpression() { 1151 return expression; 1152 } 1153 1154 /** 1155 * Generates a SQL expression, which typically this looks like 1156 * this: <code><i>tableName</i>.<i>columnName</i></code>. 1157 */ 1158 public String generateExprString(SqlQuery query) { 1159 return getExpression().getExpression(query); 1160 } 1161 1162 /** 1163 * Get column cardinality from the schema cache if possible; 1164 * otherwise issue a select count(distinct) query to retrieve 1165 * the cardinality and stores it in the cache. 1166 * 1167 * @return the column cardinality. 1168 */ 1169 public int getCardinality() { 1170 if (cardinality == -1) { 1171 RolapStar star = getStar(); 1172 RolapSchema schema = star.getSchema(); 1173 Integer card = 1174 schema.getCachedRelationExprCardinality( 1175 table.getRelation(), 1176 expression); 1177 1178 if (card != null) { 1179 cardinality = card.intValue(); 1180 } else { 1181 // If not cached, issue SQL to get the cardinality for 1182 // this column. 1183 cardinality = getCardinality(star.getDataSource()); 1184 schema.putCachedRelationExprCardinality( 1185 table.getRelation(), 1186 expression, 1187 cardinality); 1188 } 1189 } 1190 return cardinality; 1191 } 1192 1193 private int getCardinality(DataSource dataSource) { 1194 SqlQuery sqlQuery = getSqlQuery(); 1195 if (sqlQuery.getDialect().allowsCountDistinct()) { 1196 // e.g. "select count(distinct product_id) from product" 1197 sqlQuery.addSelect("count(distinct " 1198 + generateExprString(sqlQuery) + ")"); 1199 1200 // no need to join fact table here 1201 table.addToFrom(sqlQuery, true, false); 1202 } else if (sqlQuery.getDialect().allowsFromQuery()) { 1203 // Some databases (e.g. Access) don't like 'count(distinct)', 1204 // so use, e.g., "select count(*) from (select distinct 1205 // product_id from product)" 1206 SqlQuery inner = sqlQuery.cloneEmpty(); 1207 inner.setDistinct(true); 1208 inner.addSelect(generateExprString(inner)); 1209 boolean failIfExists = true, 1210 joinToParent = false; 1211 table.addToFrom(inner, failIfExists, joinToParent); 1212 sqlQuery.addSelect("count(*)"); 1213 sqlQuery.addFrom(inner, "init", failIfExists); 1214 } else { 1215 throw Util.newInternal("Cannot compute cardinality: this " + 1216 "database neither supports COUNT DISTINCT nor SELECT in " + 1217 "the FROM clause."); 1218 } 1219 String sql = sqlQuery.toString(); 1220 final SqlStatement stmt = 1221 RolapUtil.executeQuery( 1222 dataSource, sql, 1223 "RolapStar.Column.getCardinality", 1224 "while counting distinct values of column '" + 1225 expression.getGenericExpression()); 1226 try { 1227 ResultSet resultSet = stmt.getResultSet(); 1228 Util.assertTrue(resultSet.next()); 1229 ++stmt.rowCount; 1230 return resultSet.getInt(1); 1231 } catch (SQLException e) { 1232 throw stmt.handle(e); 1233 } finally { 1234 stmt.close(); 1235 } 1236 } 1237 1238 /** 1239 * Generates a predicate that a column matches one of a list of values. 1240 * 1241 * <p> 1242 * Several possible outputs, depending upon whether the there are 1243 * nulls:<ul> 1244 * 1245 * <li>One not-null value: <code>foo.bar = 1</code> 1246 * 1247 * <li>All values not null: <code>foo.bar in (1, 2, 3)</code></li 1248 * 1249 * <li>Null and not null values: 1250 * <code>(foo.bar is null or foo.bar in (1, 2))</code></li> 1251 * 1252 * <li>Only null values: 1253 * <code>foo.bar is null</code></li> 1254 * 1255 * <li>String values: <code>foo.bar in ('a', 'b', 'c')</code></li> 1256 * 1257 * </ul> 1258 */ 1259 public static String createInExpr( 1260 final String expr, 1261 StarColumnPredicate predicate, 1262 SqlQuery.Datatype datatype, 1263 SqlQuery sqlQuery) 1264 { 1265 // Sometimes a column predicate is created without a column. This 1266 // is unfortunate, and we will fix it some day. For now, create 1267 // a fake column with all of the information needed by the toSql 1268 // method, and a copy of the predicate wrapping that fake column. 1269 if (!Bug.Bug1767775Fixed || 1270 !Bug.Bug1767779Fixed && predicate.getConstrainedColumn() == null) 1271 { 1272 Column column = new Column(datatype) { 1273 public String generateExprString(SqlQuery query) { 1274 return expr; 1275 } 1276 }; 1277 predicate = predicate.cloneWithColumn(column); 1278 } 1279 1280 StringBuilder buf = new StringBuilder(64); 1281 predicate.toSql(sqlQuery, buf); 1282 return buf.toString(); 1283 } 1284 1285 public String toString() { 1286 StringWriter sw = new StringWriter(256); 1287 PrintWriter pw = new PrintWriter(sw); 1288 print(pw, ""); 1289 pw.flush(); 1290 return sw.toString(); 1291 } 1292 1293 /** 1294 * Prints this column. 1295 * 1296 * @param pw Print writer 1297 * @param prefix Prefix to print first, such as spaces for indentation 1298 */ 1299 public void print(PrintWriter pw, String prefix) { 1300 SqlQuery sqlQuery = getSqlQuery(); 1301 pw.print(prefix); 1302 pw.print(getName()); 1303 pw.print(" ("); 1304 pw.print(getBitPosition()); 1305 pw.print("): "); 1306 pw.print(generateExprString(sqlQuery)); 1307 } 1308 1309 public SqlQuery.Datatype getDatatype() { 1310 return datatype; 1311 } 1312 1313 /** 1314 * Returns a string representation of the datatype of this column, in 1315 * the dialect specified. For example, 'DECIMAL(10, 2) NOT NULL'. 1316 * 1317 * @param dialect Dialect 1318 * @return String representation of column's datatype 1319 */ 1320 public String getDatatypeString(SqlQuery.Dialect dialect) { 1321 final SqlQuery query = new SqlQuery(dialect); 1322 query.addFrom( 1323 table.star.factTable.relation, table.star.factTable.alias, 1324 false); 1325 query.addFrom(table.relation, table.alias, false); 1326 query.addSelect(expression.getExpression(query)); 1327 final String sql = query.toString(); 1328 Connection jdbcConnection = null; 1329 try { 1330 jdbcConnection = table.star.dataSource.getConnection(); 1331 final PreparedStatement pstmt = 1332 jdbcConnection.prepareStatement(sql); 1333 final ResultSetMetaData resultSetMetaData = 1334 pstmt.getMetaData(); 1335 assert resultSetMetaData.getColumnCount() == 1; 1336 final String type = resultSetMetaData.getColumnTypeName(1); 1337 int precision = resultSetMetaData.getPrecision(1); 1338 final int scale = resultSetMetaData.getScale(1); 1339 if (type.equals("DOUBLE")) { 1340 precision = 0; 1341 } 1342 String typeString; 1343 if (precision == 0) { 1344 typeString = type; 1345 } else if (scale == 0) { 1346 typeString = type + "(" + precision + ")"; 1347 } else { 1348 typeString = type + "(" + precision + ", " + scale + ")"; 1349 } 1350 pstmt.close(); 1351 jdbcConnection.close(); 1352 jdbcConnection = null; 1353 return typeString; 1354 } catch (SQLException e) { 1355 throw Util.newError( 1356 e, 1357 "Error while deriving type of column " + toString()); 1358 } finally { 1359 if (jdbcConnection != null) { 1360 try { 1361 jdbcConnection.close(); 1362 } catch (SQLException e) { 1363 // ignore 1364 } 1365 } 1366 } 1367 } 1368 } 1369 1370 /** 1371 * Definition of a measure in a star schema. 1372 * 1373 * <p>A measure is basically just a column; except that its 1374 * {@link #aggregator} defines how it is to be rolled up. 1375 */ 1376 public static class Measure extends Column { 1377 private final String cubeName; 1378 private final RolapAggregator aggregator; 1379 1380 public Measure( 1381 String name, 1382 String cubeName, 1383 RolapAggregator aggregator, 1384 Table table, 1385 MondrianDef.Expression expression, 1386 SqlQuery.Datatype datatype) 1387 { 1388 super(name, table, expression, datatype); 1389 this.cubeName = cubeName; 1390 this.aggregator = aggregator; 1391 } 1392 1393 public RolapAggregator getAggregator() { 1394 return aggregator; 1395 } 1396 1397 public boolean equals(Object o) { 1398 if (! (o instanceof RolapStar.Measure)) { 1399 return false; 1400 } 1401 RolapStar.Measure that = (RolapStar.Measure) o; 1402 if (!super.equals(that)) { 1403 return false; 1404 } 1405 // Measure names are only unique within their cube - and remember 1406 // that a given RolapStar can support multiple cubes if they have 1407 // the same fact table. 1408 if (!cubeName.equals(that.cubeName)) { 1409 return false; 1410 } 1411 // Note: both measure have to have the same aggregator 1412 return (that.aggregator == this.aggregator); 1413 } 1414 1415 public int hashCode() { 1416 int h = super.hashCode(); 1417 h = Util.hash(h, aggregator); 1418 return h; 1419 } 1420 1421 public void print(PrintWriter pw, String prefix) { 1422 SqlQuery sqlQuery = getSqlQuery(); 1423 pw.print(prefix); 1424 pw.print(getName()); 1425 pw.print(" ("); 1426 pw.print(getBitPosition()); 1427 pw.print("): "); 1428 pw.print(aggregator.getExpression(generateExprString(sqlQuery))); 1429 } 1430 1431 public String getCubeName() { 1432 return cubeName; 1433 } 1434 } 1435 1436 /** 1437 * Definition of a table in a star schema. 1438 * 1439 * <p>A 'table' is defined by a 1440 * {@link mondrian.olap.MondrianDef.RelationOrJoin} so may, in fact, be a view. 1441 * 1442 * <p>Every table in the star schema except the fact table has a parent 1443 * table, and a condition which specifies how it is joined to its parent. 1444 * So the star schema is, in effect, a hierarchy with the fact table at 1445 * its root. 1446 */ 1447 public static class Table { 1448 private final RolapStar star; 1449 private final MondrianDef.Relation relation; 1450 private final List<Column> columnList; 1451 private final Table parent; 1452 private List<Table> children; 1453 private final Condition joinCondition; 1454 private final String alias; 1455 1456 private Table( 1457 RolapStar star, 1458 MondrianDef.Relation relation, 1459 Table parent, 1460 Condition joinCondition) 1461 { 1462 this.star = star; 1463 this.relation = relation; 1464 this.alias = chooseAlias(); 1465 this.parent = parent; 1466 final AliasReplacer aliasReplacer = 1467 new AliasReplacer(relation.getAlias(), this.alias); 1468 this.joinCondition = aliasReplacer.visit(joinCondition); 1469 if (this.joinCondition != null) { 1470 this.joinCondition.table = this; 1471 } 1472 this.columnList = new ArrayList<Column>(); 1473 this.children = Collections.emptyList(); 1474 Util.assertTrue((parent == null) == (joinCondition == null)); 1475 } 1476 1477 /** 1478 * Returns the condition by which a dimension table is connected to its 1479 * {@link #getParentTable() parent}; or null if this is the fact table. 1480 */ 1481 public Condition getJoinCondition() { 1482 return joinCondition; 1483 } 1484 1485 /** 1486 * Returns this table's parent table, or null if this is the fact table 1487 * (which is at the center of the star). 1488 */ 1489 public Table getParentTable() { 1490 return parent; 1491 } 1492 1493 private void addColumn(Column column) { 1494 columnList.add(column); 1495 } 1496 1497 /** 1498 * Adds to a list all columns of this table or a child table 1499 * which are present in a given bitKey. 1500 * 1501 * <p>Note: This method is slow, but that's acceptable because it is 1502 * only used for tracing. It would be more efficient to store an 1503 * array in the {@link RolapStar} mapping column ordinals to columns. 1504 */ 1505 private void collectColumns(BitKey bitKey, List<Column> list) { 1506 for (Column column : getColumns()) { 1507 if (bitKey.get(column.getBitPosition())) { 1508 list.add(column); 1509 } 1510 } 1511 for (Table table : getChildren()) { 1512 table.collectColumns(bitKey, list); 1513 } 1514 } 1515 1516 /** 1517 * Returns an array of all columns in this star with a given name. 1518 */ 1519 public Column[] lookupColumns(String columnName) { 1520 List<Column> l = new ArrayList<Column>(); 1521 for (Column column : getColumns()) { 1522 if (column.getExpression() instanceof MondrianDef.Column) { 1523 MondrianDef.Column columnExpr = 1524 (MondrianDef.Column) column.getExpression(); 1525 if (columnExpr.name.equals(columnName)) { 1526 l.add(column); 1527 } 1528 } 1529 } 1530 return l.toArray(new Column[l.size()]); 1531 } 1532 1533 public Column lookupColumn(String columnName) { 1534 for (Column column : getColumns()) { 1535 if (column.getExpression() instanceof MondrianDef.Column) { 1536 MondrianDef.Column columnExpr = 1537 (MondrianDef.Column) column.getExpression(); 1538 if (columnExpr.name.equals(columnName)) { 1539 return column; 1540 } 1541 } else if (column.getName().equals(columnName)) { 1542 return column; 1543 } 1544 } 1545 return null; 1546 } 1547 1548 /** 1549 * Given a MondrianDef.Expression return a column with that expression 1550 * or null. 1551 */ 1552 public Column lookupColumnByExpression(MondrianDef.Expression xmlExpr) { 1553 for (Column column : getColumns()) { 1554 if (column instanceof Measure) { 1555 continue; 1556 } 1557 if (column.getExpression().equals(xmlExpr)) { 1558 return column; 1559 } 1560 } 1561 return null; 1562 } 1563 1564 public boolean containsColumn(Column column) { 1565 return getColumns().contains(column); 1566 } 1567 1568 /** 1569 * Look up a {@link Measure} by its name. 1570 * Returns null if not found. 1571 */ 1572 public Measure lookupMeasureByName(String cubeName, String name) { 1573 for (Column column : getColumns()) { 1574 if (column instanceof Measure) { 1575 Measure measure = (Measure) column; 1576 if (measure.getName().equals(name) && 1577 measure.getCubeName().equals(cubeName)) { 1578 return measure; 1579 } 1580 } 1581 } 1582 return null; 1583 } 1584 1585 RolapStar getStar() { 1586 return star; 1587 } 1588 private SqlQuery getSqlQuery() { 1589 return getStar().getSqlQuery(); 1590 } 1591 public MondrianDef.Relation getRelation() { 1592 return relation; 1593 } 1594 1595 /** Chooses an alias which is unique within the star. */ 1596 private String chooseAlias() { 1597 List<String> aliasList = star.getAliasList(); 1598 for (int i = 0;; ++i) { 1599 String candidateAlias = relation.getAlias(); 1600 if (i > 0) { 1601 candidateAlias += "_" + i; 1602 } 1603 if (!aliasList.contains(candidateAlias)) { 1604 return candidateAlias; 1605 } 1606 } 1607 } 1608 1609 public String getAlias() { 1610 return alias; 1611 } 1612 1613 /** 1614 * Sometimes one need to get to the "real" name when the table has 1615 * been given an alias. 1616 */ 1617 public String getTableName() { 1618 if (relation instanceof MondrianDef.Table) { 1619 MondrianDef.Table t = (MondrianDef.Table) relation; 1620 return t.name; 1621 } else { 1622 return null; 1623 } 1624 } 1625 1626 synchronized void makeMeasure(RolapBaseCubeMeasure measure) { 1627 assert lookupMeasureByName( 1628 measure.getCube().getName(), measure.getName()) == null; 1629 RolapStar.Measure starMeasure = new RolapStar.Measure( 1630 measure.getName(), 1631 measure.getCube().getName(), 1632 measure.getAggregator(), 1633 this, 1634 measure.getMondrianDefExpression(), 1635 measure.getDatatype()); 1636 1637 measure.setStarMeasure(starMeasure); // reverse mapping 1638 1639 if (containsColumn(starMeasure)) { 1640 star.decrementColumnCount(); 1641 } else { 1642 addColumn(starMeasure); 1643 } 1644 } 1645 1646 /** 1647 * This is only called by RolapCube. If the RolapLevel has a non-null 1648 * name expression then two columns will be made, otherwise only one. 1649 * Updates the RolapLevel to RolapStar.Column mapping associated with 1650 * this cube. 1651 * 1652 * @param cube Cube 1653 * @param level Level 1654 * @param parentColumn Parent column 1655 */ 1656 synchronized Column makeColumns( 1657 RolapCube cube, 1658 RolapCubeLevel level, 1659 Column parentColumn, 1660 String usagePrefix) { 1661 1662 Column nameColumn = null; 1663 if (level.getNameExp() != null) { 1664 // make a column for the name expression 1665 nameColumn = makeColumnForLevelExpr( 1666 cube, 1667 level, 1668 level.getName(), 1669 level.getNameExp(), 1670 SqlQuery.Datatype.String, 1671 null, 1672 null, 1673 null); 1674 } 1675 1676 // select the column's name depending upon whether or not a 1677 // "named" column, above, has been created. 1678 String name = (level.getNameExp() == null) 1679 ? level.getName() 1680 : level.getName() + " (Key)"; 1681 1682 // If the nameColumn is not null, then it is associated with this 1683 // column. 1684 Column column = makeColumnForLevelExpr( 1685 cube, 1686 level, 1687 name, 1688 level.getKeyExp(), 1689 level.getDatatype(), 1690 nameColumn, 1691 parentColumn, 1692 usagePrefix); 1693 1694 if (column != null) { 1695 level.setStarKeyColumn(column); 1696 } 1697 1698 return column; 1699 } 1700 1701 private Column makeColumnForLevelExpr( 1702 RolapCube cube, 1703 RolapLevel level, 1704 String name, 1705 MondrianDef.Expression xmlExpr, 1706 SqlQuery.Datatype datatype, 1707 Column nameColumn, 1708 Column parentColumn, 1709 String usagePrefix) { 1710 Table table = this; 1711 if (xmlExpr instanceof MondrianDef.Column) { 1712 final MondrianDef.Column xmlColumn = 1713 (MondrianDef.Column) xmlExpr; 1714 1715 String tableName = xmlColumn.table; 1716 table = findAncestor(tableName); 1717 if (table == null) { 1718 throw Util.newError( 1719 "Level '" + level.getUniqueName() 1720 + "' of cube '" 1721 + this 1722 + "' is invalid: table '" + tableName 1723 + "' is not found in current scope" 1724 + Util.nl 1725 + ", star:" 1726 + Util.nl 1727 + getStar()); 1728 } 1729 RolapStar.AliasReplacer aliasReplacer = 1730 new RolapStar.AliasReplacer(tableName, table.getAlias()); 1731 xmlExpr = aliasReplacer.visit(xmlExpr); 1732 } 1733 // does the column already exist?? 1734 Column c = lookupColumnByExpression(xmlExpr); 1735 1736 RolapStar.Column column = null; 1737 // Verify Column is not null and not the same as the 1738 // nameColumn created previously (bug 1438285) 1739 if (c != null && !c.equals(nameColumn)) { 1740 // Yes, well just reuse it 1741 // You might wonder why the column need be returned if it 1742 // already exists. Well, it might have been created for one 1743 // cube, but for another cube using the same fact table, it 1744 // still needs to be put into the cube level to column map. 1745 // Trust me, return null and a junit test fails. 1746 column = c; 1747 } else { 1748 // Make a new column and add it 1749 column = new RolapStar.Column( 1750 name, 1751 table, 1752 xmlExpr, 1753 datatype, 1754 nameColumn, 1755 parentColumn, 1756 usagePrefix); 1757 addColumn(column); 1758 } 1759 return column; 1760 } 1761 1762 /** 1763 * Extends this 'leg' of the star by adding <code>relation</code> 1764 * joined by <code>joinCondition</code>. If the same expression is 1765 * already present, does not create it again. Stores the unaliased 1766 * table names to RolapStar.Table mapping associated with the 1767 * input <code>cube</code>. 1768 */ 1769 synchronized Table addJoin( 1770 RolapCube cube, 1771 MondrianDef.RelationOrJoin relationOrJoin, 1772 RolapStar.Condition joinCondition) 1773 { 1774 if (relationOrJoin instanceof MondrianDef.Relation) { 1775 final MondrianDef.Relation relation = 1776 (MondrianDef.Relation) relationOrJoin; 1777 RolapStar.Table starTable = 1778 findChild(relation, joinCondition); 1779 if (starTable == null) { 1780 starTable = new RolapStar.Table( 1781 star, relation, this, joinCondition); 1782 if (this.children.isEmpty()) { 1783 this.children = new ArrayList<Table>(); 1784 } 1785 this.children.add(starTable); 1786 } 1787 return starTable; 1788 } else if (relationOrJoin instanceof MondrianDef.Join) { 1789 MondrianDef.Join join = (MondrianDef.Join) relationOrJoin; 1790 RolapStar.Table leftTable = addJoin(cube, join.left, joinCondition); 1791 String leftAlias = join.leftAlias; 1792 if (leftAlias == null) { 1793 // REVIEW: is cast to Relation valid? 1794 leftAlias = ((MondrianDef.Relation) join.left).getAlias(); 1795 if (leftAlias == null) { 1796 throw Util.newError( 1797 "missing leftKeyAlias in " + relationOrJoin); 1798 } 1799 } 1800 assert leftTable.findAncestor(leftAlias) == leftTable; 1801 // switch to uniquified alias 1802 leftAlias = leftTable.getAlias(); 1803 1804 String rightAlias = join.rightAlias; 1805 if (rightAlias == null) { 1806 1807 // the right relation of a join may be a join 1808 // if so, we need to use the right relation join's 1809 // left relation's alias. 1810 if (join.right instanceof MondrianDef.Join) { 1811 MondrianDef.Join joinright = 1812 (MondrianDef.Join) join.right; 1813 // REVIEW: is cast to Relation valid? 1814 rightAlias = 1815 ((MondrianDef.Relation) joinright.left) 1816 .getAlias(); 1817 } else { 1818 // REVIEW: is cast to Relation valid? 1819 rightAlias = 1820 ((MondrianDef.Relation) join.right) 1821 .getAlias(); 1822 } 1823 if (rightAlias == null) { 1824 throw Util.newError( 1825 "missing rightKeyAlias in " + relationOrJoin); 1826 } 1827 } 1828 joinCondition = new RolapStar.Condition( 1829 new MondrianDef.Column(leftAlias, join.leftKey), 1830 new MondrianDef.Column(rightAlias, join.rightKey)); 1831 RolapStar.Table rightTable = leftTable.addJoin( 1832 cube, join.right, joinCondition); 1833 return rightTable; 1834 1835 } else { 1836 throw Util.newInternal("bad relation type " + relationOrJoin); 1837 } 1838 } 1839 1840 /** 1841 * Returns a child relation which maps onto a given relation, or null 1842 * if there is none. 1843 */ 1844 public Table findChild( 1845 MondrianDef.Relation relation, 1846 Condition joinCondition) 1847 { 1848 for (Table child : getChildren()) { 1849 if (child.relation.equals(relation)) { 1850 Condition condition = joinCondition; 1851 if (!Util.equalName(relation.getAlias(), child.alias)) { 1852 // Make the two conditions comparable, by replacing 1853 // occurrence of this table's alias with occurrences 1854 // of the child's alias. 1855 AliasReplacer aliasReplacer = new AliasReplacer( 1856 relation.getAlias(), child.alias); 1857 condition = aliasReplacer.visit(joinCondition); 1858 } 1859 if (child.joinCondition.equals(condition)) { 1860 return child; 1861 } 1862 } 1863 } 1864 return null; 1865 } 1866 1867 /** 1868 * Returns a descendant with a given alias, or null if none found. 1869 */ 1870 public Table findDescendant(String seekAlias) { 1871 if (getAlias().equals(seekAlias)) { 1872 return this; 1873 } 1874 for (Table child : getChildren()) { 1875 Table found = child.findDescendant(seekAlias); 1876 if (found != null) { 1877 return found; 1878 } 1879 } 1880 return null; 1881 } 1882 1883 /** 1884 * Returns an ancestor with a given alias, or null if not found. 1885 */ 1886 public Table findAncestor(String tableName) { 1887 for (Table t = this; t != null; t = t.parent) { 1888 if (t.relation.getAlias().equals(tableName)) { 1889 return t; 1890 } 1891 } 1892 return null; 1893 } 1894 1895 public boolean equalsTableName(String tableName) { 1896 if (this.relation instanceof MondrianDef.Table) { 1897 MondrianDef.Table mt = (MondrianDef.Table) this.relation; 1898 if (mt.name.equals(tableName)) { 1899 return true; 1900 } 1901 } 1902 return false; 1903 } 1904 1905 /** 1906 * Adds this table to the FROM clause of a query, and also, if 1907 * <code>joinToParent</code>, any join condition. 1908 * 1909 * @param query Query to add to 1910 * @param failIfExists Pass in false if you might have already added 1911 * the table before and if that happens you want to do nothing. 1912 * @param joinToParent Pass in true if you are constraining a cell 1913 * calculation, false if you are retrieving members. 1914 */ 1915 public void addToFrom( 1916 SqlQuery query, 1917 boolean failIfExists, 1918 boolean joinToParent) { 1919 query.addFrom(relation, alias, failIfExists); 1920 Util.assertTrue((parent == null) == (joinCondition == null)); 1921 if (joinToParent) { 1922 if (parent != null) { 1923 parent.addToFrom(query, failIfExists, joinToParent); 1924 } 1925 if (joinCondition != null) { 1926 query.addWhere(joinCondition.toString(query)); 1927 } 1928 } 1929 } 1930 1931 /** 1932 * Returns a list of child {@link Table}s. 1933 */ 1934 public List<Table> getChildren() { 1935 return children; 1936 } 1937 1938 /** 1939 * Returns a list of this table's {@link Column}s. 1940 */ 1941 public List<Column> getColumns() { 1942 return columnList; 1943 } 1944 1945 /** 1946 * Finds the child table of the fact table with the given columnName 1947 * used in its left join condition. This is used by the AggTableManager 1948 * while characterizing the fact table columns. 1949 */ 1950 public RolapStar.Table findTableWithLeftJoinCondition( 1951 final String columnName) 1952 { 1953 for (Table child : getChildren()) { 1954 Condition condition = child.joinCondition; 1955 if (condition != null) { 1956 if (condition.left instanceof MondrianDef.Column) { 1957 MondrianDef.Column mcolumn = 1958 (MondrianDef.Column) condition.left; 1959 if (mcolumn.name.equals(columnName)) { 1960 return child; 1961 } 1962 } 1963 } 1964 1965 } 1966 return null; 1967 } 1968 1969 /** 1970 * This is used during aggregate table validation to make sure that the 1971 * mapping from for the aggregate join condition is valid. It returns 1972 * the child table with the matching left join condition. 1973 */ 1974 public RolapStar.Table findTableWithLeftCondition( 1975 final MondrianDef.Expression left) 1976 { 1977 for (Table child : getChildren()) { 1978 Condition condition = child.joinCondition; 1979 if (condition != null) { 1980 if (condition.left instanceof MondrianDef.Column) { 1981 MondrianDef.Column mcolumn = 1982 (MondrianDef.Column) condition.left; 1983 if (mcolumn.equals(left)) { 1984 return child; 1985 } 1986 } 1987 } 1988 1989 } 1990 return null; 1991 } 1992 1993 /** 1994 * Note: I do not think that this is ever true. 1995 */ 1996 public boolean isFunky() { 1997 return (relation == null); 1998 } 1999 2000 public boolean equals(Object obj) { 2001 if (!(obj instanceof Table)) { 2002 return false; 2003 } 2004 Table other = (Table) obj; 2005 return getAlias().equals(other.getAlias()); 2006 } 2007 public int hashCode() { 2008 return getAlias().hashCode(); 2009 } 2010 2011 public String toString() { 2012 StringWriter sw = new StringWriter(256); 2013 PrintWriter pw = new PrintWriter(sw); 2014 print(pw, ""); 2015 pw.flush(); 2016 return sw.toString(); 2017 } 2018 2019 /** 2020 * Prints this table and its children. 2021 */ 2022 public void print(PrintWriter pw, String prefix) { 2023 pw.print(prefix); 2024 pw.println("Table:"); 2025 String subprefix = prefix + " "; 2026 2027 pw.print(subprefix); 2028 pw.print("alias="); 2029 pw.println(getAlias()); 2030 2031 if (this.relation != null) { 2032 pw.print(subprefix); 2033 pw.print("relation="); 2034 pw.println(relation); 2035 } 2036 2037 pw.print(subprefix); 2038 pw.println("Columns:"); 2039 String subsubprefix = subprefix + " "; 2040 2041 for (Column column : getColumns()) { 2042 column.print(pw, subsubprefix); 2043 pw.println(); 2044 } 2045 2046 if (this.joinCondition != null) { 2047 this.joinCondition.print(pw, subprefix); 2048 } 2049 for (Table child : getChildren()) { 2050 child.print(pw, subprefix); 2051 } 2052 } 2053 2054 /** 2055 * Returns whether this table has a column with the given name. 2056 */ 2057 public boolean containsColumn(String columnName) { 2058 if (relation instanceof MondrianDef.Relation) { 2059 return star.containsColumn( 2060 ((MondrianDef.Relation) relation).getAlias(), 2061 columnName); 2062 } else { 2063 // todo: Deal with join. 2064 return false; 2065 } 2066 } 2067 } 2068 2069 public static class Condition { 2070 private static final Logger LOGGER = Logger.getLogger(Condition.class); 2071 2072 private final MondrianDef.Expression left; 2073 private final MondrianDef.Expression right; 2074 // set in Table constructor 2075 Table table; 2076 2077 Condition(MondrianDef.Expression left, 2078 MondrianDef.Expression right) { 2079 assert left != null; 2080 assert right != null; 2081 2082 if (!(left instanceof MondrianDef.Column)) { 2083 // TODO: Will this ever print?? if not then left should be 2084 // of type MondrianDef.Column. 2085 LOGGER.debug("Condition.left NOT Column: " 2086 + left.getClass().getName()); 2087 } 2088 this.left = left; 2089 this.right = right; 2090 } 2091 public MondrianDef.Expression getLeft() { 2092 return left; 2093 } 2094 public String getLeft(final SqlQuery query) { 2095 return this.left.getExpression(query); 2096 } 2097 public MondrianDef.Expression getRight() { 2098 return right; 2099 } 2100 public String getRight(final SqlQuery query) { 2101 return this.right.getExpression(query); 2102 } 2103 public String toString(SqlQuery query) { 2104 return left.getExpression(query) + " = " + right.getExpression(query); 2105 } 2106 public int hashCode() { 2107 return left.hashCode() ^ right.hashCode(); 2108 } 2109 2110 public boolean equals(Object obj) { 2111 if (!(obj instanceof Condition)) { 2112 return false; 2113 } 2114 Condition that = (Condition) obj; 2115 return (this.left.equals(that.left) && 2116 this.right.equals(that.right)); 2117 } 2118 2119 public String toString() { 2120 StringWriter sw = new StringWriter(256); 2121 PrintWriter pw = new PrintWriter(sw); 2122 print(pw, ""); 2123 pw.flush(); 2124 return sw.toString(); 2125 } 2126 2127 /** 2128 * Prints this table and its children. 2129 */ 2130 public void print(PrintWriter pw, String prefix) { 2131 SqlQuery sqlQueuy = table.getSqlQuery(); 2132 pw.print(prefix); 2133 pw.println("Condition:"); 2134 String subprefix = prefix + " "; 2135 2136 pw.print(subprefix); 2137 pw.print("left="); 2138 // print the foreign key bit position if we can figure it out 2139 if (left instanceof MondrianDef.Column) { 2140 MondrianDef.Column c = (MondrianDef.Column) left; 2141 Column col = table.star.getFactTable().lookupColumn(c.name); 2142 if (col != null) { 2143 pw.print(" ("); 2144 pw.print(col.getBitPosition()); 2145 pw.print(") "); 2146 } 2147 } 2148 pw.println(left.getExpression(sqlQueuy)); 2149 2150 pw.print(subprefix); 2151 pw.print("right="); 2152 pw.println(right.getExpression(sqlQueuy)); 2153 } 2154 } 2155 2156 /** 2157 * Creates a copy of an expression, everywhere replacing one alias 2158 * with another. 2159 */ 2160 public static class AliasReplacer { 2161 private final String oldAlias; 2162 private final String newAlias; 2163 2164 public AliasReplacer(String oldAlias, String newAlias) { 2165 this.oldAlias = oldAlias; 2166 this.newAlias = newAlias; 2167 } 2168 2169 private Condition visit(Condition condition) { 2170 if (condition == null) { 2171 return null; 2172 } 2173 if (newAlias.equals(oldAlias)) { 2174 return condition; 2175 } 2176 return new Condition( 2177 visit(condition.left), 2178 visit(condition.right)); 2179 } 2180 2181 public MondrianDef.Expression visit(MondrianDef.Expression expression) { 2182 if (expression == null) { 2183 return null; 2184 } 2185 if (newAlias.equals(oldAlias)) { 2186 return expression; 2187 } 2188 if (expression instanceof MondrianDef.Column) { 2189 MondrianDef.Column column = (MondrianDef.Column) expression; 2190 return new MondrianDef.Column(visit(column.table), column.name); 2191 } else { 2192 throw Util.newInternal("need to implement " + expression); 2193 } 2194 } 2195 2196 private String visit(String table) { 2197 return table.equals(oldAlias) 2198 ? newAlias 2199 : table; 2200 } 2201 } 2202 2203 /** 2204 * Comparator to compare columns based on their name 2205 */ 2206 public static class ColumnComparator implements Comparator<Column> { 2207 2208 public static ColumnComparator instance = new ColumnComparator(); 2209 2210 private ColumnComparator() { 2211 } 2212 2213 public int compare(Column o1, Column o2) { 2214 return o1.getName().compareTo(o2.getName()); 2215 } 2216 } 2217 } 2218 2219 // End RolapStar.java