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