diff --git a/src/org/olap4j/query/Olap4jNodeConverter.java b/src/org/olap4j/query/Olap4jNodeConverter.java index 4e7a066..6a05f5e 100644 --- a/src/org/olap4j/query/Olap4jNodeConverter.java +++ b/src/org/olap4j/query/Olap4jNodeConverter.java @@ -10,8 +10,11 @@ package org.olap4j.query; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.olap4j.Axis; import org.olap4j.mdx.AxisNode; @@ -161,6 +164,21 @@ private static void generateUnionsRecursively( for (Selection sel : qDim.getInclusions()) { ParseTreeNode selectionNode = toOlap4j(sel); + // If a the querydimension should return only hierarchy + // consistent results, generate a filter that checks + // inclusions for ancestors in higher levels + if (qDim.isHierarchyConsistent() + && qDim.getInclusions().size() > 1) + { + Integer currentDepth = null; + if (sel.getRootElement() instanceof Member) { + currentDepth = ((Member)sel.getRootElement()).getDepth(); + } else if (sel.getRootElement() instanceof Level) { + currentDepth = ((Level)sel.getRootElement()).getDepth(); + } + selectionNode = + toHierarchyConsistentNode(selectionNode, currentDepth, qDim); + } // If a sort Order was specified for this dimension // apply it for this inclusion if (qDim.getSortOrder() != null) { @@ -319,10 +337,52 @@ private static AxisNode toOlap4j(QueryAxis axis) { private static List toOlap4j(QueryDimension dimension) { // Let's build a first list of included members. List includeList = new ArrayList(); + Map> levelNodes = + new HashMap>(); for (Selection selection : dimension.getInclusions()) { - includeList.add(toOlap4j(selection)); - } + ParseTreeNode selectionNode = toOlap4j(selection); + // If a the querydimension should return only hierarchy + // consistent results, generate a filter that checks + // inclusions for ancestors in higher levels + if (dimension.isHierarchyConsistent() + && dimension.getInclusions().size() > 1) + { + Integer curdepth = 0; + if (selection.getRootElement() instanceof Member) { + curdepth = ((Member)selection.getRootElement()).getDepth(); + } else if (selection.getRootElement() instanceof Level) { + curdepth = ((Level)selection.getRootElement()).getDepth(); + } + if (levelNodes.get(curdepth) != null) { + levelNodes.get(curdepth).add(selectionNode); + } else { + List nodes = new ArrayList(); + nodes.add(selectionNode); + levelNodes.put(curdepth, nodes); + } + } else { + includeList.add(selectionNode); + } + } + if (dimension.isHierarchyConsistent() + && dimension.getInclusions().size() > 1) + { + Integer levelDepths[] = + levelNodes.keySet() + .toArray(new Integer[levelNodes.keySet().size()]); + + Arrays.sort(levelDepths); + + for (Integer depth : levelDepths) { + ParseTreeNode levelNode = + generateListSetCall(levelNodes.get(depth)); + + levelNode = + toHierarchyConsistentNode(levelNode, depth, dimension); + includeList.add(levelNode); + } + } // If a sort order was specified, we need to wrap the inclusions in an // Order() mdx function. List orderedList = new ArrayList(); @@ -508,6 +568,108 @@ private static List toOlap4j(List axes) { } return axisList; } + + private static ParseTreeNode toHierarchyConsistentNode( + ParseTreeNode selectionNode, + Integer maxDepth, + QueryDimension qDim) + { + // If a the querydimension should return only hierarchy + // consistent results, generate a filter that checks + // inclusions for ancestors in higher levels + if (qDim.getInclusions().size() > 1) { + CallNode currentMemberNode = + new CallNode( + null, + "CurrentMember", + Syntax.Property, + new DimensionNode(null, qDim.getDimension())); + + Map levels = new HashMap(); + for (Selection s : qDim.getInclusions()) { + if (s.getRootElement() instanceof Member) { + Integer d = ((Member)s.getRootElement()).getDepth(); + if (!levels.containsKey(d)) { + Level lvl = ((Member)s.getRootElement()).getLevel(); + levels.put(d, lvl); + } + } else if (s.getRootElement() instanceof Level) { + Integer d = ((Level)s.getRootElement()).getDepth(); + if (!levels.containsKey(d)) { + Level lvl = ((Level)s.getRootElement()); + levels.put(d, lvl); + } + } + } + Integer levelDepths[] = + levels.keySet() + .toArray(new Integer[levels.keySet().size()]); + + Arrays.sort(levelDepths); + + List inConditions = new ArrayList(); + for (Integer i = 0; i < levelDepths.length - 1; i++) { + Level currentLevel = levels.get(levelDepths[i]); + if (levelDepths[i] < maxDepth + && currentLevel.getLevelType() != Level.Type.ALL) + { + CallNode ancestorNode = + new CallNode( + null, + "Ancestor", + Syntax.Function, + currentMemberNode, + new LevelNode(null, currentLevel)); + + List ancestorList = + new ArrayList(); + + for (Selection anc : qDim.getInclusions()) { + if (anc.getRootElement() instanceof Member) { + Level l = ((Member)anc.getRootElement()).getLevel(); + if (l.equals(levels.get(levelDepths[i]))) { + ancestorList.add(anc.visit()); + } + } else if (anc.getRootElement() instanceof Level) { + Level l = ((Level)anc.getRootElement()); + if (l.equals(levels.get(levelDepths[i]))) { + ancestorList.add(anc.visit()); + } + } + } + CallNode ancestorSet = generateListSetCall(ancestorList); + CallNode inClause = new CallNode( + null, + "IN", + Syntax.Infix, + ancestorNode, + ancestorSet); + inConditions.add(inClause); + } + } + if (inConditions.size() > 0) { + CallNode chainedIn = inConditions.get(0); + if (inConditions.size() > 1) { + for (int c = 1;c < inConditions.size();c++) { + chainedIn = new CallNode( + null, + "AND", + Syntax.Infix, + chainedIn, + inConditions.get(c)); + } + } + + return new CallNode( + null, + "Filter", + Syntax.Function, + generateSetCall(selectionNode), + chainedIn); + } + } + return selectionNode; + } } // End Olap4jNodeConverter.java diff --git a/src/org/olap4j/query/QueryDimension.java b/src/org/olap4j/query/QueryDimension.java index f841600..0e1e1be 100644 --- a/src/org/olap4j/query/QueryDimension.java +++ b/src/org/olap4j/query/QueryDimension.java @@ -43,6 +43,7 @@ public class QueryDimension extends QueryNodeImpl { protected Dimension dimension; private SortOrder sortOrder = null; private HierarchizeMode hierarchizeMode = null; + private boolean hierarchyConsistent = false; public QueryDimension(Query query, Dimension dimension) { super(); @@ -527,6 +528,28 @@ public void clearHierarchizeMode() { this.hierarchizeMode = null; } + /** + * Tells the QueryDimension not to keep a consistent hierarchy + * within the inclusions when the mdx is generated. + * Only members whose Ancestors are included will be included. + * + *

It uses the MDX function FILTER() in combination with + * ANCESTOR() to produce a set like:

+ * {[Time].[1997]},
+ * Filter({{[Time].[Quarter].Members}}, + * (Ancestor([Time].CurrentMember, [Time].[Year]) IN {[Time].[1997]})) + */ + public void setHierarchyConsistent(boolean consistent) { + this.hierarchyConsistent = consistent; + } + + /** + * Tells the QueryDimension not to keep a consistent hierarchy + */ + public boolean isHierarchyConsistent() { + return this.hierarchyConsistent; + } + private class SelectionList extends AbstractList { private final List list = new ArrayList(); diff --git a/testsrc/org/olap4j/OlapTest.java b/testsrc/org/olap4j/OlapTest.java index a5158dd..2e55c8e 100644 --- a/testsrc/org/olap4j/OlapTest.java +++ b/testsrc/org/olap4j/OlapTest.java @@ -15,7 +15,6 @@ import org.olap4j.query.QueryDimension.HierarchizeMode; import org.olap4j.query.Selection.Operator; import org.olap4j.test.TestContext; - import java.sql.Connection; import java.sql.DriverManager; @@ -1440,6 +1439,138 @@ public void testCompoundFilter() { fail(); } } + public void testHierarchyConsistency() { + try { + Cube cube = getFoodmartCube("Sales"); + if (cube == null) { + fail("Could not find Sales cube"); + } + // Setup a base query. + Query query = new Query("my query", cube); + QueryDimension productDimension = query.getDimension("Product"); + productDimension.setHierarchyConsistent(true); + NamedList productLevels = + productDimension.getDimension() + .getDefaultHierarchy().getLevels(); + + Level productLevel = productLevels.get("Product Category"); + productDimension.include(productLevel); + + productDimension.include( + Selection.Operator.MEMBER, + nameList("Product", "Food", "Deli")); + productDimension.include( + Selection.Operator.MEMBER, + nameList("Product", "Food", "Dairy")); + productDimension.include( + Selection.Operator.MEMBER, + nameList("Product", "Product Family", "Food")); + productDimension.include( + Selection.Operator.MEMBER, + nameList("Product", "All Products")); + QueryDimension timeDimension = query.getDimension("Time"); + timeDimension.setHierarchyConsistent(true); + + timeDimension.include(nameList("Time", "Year", "1997", "Q3", "7")); + timeDimension.include(nameList("Time", "Year", "1997", "Q4", "11")); + + timeDimension.include(nameList("Time", "Year", "1997")); + QueryDimension measuresDimension = query.getDimension("Measures"); + measuresDimension.include(nameList("Measures", "Sales Count")); + + query.getAxis(Axis.COLUMNS).addDimension(productDimension); + query.getAxis(Axis.ROWS).addDimension(timeDimension); + + query.validate(); + + // Validate the generated MDX + String mdxString = query.getSelect().toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{{[Product].[All Products]}, {[Product].[Food]}, Filter({{[Product].[Food].[Deli], [Product].[Food].[Dairy]}}, (Ancestor([Product].CurrentMember, [Product].[Product Family]) IN {[Product].[Food]})), Filter({{[Product].[Product Category].Members}}, ((Ancestor([Product].CurrentMember, [Product].[Product Family]) IN {[Product].[Food]}) AND (Ancestor([Product].CurrentMember, [Product].[Product Department]) IN {[Product].[Food].[Deli], [Product].[Food].[Dairy]})))} ON COLUMNS,\n" + + "{{[Time].[1997]}, Filter({{[Time].[1997].[Q3].[7], [Time].[1997].[Q4].[11]}}, (Ancestor([Time].CurrentMember, [Time].[Year]) IN {[Time].[1997]}))} ON ROWS\n" + + "FROM [Sales]", + mdxString); + + // Validate the returned results + CellSet results = query.execute(); + String resultsString = TestContext.toString(results); + TestContext.assertEqualsVerbose( + "Axis #0:\n" + + "{}\n" + + "Axis #1:\n" + + "{[Product].[All Products]}\n" + + "{[Product].[Food]}\n" + + "{[Product].[Food].[Deli]}\n" + + "{[Product].[Food].[Dairy]}\n" + + "{[Product].[Food].[Dairy].[Dairy]}\n" + + "{[Product].[Food].[Deli].[Meat]}\n" + + "{[Product].[Food].[Deli].[Side Dishes]}\n" + + "Axis #2:\n" + + "{[Time].[1997]}\n" + + "{[Time].[1997].[Q3].[7]}\n" + + "{[Time].[1997].[Q4].[11]}\n" + + "Row #0: 266,773\n" + + "Row #0: 191,940\n" + + "Row #0: 12,037\n" + + "Row #0: 12,885\n" + + "Row #0: 12,885\n" + + "Row #0: 9,433\n" + + "Row #0: 2,604\n" + + "Row #1: 23,763\n" + + "Row #1: 17,036\n" + + "Row #1: 1,050\n" + + "Row #1: 1,229\n" + + "Row #1: 1,229\n" + + "Row #1: 847\n" + + "Row #1: 203\n" + + "Row #2: 25,270\n" + + "Row #2: 18,278\n" + + "Row #2: 1,312\n" + + "Row #2: 1,232\n" + + "Row #2: 1,232\n" + + "Row #2: 1,033\n" + + "Row #2: 279\n", + resultsString); + query.validate(); + + query.getAxis(Axis.ROWS).addDimension(measuresDimension); + productDimension.clearInclusions(); + productDimension.include( + Selection.Operator.MEMBER, + nameList("Product", "Product Family", "Food")); + + // Validate the generated MDX + String mdxString2 = query.getSelect().toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{[Product].[Food]} ON COLUMNS,\n" + + "Hierarchize(Union(CrossJoin(Filter({[Time].[1997].[Q3].[7]}, (Ancestor([Time].CurrentMember, [Time].[Year]) IN {[Time].[1997]})), {[Measures].[Sales Count]}), Union(CrossJoin(Filter({[Time].[1997].[Q4].[11]}, (Ancestor([Time].CurrentMember, [Time].[Year]) IN {[Time].[1997]})), {[Measures].[Sales Count]}), CrossJoin({[Time].[1997]}, {[Measures].[Sales Count]})))) ON ROWS\n" + + "FROM [Sales]", + mdxString2); + + // Validate the returned results + CellSet results2 = query.execute(); + String resultsString2 = TestContext.toString(results2); + TestContext.assertEqualsVerbose( + "Axis #0:\n" + + "{}\n" + + "Axis #1:\n" + + "{[Product].[Food]}\n" + + "Axis #2:\n" + + "{[Time].[1997], [Measures].[Sales Count]}\n" + + "{[Time].[1997].[Q3].[7], [Measures].[Sales Count]}\n" + + "{[Time].[1997].[Q4].[11], [Measures].[Sales Count]}\n" + + "Row #0: 62,445\n" + + "Row #1: 5,552\n" + + "Row #2: 5,944\n", + resultsString2); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + public void testNonMandatoryQueryAxis() { try { Cube cube = getFoodmartCube("Sales");