Skip to content

Commit

Permalink
Implement hierarchy consistency check for QueryDimension inclusions i…
Browse files Browse the repository at this point in the history
…n the Query Model. Useful especially in combination with Level selections

git-svn-id: https://olap4j.svn.sourceforge.net/svnroot/olap4j/trunk@456 c6a108a4-781c-0410-a6c6-c2d559e19af0
  • Loading branch information
pstoellberger committed Jun 24, 2011
1 parent 57c37b8 commit ce511f6
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 3 deletions.
166 changes: 164 additions & 2 deletions src/org/olap4j/query/Olap4jNodeConverter.java
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -319,10 +337,52 @@ private static AxisNode toOlap4j(QueryAxis axis) {
private static List<ParseTreeNode> toOlap4j(QueryDimension dimension) {
// Let's build a first list of included members.
List<ParseTreeNode> includeList = new ArrayList<ParseTreeNode>();
Map<Integer, List<ParseTreeNode>> levelNodes =
new HashMap<Integer, List<ParseTreeNode>>();
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<ParseTreeNode> nodes = new ArrayList<ParseTreeNode>();
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<ParseTreeNode> orderedList = new ArrayList<ParseTreeNode>();
Expand Down Expand Up @@ -508,6 +568,108 @@ private static List<AxisNode> toOlap4j(List<QueryAxis> 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<Integer, Level> levels = new HashMap<Integer, Level>();
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<CallNode> inConditions = new ArrayList<CallNode>();
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 <ParseTreeNode> ancestorList =
new ArrayList<ParseTreeNode>();

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
Expand Down
23 changes: 23 additions & 0 deletions src/org/olap4j/query/QueryDimension.java
Expand Up @@ -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();
Expand Down Expand Up @@ -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.
*
* <p>It uses the MDX function FILTER() in combination with
* ANCESTOR() to produce a set like:<br /><br />
* {[Time].[1997]}, <br />
* 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<Selection> {
private final List<Selection> list = new ArrayList<Selection>();

Expand Down
133 changes: 132 additions & 1 deletion testsrc/org/olap4j/OlapTest.java
Expand Up @@ -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;

Expand Down Expand Up @@ -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<Level> 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");
Expand Down

0 comments on commit ce511f6

Please sign in to comment.