001 /* 002 // $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/Recognizer.java#22 $ 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) 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 011 package mondrian.rolap.aggmatcher; 012 013 import mondrian.olap.Hierarchy; 014 import mondrian.olap.Dimension; 015 import mondrian.olap.MondrianDef; 016 import mondrian.resource.MondrianResource; 017 import mondrian.recorder.MessageRecorder; 018 import mondrian.rolap.RolapStar; 019 import mondrian.rolap.RolapLevel; 020 import mondrian.rolap.RolapSchema; 021 import mondrian.rolap.RolapCube; 022 import mondrian.rolap.RolapAggregator; 023 import mondrian.rolap.HierarchyUsage; 024 import mondrian.rolap.sql.SqlQuery; 025 026 import java.util.*; 027 028 import org.apache.log4j.Logger; 029 030 /** 031 * Abstract Recognizer class used to determine if a candidate aggregate table 032 * has the column categories: "fact_count" column, measure columns, foreign key 033 * and level columns. 034 * 035 * <p>Derived classes use either the default or explicit column descriptions in 036 * matching column categories. The basic matching algorithm is in this class 037 * while some specific column category matching and column building must be 038 * specified in derived classes. 039 * 040 * <p>A Recognizer is created per candidate aggregate table. The tables columns are 041 * then categorized. All errors and warnings are added to a MessageRecorder. 042 * 043 * <p>This class is less about defining a type and more about code sharing. 044 * 045 * @author Richard M. Emberson 046 * @version $Id: //open/mondrian/src/main/mondrian/rolap/aggmatcher/Recognizer.java#22 $ 047 */ 048 abstract class Recognizer { 049 050 private static final MondrianResource mres = MondrianResource.instance(); 051 private static final Logger LOGGER = Logger.getLogger(Recognizer.class); 052 /** 053 * This is used to wrap column name matching rules. 054 */ 055 public interface Matcher { 056 057 /** 058 * Return true it the name matches and false otherwise. 059 */ 060 boolean matches(String name); 061 } 062 063 protected final RolapStar star; 064 protected final JdbcSchema.Table dbFactTable; 065 protected final JdbcSchema.Table aggTable; 066 protected final MessageRecorder msgRecorder; 067 protected boolean returnValue; 068 069 protected Recognizer(final RolapStar star, 070 final JdbcSchema.Table dbFactTable, 071 final JdbcSchema.Table aggTable, 072 final MessageRecorder msgRecorder) { 073 this.star = star; 074 this.dbFactTable = dbFactTable; 075 this.aggTable = aggTable; 076 this.msgRecorder = msgRecorder; 077 078 returnValue = true; 079 } 080 081 /** 082 * Return true if the candidate aggregate table was successfully mapped into 083 * the fact table. This is the top-level checking method. 084 * <p> 085 * It first checks the ignore columns. 086 * <p> 087 * Next, the existence of a fact count column is checked. 088 * <p> 089 * Then the measures are checked. First the specified (defined, 090 * explicit) measures are all determined. There must be at least one such 091 * measure. This if followed by checking for implied measures (e.g., if base 092 * fact table as both sum and average of a column and the aggregate has a 093 * sum measure, the there is an implied average measure in the aggregate). 094 * <p> 095 * Now the levels are checked. This is in two parts. First, foreign keys are 096 * checked followed by level columns (for collapsed dimension aggregates). 097 * <p> 098 * If eveything checks out, returns true. 099 */ 100 public boolean check() { 101 checkIgnores(); 102 checkFactCount(); 103 104 // Check measures 105 int nosMeasures = checkMeasures(); 106 // There must be at least one measure 107 checkNosMeasures(nosMeasures); 108 generateImpliedMeasures(); 109 110 // Check levels 111 List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys = checkForeignKeys(); 112 //printNotSeenForeignKeys(notSeenForeignKeys); 113 checkLevels(notSeenForeignKeys); 114 115 if (returnValue) { 116 // Add all unused columns as warning to the MessageRecorder 117 checkUnusedColumns(); 118 } 119 120 return returnValue; 121 } 122 123 /** 124 * Return the ignore column Matcher. 125 */ 126 protected abstract Matcher getIgnoreMatcher(); 127 128 /** 129 * Check all columns to be marked as ignore. 130 */ 131 protected void checkIgnores() { 132 Matcher ignoreMatcher = getIgnoreMatcher(); 133 134 for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) { 135 if (ignoreMatcher.matches(aggColumn.getName())) { 136 makeIgnore(aggColumn); 137 } 138 } 139 } 140 141 /** 142 * Create an ignore usage for the aggColumn. 143 * 144 * @param aggColumn 145 */ 146 protected void makeIgnore(final JdbcSchema.Table.Column aggColumn) { 147 JdbcSchema.Table.Column.Usage usage = 148 aggColumn.newUsage(JdbcSchema.UsageType.IGNORE); 149 usage.setSymbolicName("Ignore"); 150 } 151 152 153 154 /** 155 * Return the fact count column Matcher. 156 */ 157 protected abstract Matcher getFactCountMatcher(); 158 159 /** 160 * Make sure that the aggregate table has one fact count column and that its 161 * type is numeric. 162 */ 163 protected void checkFactCount() { 164 msgRecorder.pushContextName("Recognizer.checkFactCount"); 165 166 try { 167 168 Matcher factCountMatcher = getFactCountMatcher(); 169 170 int nosOfFactCounts = 0; 171 for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) { 172 // if marked as ignore, then do not consider 173 if (aggColumn.hasUsage(JdbcSchema.UsageType.IGNORE)) { 174 continue; 175 } 176 if (factCountMatcher.matches(aggColumn.getName())) { 177 if (aggColumn.getDatatype().isNumeric()) { 178 makeFactCount(aggColumn); 179 nosOfFactCounts++; 180 } else { 181 String msg = mres.NonNumericFactCountColumn.str( 182 aggTable.getName(), 183 dbFactTable.getName(), 184 aggColumn.getName(), 185 aggColumn.getTypeName()); 186 msgRecorder.reportError(msg); 187 188 returnValue = false; 189 } 190 } 191 192 } 193 if (nosOfFactCounts == 0) { 194 String msg = mres.NoFactCountColumns.str( 195 aggTable.getName(), 196 dbFactTable.getName()); 197 msgRecorder.reportError(msg); 198 199 returnValue = false; 200 201 } else if (nosOfFactCounts > 1) { 202 String msg = mres.TooManyFactCountColumns.str( 203 aggTable.getName(), 204 dbFactTable.getName(), 205 nosOfFactCounts); 206 msgRecorder.reportError(msg); 207 208 returnValue = false; 209 } 210 211 } finally { 212 msgRecorder.popContextName(); 213 } 214 } 215 216 217 /** 218 * Check all measure columns returning the number of measure columns. 219 */ 220 protected abstract int checkMeasures(); 221 222 /** 223 * Create a fact count usage for the aggColumn. 224 * 225 * @param aggColumn 226 */ 227 protected void makeFactCount(final JdbcSchema.Table.Column aggColumn) { 228 JdbcSchema.Table.Column.Usage usage = 229 aggColumn.newUsage(JdbcSchema.UsageType.FACT_COUNT); 230 usage.setSymbolicName("Fact Count"); 231 } 232 233 234 /** 235 * Make sure there was at least one measure column identified. 236 * 237 * @param nosMeasures 238 */ 239 protected void checkNosMeasures(int nosMeasures) { 240 msgRecorder.pushContextName("Recognizer.checkNosMeasures"); 241 242 try { 243 if (nosMeasures == 0) { 244 String msg = mres.NoMeasureColumns.str( 245 aggTable.getName(), 246 dbFactTable.getName()); 247 msgRecorder.reportError(msg); 248 249 returnValue = false; 250 } 251 252 } finally { 253 msgRecorder.popContextName(); 254 } 255 } 256 257 /** 258 * An implied measure in an aggregate table is one where there is both a sum 259 * and average measures in the base fact table and the aggregate table has 260 * either a sum or average, the other measure is implied and can be 261 * generated from the measure and the fact_count column. 262 * <p> 263 * For each column in the fact table, get its measure usages. If there is 264 * both an average and sum aggregator associated with the column, then 265 * iterator over all of the column usage of type measure of the aggregator 266 * table. If only one aggregate column usage measure is found and this 267 * RolapStar.Measure measure instance variable is the same as the 268 * the fact table's usage's instance variable, then the other measure is 269 * implied and the measure is created for the aggregate table. 270 */ 271 protected void generateImpliedMeasures() { 272 for (JdbcSchema.Table.Column factColumn : aggTable.getColumns()) { 273 JdbcSchema.Table.Column.Usage sumFactUsage = null; 274 JdbcSchema.Table.Column.Usage avgFactUsage = null; 275 276 for (Iterator<JdbcSchema.Table.Column.Usage> mit = 277 factColumn.getUsages(JdbcSchema.UsageType.MEASURE); 278 mit.hasNext();) { 279 JdbcSchema.Table.Column.Usage factUsage = mit.next(); 280 if (factUsage.getAggregator() == RolapAggregator.Avg) { 281 avgFactUsage = factUsage; 282 } else if (factUsage.getAggregator() == RolapAggregator.Sum) { 283 sumFactUsage = factUsage; 284 } 285 } 286 287 if (avgFactUsage != null && sumFactUsage != null) { 288 JdbcSchema.Table.Column.Usage sumAggUsage = null; 289 JdbcSchema.Table.Column.Usage avgAggUsage = null; 290 int seenCount = 0; 291 for (Iterator<JdbcSchema.Table.Column.Usage> mit = 292 aggTable.getColumnUsages(JdbcSchema.UsageType.MEASURE); 293 mit.hasNext();) { 294 295 JdbcSchema.Table.Column.Usage aggUsage = mit.next(); 296 if (aggUsage.rMeasure == avgFactUsage.rMeasure) { 297 avgAggUsage = aggUsage; 298 seenCount++; 299 } else if (aggUsage.rMeasure == sumFactUsage.rMeasure) { 300 sumAggUsage = aggUsage; 301 seenCount++; 302 } 303 } 304 if (seenCount == 1) { 305 if (avgAggUsage != null) { 306 makeMeasure(sumFactUsage, avgAggUsage); 307 } 308 if (sumAggUsage != null) { 309 makeMeasure(avgFactUsage, sumAggUsage); 310 } 311 } 312 } 313 } 314 } 315 316 /** 317 * Here we have the fact usage of either sum or avg and an aggregate usage 318 * of the opposite type. We wish to make a new aggregate usage based 319 * on the existing usage's column of the same type as the fact usage. 320 * 321 * @param factUsage fact usage 322 * @param aggSiblingUsage existing sibling usage 323 */ 324 protected void makeMeasure(final JdbcSchema.Table.Column.Usage factUsage, 325 final JdbcSchema.Table.Column.Usage aggSiblingUsage) { 326 JdbcSchema.Table.Column aggColumn = aggSiblingUsage.getColumn(); 327 328 JdbcSchema.Table.Column.Usage aggUsage = 329 aggColumn.newUsage(JdbcSchema.UsageType.MEASURE); 330 331 aggUsage.setSymbolicName(factUsage.getSymbolicName()); 332 RolapAggregator ra = convertAggregator( 333 aggUsage, 334 factUsage.getAggregator(), 335 aggSiblingUsage.getAggregator()); 336 aggUsage.setAggregator(ra); 337 aggUsage.rMeasure = factUsage.rMeasure; 338 } 339 340 /** 341 * This method creates an aggregate table column measure usage from a fact 342 * table column measure usage. 343 * 344 * @param factUsage 345 * @param aggColumn 346 */ 347 protected void makeMeasure(final JdbcSchema.Table.Column.Usage factUsage, 348 final JdbcSchema.Table.Column aggColumn) { 349 JdbcSchema.Table.Column.Usage aggUsage = 350 aggColumn.newUsage(JdbcSchema.UsageType.MEASURE); 351 352 aggUsage.setSymbolicName(factUsage.getSymbolicName()); 353 RolapAggregator ra = 354 convertAggregator(aggUsage, factUsage.getAggregator()); 355 aggUsage.setAggregator(ra); 356 aggUsage.rMeasure = factUsage.rMeasure; 357 } 358 359 /** 360 * This method determine how may aggregate table column's match the fact 361 * table foreign key column return in the number matched. For each matching 362 * column a foreign key usage is created. 363 */ 364 protected abstract int matchForeignKey(JdbcSchema.Table.Column.Usage factUsage); 365 366 /** 367 * This method checks the foreign key columns. 368 * <p> 369 * For each foreign key column usage in the fact table, determine how many 370 * aggregate table columns match that column usage. If there is more than 371 * one match, then that is an error. If there were no matches, then the 372 * foreign key usage is added to the list of fact column foreign key that 373 * were not in the aggregate table. This list is returned by this method. 374 * <p> 375 * This matches foreign keys that were not "lost" or "collapsed". 376 * 377 * @return list on not seen foreign key column usages 378 */ 379 protected List<JdbcSchema.Table.Column.Usage> checkForeignKeys() { 380 msgRecorder.pushContextName("Recognizer.checkForeignKeys"); 381 382 try { 383 384 List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys = 385 Collections.emptyList(); 386 387 for (Iterator<JdbcSchema.Table.Column.Usage> it = 388 dbFactTable.getColumnUsages(JdbcSchema.UsageType.FOREIGN_KEY); 389 it.hasNext();) { 390 391 JdbcSchema.Table.Column.Usage factUsage = it.next(); 392 393 int matchCount = matchForeignKey(factUsage); 394 395 if (matchCount > 1) { 396 String msg = mres.TooManyMatchingForeignKeyColumns.str( 397 aggTable.getName(), 398 dbFactTable.getName(), 399 matchCount, 400 factUsage.getColumn().getName()); 401 msgRecorder.reportError(msg); 402 403 returnValue = false; 404 405 } else if (matchCount == 0) { 406 if (notSeenForeignKeys.isEmpty()) { 407 notSeenForeignKeys = new ArrayList<JdbcSchema.Table.Column.Usage>(); 408 } 409 notSeenForeignKeys.add(factUsage); 410 } 411 } 412 return notSeenForeignKeys; 413 414 } finally { 415 msgRecorder.popContextName(); 416 } 417 } 418 419 /** 420 * This method identifies those columns in the aggregate table that match 421 * "collapsed" dimension columns. Remember that a collapsed dimension is one 422 * where the higher levels of some hierarchy are columns in the aggregate 423 * table (and all of the lower levels are missing - it has aggregated up to 424 * the first existing level). 425 * <p> 426 * Here, we do not start from the fact table, we iterator over each cube. 427 * For each of the cube's dimensions, the dimension's hirarchies are 428 * iterated over. In turn, each hierarchy's usage is iterated over. 429 * if the hierarchy's usage's foreign key is not in the list of not seen 430 * foreign keys (the notSeenForeignKeys parameter), then that hierarchy is 431 * not considered. If the hierarchy's usage's foreign key is in the not seen 432 * list, then starting with the hierarchy's top level, it is determined if 433 * the combination of hierarchy, hierarchy usage, and level matches an 434 * aggregated table column. If so, then a level usage is created for that 435 * column and the hierarchy's next level is considered and so on until a 436 * for a level an aggregate table column does not match. Then we continue 437 * iterating over the hierarchy usages. 438 * <p> 439 * This check is different. The others mine the fact table usages. This 440 * looks through the fact table's cubes' dimension, hierarchy, 441 * hiearchy usages, levels to match up symbolic names for levels. The other 442 * checks match on "physical" characteristics, the column name; this matches 443 * on "logical" characteristics. 444 * <p> 445 * Note: Levels should not be created for foreign keys that WERE seen. 446 * Currently, this is NOT checked explicitly. For the explicit rules any 447 * extra columns MUST ge declared ignored or one gets an error. 448 * 449 * @param notSeenForeignKeys 450 */ 451 protected void checkLevels(List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys) { 452 453 // These are the factTable that do not appear in the aggTable. 454 // 1) find all cubes with this given factTable 455 // 1) per cube, find all usages with the column as foreign key 456 // 2) for each usage, find dimension and its levels 457 // 3) determine if level columns are represented 458 459 // In generaly, there is only one cube. 460 for (RolapCube cube : findCubes()) { 461 Dimension[] dims = cube.getDimensions(); 462 // start dimensions at 1 (0 is measures) 463 for (int j = 1; j < dims.length; j++) { 464 Dimension dim = dims[j]; 465 // Ok, got dimension. 466 // See if any of the levels exist as columns in the 467 // aggTable. This requires applying a map from: 468 // hierarchyName 469 // levelName 470 // levelColumnName 471 // to each "unassigned" column in the aggTable. 472 // Remember that the rule is if a level does appear, 473 // then all of the higher levels must also appear. 474 String dimName = dim.getName(); 475 476 Hierarchy[] hierarchies = dim.getHierarchies(); 477 for (Hierarchy hierarchy : hierarchies) { 478 HierarchyUsage[] hierarchyUsages = 479 cube.getUsages(hierarchy); 480 for (HierarchyUsage hierarchyUsage : hierarchyUsages) { 481 // Search through the notSeenForeignKeys list 482 // making sure that this HierarchyUsage's 483 // foreign key is not in the list. 484 String foreignKey = hierarchyUsage.getForeignKey(); 485 boolean b = foreignKey == null || 486 inNotSeenForeignKeys( 487 foreignKey, 488 notSeenForeignKeys); 489 if (!b) { 490 // It was not in the not seen list, so ignore 491 continue; 492 } 493 494 495 RolapLevel[] levels = 496 (RolapLevel[]) hierarchy.getLevels(); 497 // If the top level is seen, then one or more 498 // lower levels may appear but there can be no 499 // missing levels between the top level and 500 // lowest level seen. 501 // On the other hand, if the top level is not 502 // seen, then no other levels should be present. 503 mid_level: 504 for (RolapLevel level : levels) { 505 if (level.isAll()) { 506 continue mid_level; 507 } 508 if (matchLevel(hierarchy, hierarchyUsage, level)) { 509 510 continue mid_level; 511 512 } else { 513 // There were no matches, break 514 // For now, do not check lower levels 515 break mid_level; 516 } 517 } 518 } 519 } 520 } 521 } 522 } 523 524 /** 525 * Return true if the foreignKey column name is in the list of not seen 526 * foreign keys. 527 */ 528 boolean inNotSeenForeignKeys( 529 String foreignKey, 530 List<JdbcSchema.Table.Column.Usage> notSeenForeignKeys) 531 { 532 for (JdbcSchema.Table.Column.Usage usage : notSeenForeignKeys) { 533 if (usage.getColumn().getName().equals(foreignKey)) { 534 return true; 535 } 536 } 537 return false; 538 } 539 540 /** 541 * Debug method: Print out not seen foreign key list. 542 * 543 * @param notSeenForeignKeys 544 */ 545 private void printNotSeenForeignKeys(List notSeenForeignKeys) { 546 LOGGER.debug("Recognizer.printNotSeenForeignKeys: " 547 + aggTable.getName()); 548 for (Iterator it = notSeenForeignKeys.iterator(); it.hasNext();) { 549 JdbcSchema.Table.Column.Usage usage = 550 (JdbcSchema.Table.Column.Usage) it.next(); 551 LOGGER.debug(" " + usage.getColumn().getName()); 552 } 553 } 554 555 /** 556 * Here a measure ussage is created and the right join condition is 557 * explicitly supplied. This is needed is when the aggregate table's column 558 * names may not match those found in the RolapStar. 559 * 560 * @param factUsage 561 * @param aggColumn 562 * @param rightJoinConditionColumnName 563 */ 564 protected void makeForeignKey(final JdbcSchema.Table.Column.Usage factUsage, 565 final JdbcSchema.Table.Column aggColumn, 566 final String rightJoinConditionColumnName) { 567 JdbcSchema.Table.Column.Usage aggUsage = 568 aggColumn.newUsage(JdbcSchema.UsageType.FOREIGN_KEY); 569 aggUsage.setSymbolicName("FOREIGN_KEY"); 570 // Extract from RolapStar enough stuff to build 571 // AggStar subtable except the column name of the right join 572 // condition might be different 573 aggUsage.rTable = factUsage.rTable; 574 aggUsage.rightJoinConditionColumnName = rightJoinConditionColumnName; 575 576 aggUsage.rColumn = factUsage.rColumn; 577 } 578 579 /** 580 * Match a aggregate table column given the hierarchy, hierarchy usage, and 581 * rolap level returning true if a match is found. 582 */ 583 protected abstract boolean matchLevel( 584 final Hierarchy hierarchy, 585 final HierarchyUsage hierarchyUsage, 586 final RolapLevel level); 587 588 /** 589 * Make a level column usage. 590 * 591 * <p> Note there is a check in this code. If a given aggregate table 592 * column has already has a level usage, then that usage must all refer to 593 * the same hierarchy usage join table and column name as the one that 594 * calling this method was to create. If there is an existing level usage 595 * for the column and it matches something else, then it is an error. 596 */ 597 protected void makeLevel( 598 final JdbcSchema.Table.Column aggColumn, 599 final Hierarchy hierarchy, 600 final HierarchyUsage hierarchyUsage, 601 final String factColumnName, 602 final String levelColumnName, 603 final String symbolicName) { 604 605 msgRecorder.pushContextName("Recognizer.makeLevel"); 606 607 try { 608 609 if (aggColumn.hasUsage(JdbcSchema.UsageType.LEVEL)) { 610 // The column has at least one usage of level type 611 // make sure we are looking at the 612 // same table and column 613 for (Iterator<JdbcSchema.Table.Column.Usage> uit = 614 aggColumn.getUsages(JdbcSchema.UsageType.LEVEL); 615 uit.hasNext();) { 616 JdbcSchema.Table.Column.Usage aggUsage = uit.next(); 617 618 MondrianDef.Relation rel = hierarchyUsage.getJoinTable(); 619 String cName = levelColumnName; 620 621 if (! aggUsage.relation.equals(rel) || 622 ! aggColumn.column.name.equals(cName)) { 623 624 // this is an error so return 625 String msg = mres.DoubleMatchForLevel.str( 626 aggTable.getName(), 627 dbFactTable.getName(), 628 aggColumn.getName(), 629 aggUsage.relation.toString(), 630 aggColumn.column.name, 631 rel.toString(), 632 cName); 633 msgRecorder.reportError(msg); 634 635 returnValue = false; 636 637 msgRecorder.throwRTException(); 638 } 639 } 640 } else { 641 JdbcSchema.Table.Column.Usage aggUsage = 642 aggColumn.newUsage(JdbcSchema.UsageType.LEVEL); 643 // Cache table and column for the above 644 // check 645 aggUsage.relation = hierarchyUsage.getJoinTable(); 646 aggUsage.joinExp = hierarchyUsage.getJoinExp(); 647 aggUsage.levelColumnName = levelColumnName; 648 649 aggUsage.setSymbolicName(symbolicName); 650 651 String tableAlias; 652 if (aggUsage.joinExp instanceof MondrianDef.Column) { 653 MondrianDef.Column mcolumn = 654 (MondrianDef.Column) aggUsage.joinExp; 655 tableAlias = mcolumn.table; 656 } else { 657 tableAlias = aggUsage.relation.getAlias(); 658 } 659 660 661 RolapStar.Table factTable = star.getFactTable(); 662 RolapStar.Table descTable = factTable.findDescendant(tableAlias); 663 664 if (descTable == null) { 665 // TODO: what to do here??? 666 StringBuilder buf = new StringBuilder(256); 667 buf.append("descendant table is null for factTable="); 668 buf.append(factTable.getAlias()); 669 buf.append(", tableAlias="); 670 buf.append(tableAlias); 671 msgRecorder.reportError(buf.toString()); 672 673 returnValue = false; 674 675 msgRecorder.throwRTException(); 676 } 677 678 RolapStar.Column rc = descTable.lookupColumn(factColumnName); 679 680 if (rc == null) { 681 rc = lookupInChildren(descTable, factColumnName); 682 683 } 684 if (rc == null) { 685 StringBuilder buf = new StringBuilder(256); 686 buf.append("Rolap.Column not found (null) for tableAlias="); 687 buf.append(tableAlias); 688 buf.append(", factColumnName="); 689 buf.append(factColumnName); 690 buf.append(", levelColumnName="); 691 buf.append(levelColumnName); 692 buf.append(", symbolicName="); 693 buf.append(symbolicName); 694 msgRecorder.reportError(buf.toString()); 695 696 returnValue = false; 697 698 msgRecorder.throwRTException(); 699 } else { 700 aggUsage.rColumn = rc; 701 } 702 } 703 } finally { 704 msgRecorder.popContextName(); 705 } 706 } 707 708 protected RolapStar.Column lookupInChildren( 709 final RolapStar.Table table, 710 final String factColumnName) 711 { 712 // This can happen if we are looking at a collapsed dimension 713 // table, and the collapsed dimension in question in the 714 // fact table is a snowflake (not just a star), so we 715 // must look deeper... 716 for (RolapStar.Table child : table.getChildren()) { 717 RolapStar.Column rc = child.lookupColumn(factColumnName); 718 if (rc != null) { 719 return rc; 720 } else { 721 rc = lookupInChildren(child, factColumnName); 722 if (rc != null) { 723 return rc; 724 } 725 } 726 } 727 return null; 728 } 729 730 731 // Question: what if foreign key is seen, but there are also level 732 // columns - is this at least is a warning. 733 734 735 /** 736 * If everything is ok, issue warning for each aggTable column 737 * that has not been identified as a FACT_COLUMN, MEASURE_COLUMN or 738 * LEVEL_COLUMN. 739 */ 740 protected void checkUnusedColumns() { 741 msgRecorder.pushContextName("Recognizer.checkUnusedColumns"); 742 // Collection of messages for unused columns, sorted by column name 743 // so that tests are deterministic. 744 SortedMap<String, String> unusedColumnMsgs = 745 new TreeMap<String, String>(); 746 for (JdbcSchema.Table.Column aggColumn : aggTable.getColumns()) { 747 if (! aggColumn.hasUsage()) { 748 String msg = mres.AggUnknownColumn.str( 749 aggTable.getName(), 750 dbFactTable.getName(), 751 aggColumn.getName()); 752 unusedColumnMsgs.put(aggColumn.getName(), msg); 753 } 754 } 755 for (String msg : unusedColumnMsgs.values()) { 756 msgRecorder.reportWarning(msg); 757 } 758 msgRecorder.popContextName(); 759 } 760 761 /** 762 * Figure out what aggregator should be associated with a column usage. 763 * Generally, this aggregator is simply the RolapAggregator returned by 764 * calling the getRollup() method of the fact table column's 765 * RolapAggregator. But in the case that the fact table column's 766 * RolapAggregator is the "Avg" aggregator, then the special 767 * RolapAggregator.AvgFromSum is used. 768 * <p> 769 * Note: this code assumes that the aggregate table does not have an 770 * explicit average aggregation column. 771 * 772 * @param aggUsage 773 * @param factAgg 774 */ 775 protected RolapAggregator convertAggregator( 776 final JdbcSchema.Table.Column.Usage aggUsage, 777 final RolapAggregator factAgg) { 778 779 // NOTE: This assumes that the aggregate table does not have an explicit 780 // average column. 781 if (factAgg == RolapAggregator.Avg) { 782 String columnExpr = getFactCountExpr(aggUsage); 783 return new RolapAggregator.AvgFromSum(columnExpr); 784 } else if (factAgg == RolapAggregator.DistinctCount) { 785 //return RolapAggregator.Count; 786 return RolapAggregator.DistinctCount; 787 } else { 788 return factAgg; 789 } 790 } 791 792 /** 793 * The method chooses a special aggregator for the aggregate table column's 794 * usage. 795 * <pre> 796 * If the fact table column's aggregator was "Avg": 797 * then if the sibling aggregator was "Avg": 798 * the new aggregator is RolapAggregator.AvgFromAvg 799 * else if the sibling aggregator was "Sum": 800 * the new aggregator is RolapAggregator.AvgFromSum 801 * else if the fact table column's aggregator was "Sum": 802 * if the sibling aggregator was "Avg": 803 * the new aggregator is RolapAggregator.SumFromAvg 804 * </pre> 805 * Note that there is no SumFromSum since that is not a special case 806 * requiring a special aggregator. 807 * <p> 808 * if no new aggregator was selected, then the fact table's aggregator 809 * rollup aggregator is used. 810 * 811 * @param aggUsage 812 * @param factAgg 813 * @param siblingAgg 814 */ 815 protected RolapAggregator convertAggregator( 816 final JdbcSchema.Table.Column.Usage aggUsage, 817 final RolapAggregator factAgg, 818 final RolapAggregator siblingAgg) { 819 820 msgRecorder.pushContextName("Recognizer.convertAggregator"); 821 RolapAggregator rollupAgg = null; 822 823 String columnExpr = getFactCountExpr(aggUsage); 824 if (factAgg == RolapAggregator.Avg) { 825 if (siblingAgg == RolapAggregator.Avg) { 826 rollupAgg = new RolapAggregator.AvgFromAvg(columnExpr); 827 } else if (siblingAgg == RolapAggregator.Sum) { 828 rollupAgg = new RolapAggregator.AvgFromSum(columnExpr); 829 } 830 } else if (factAgg == RolapAggregator.Sum) { 831 if (siblingAgg == RolapAggregator.Avg) { 832 rollupAgg = new RolapAggregator.SumFromAvg(columnExpr); 833 } else if (siblingAgg instanceof RolapAggregator.AvgFromAvg) { 834 // needed for BUG_1541077.testTotalAmount 835 rollupAgg = new RolapAggregator.SumFromAvg(columnExpr); 836 } 837 } 838 839 if (rollupAgg == null) { 840 rollupAgg = (RolapAggregator) factAgg.getRollup(); 841 } 842 843 if (rollupAgg == null) { 844 String msg = mres.NoAggregatorFound.str( 845 aggUsage.getSymbolicName(), 846 factAgg.getName(), 847 siblingAgg.getName()); 848 msgRecorder.reportError(msg); 849 } 850 851 msgRecorder.popContextName(); 852 return rollupAgg; 853 } 854 855 /** 856 * Given an aggregate table column usage, find the column name of the 857 * table's fact count column usage. 858 * 859 * @param aggUsage Aggregate table column usage 860 * @return The name of the column which holds the fact count. 861 */ 862 private String getFactCountExpr(final JdbcSchema.Table.Column.Usage aggUsage) { 863 // get the fact count column name. 864 JdbcSchema.Table aggTable = aggUsage.getColumn().getTable(); 865 866 // iterator over fact count usages - in the end there can be only one!! 867 Iterator<JdbcSchema.Table.Column.Usage> it = 868 aggTable.getColumnUsages(JdbcSchema.UsageType.FACT_COUNT); 869 it.hasNext(); 870 JdbcSchema.Table.Column.Usage usage = it.next(); 871 872 // get the columns name 873 String factCountColumnName = usage.getColumn().getName(); 874 String tableName = aggTable.getName(); 875 876 // we want the fact count expression 877 MondrianDef.Column column = 878 new MondrianDef.Column(tableName, factCountColumnName); 879 SqlQuery sqlQuery = star.getSqlQuery(); 880 return column.getExpression(sqlQuery); 881 } 882 883 /** 884 * Finds all cubes that use this fact table. 885 */ 886 protected List<RolapCube> findCubes() { 887 String name = dbFactTable.getName(); 888 889 List<RolapCube> list = new ArrayList<RolapCube>(); 890 RolapSchema schema = star.getSchema(); 891 for (RolapCube cube : schema.getCubeList()) { 892 if (cube.isVirtual()) { 893 continue; 894 } 895 RolapStar cubeStar = cube.getStar(); 896 String factTableName = cubeStar.getFactTable().getAlias(); 897 if (name.equals(factTableName)) { 898 list.add(cube); 899 } 900 } 901 return list; 902 } 903 904 /** 905 * Given a {@link mondrian.olap.MondrianDef.Expression}, returns 906 * the associated column name. 907 * 908 * <p>Note: if the {@link mondrian.olap.MondrianDef.Expression} is 909 * not a {@link mondrian.olap.MondrianDef.Column} or {@link 910 * mondrian.olap.MondrianDef.KeyExpression}, returns null. This 911 * will result in an error. 912 */ 913 protected String getColumnName(MondrianDef.Expression expr) { 914 msgRecorder.pushContextName("Recognizer.getColumnName"); 915 916 try { 917 if (expr instanceof MondrianDef.Column) { 918 MondrianDef.Column column = (MondrianDef.Column) expr; 919 return column.getColumnName(); 920 } else if (expr instanceof MondrianDef.KeyExpression) { 921 MondrianDef.KeyExpression key = (MondrianDef.KeyExpression) expr; 922 return key.toString(); 923 } 924 925 String msg = mres.NoColumnNameFromExpression.str( 926 expr.toString()); 927 msgRecorder.reportError(msg); 928 929 return null; 930 931 } finally { 932 msgRecorder.popContextName(); 933 } 934 } 935 } 936 937 // End Recognizer.java