From a236eb091f2081654c4287c6f9111dd2f1838635 Mon Sep 17 00:00:00 2001 From: Will Gorman Date: Mon, 8 Mar 2010 04:27:50 +0000 Subject: [PATCH] added selection context concept and a new union algorithm when selecting multiple dimensions on an axis to support drill down git-svn-id: https://olap4j.svn.sourceforge.net/svnroot/olap4j/trunk@304 c6a108a4-781c-0410-a6c6-c2d559e19af0 --- src/org/olap4j/query/Olap4jNodeConverter.java | 136 ++++++++++---- src/org/olap4j/query/QueryDimension.java | 75 ++++++-- src/org/olap4j/query/Selection.java | 13 ++ src/org/olap4j/query/SelectionImpl.java | 19 ++ testsrc/org/olap4j/OlapTest.java | 168 ++++++++++++++---- 5 files changed, 326 insertions(+), 85 deletions(-) diff --git a/src/org/olap4j/query/Olap4jNodeConverter.java b/src/org/olap4j/query/Olap4jNodeConverter.java index 165f067..614dc3d 100644 --- a/src/org/olap4j/query/Olap4jNodeConverter.java +++ b/src/org/olap4j/query/Olap4jNodeConverter.java @@ -13,6 +13,8 @@ import java.util.Collections; import java.util.List; +import mondrian.olap.Util; + import org.olap4j.Axis; import org.olap4j.mdx.AxisNode; import org.olap4j.mdx.CallNode; @@ -82,27 +84,93 @@ private static CallNode generateListTupleCall(List cnodes) { Syntax.Parentheses, cnodes); } - - protected static CallNode getMemberSet(QueryDimension dimension) { - return - new CallNode( - null, - "{}", - Syntax.Braces, - toOlap4j(dimension)); + + protected static CallNode generateCrossJoin(List selections) { + ParseTreeNode sel1 = toOlap4j(selections.remove(0)); + if (sel1 instanceof MemberNode) { + sel1 = generateSetCall(sel1); + } + if (selections.size() == 1) { + ParseTreeNode sel2 = toOlap4j(selections.get(0)); + if (sel2 instanceof MemberNode) { + sel2 = generateSetCall(sel2); + } + + return new CallNode( null, "CrossJoin", Syntax.Function, sel1, sel2); + } else { + return new CallNode( null, "CrossJoin", Syntax.Function, sel1, generateCrossJoin(selections)); + } } + + protected static CallNode generateUnion(List> unions) { + if (unions.size() > 2) { + List first = unions.remove(0); + return new CallNode( null, "Union", Syntax.Function, generateCrossJoin(first), generateUnion(unions)); + } else { + return new CallNode( null, "Union", Syntax.Function, generateCrossJoin(unions.get(0)), generateCrossJoin(unions.get(1))); + } + } + + protected static CallNode generateHierarchizeUnion(List> unions) { + return new CallNode(null, "Hierarchize", Syntax.Function, + generateUnion(unions) + ); + } + + /** + * + * Algorithm: + * - generate all combinations of dimension groups + * - skip the selection if has a context + * - for all the selections with context, resolve them last + * - union all combinations + */ + private static void generateUnionsRecursively(QueryAxis axis, int dim, List curr, List> unions, List selsWithContext, List> contextUnions) { + for (Selection sel : axis.getDimensions().get(dim).getInclusions()) { - protected static CallNode crossJoin( - QueryDimension dim1, - QueryDimension dim2) - { - return - new CallNode( - null, - "CrossJoin", - Syntax.Function, - getMemberSet(dim1), - getMemberSet(dim2)); + if (sel.getSelectionContext() != null && sel.getSelectionContext().size() > 0) { + // selections that have a context are treated differently than the + // rest of the MDX generation + if (!selsWithContext.contains(sel)) { + ArrayList sels = new ArrayList(); + for (int i = 0; i < axis.getDimensions().size(); i++) { + if (dim == i) { + sels.add(sel); + } else { + // return the selections in the correct dimensional order + QueryDimension dimension = axis.getDimensions().get(i); + boolean found = false; + for (Selection selection : sel.getSelectionContext()) { + if (selection.getDimension().equals(dimension.getDimension())) { + sels.add(selection); + found = true; + } + } + if (!found) { + // add the first selection of the dimension + if (dimension.getInclusions().size() > 0) { + sels.add(dimension.getInclusions().get(0)); + } + } + } + } + contextUnions.add(sels); + selsWithContext.add(sel); + } + } else { + List ncurr = new ArrayList(); + if (curr != null) { + ncurr.addAll(curr); + } + ncurr.add(sel); + if (dim == axis.getDimensions().size() - 1) { + // last dimension + unions.add(ncurr); + } else { + generateUnionsRecursively(axis, dim + 1, ncurr, unions, selsWithContext, contextUnions); + } + } + } } /* @@ -131,26 +199,18 @@ private static AxisNode toOlap4j(QueryAxis axis) { QueryDimension dimension = axis.getDimensions().get(0); List members = toOlap4j(dimension); callNode = generateListSetCall(members); - } else if (numDimensions == 2) { - callNode = - crossJoin( - axis.getDimensions().get(0), - axis.getDimensions().get(1)); } else { - // need a longer crossjoin - // start from the back of the list; - List dims = axis.getDimensions(); - callNode = getMemberSet(dims.get(dims.size() - 1)); - for (int i = dims.size() - 2; i >= 0; i--) { - CallNode memberSet = getMemberSet(dims.get(i)); - callNode = - new CallNode( - null, - "CrossJoin", - Syntax.Function, - memberSet, - callNode); - } + // generate union sets of selections in each dimension + List> unions = new ArrayList>(); + List selsWithContext = new ArrayList(); + List> contextUnions = new ArrayList>(); + generateUnionsRecursively(axis, 0, null, unions, selsWithContext, contextUnions); + unions.addAll(contextUnions); + if (unions.size() > 1) { + callNode = generateHierarchizeUnion(unions); + } else { + callNode = generateCrossJoin(unions.get(0)); + } } // We might need to sort the whole axis. diff --git a/src/org/olap4j/query/QueryDimension.java b/src/org/olap4j/query/QueryDimension.java index cc8f0a7..f668619 100644 --- a/src/org/olap4j/query/QueryDimension.java +++ b/src/org/olap4j/query/QueryDimension.java @@ -111,10 +111,14 @@ public void clearSelection() { * @throws OlapException If no member corresponding to the supplied * name parts could be resolved in the cube. */ - public void include(String... nameParts) throws OlapException { - this.include(Selection.Operator.MEMBER, nameParts); + public Selection include(String... nameParts) throws OlapException { + return this.include(Selection.Operator.MEMBER, nameParts); } + public Selection createSelection(String... nameParts) throws OlapException { + return this.createSelection(Selection.Operator.MEMBER, nameParts); + } + /** * Selects members and includes them in the query. *

This method selects and includes a member along with it's @@ -126,7 +130,23 @@ public void include(String... nameParts) throws OlapException { * @throws OlapException If no member corresponding to the supplied * name parts could be resolved in the cube. */ - public void include( + public Selection include( + Selection.Operator operator, + String... nameParts) throws OlapException + { + Member member = this.getQuery().getCube().lookupMember(nameParts); + if (member == null) { + throw new OlapException( + "Unable to find a member with name " + + Olap4jUtil.stringArrayToString(nameParts)); + } else { + return this.include( + operator, + member); + } + } + + public Selection createSelection( Selection.Operator operator, String... nameParts) throws OlapException { @@ -136,7 +156,7 @@ public void include( "Unable to find a member with name " + Olap4jUtil.stringArrayToString(nameParts)); } else { - this.include( + return this.createSelection( operator, member); } @@ -148,8 +168,34 @@ public void include( * {@link Selection.Operator#MEMBER} selection operator. * @param member The member to select and include in the query. */ - public void include(Member member) { - include(Selection.Operator.MEMBER, member); + public Selection include(Member member) { + return include(Selection.Operator.MEMBER, member); + } + + public Selection createSelection(Member member) { + return createSelection(Selection.Operator.MEMBER, member); + } + + /** + * Selects members and includes them in the query. + *

This method selects and includes a member along with it's + * relatives, depending on the supplied {@link Selection.Operator} + * operator. + * @param operator Selection operator that defines what relatives of the + * supplied member name to include along. + * @param member Root member to select and include. + */ + public Selection createSelection( + Selection.Operator operator, + Member member) + { + if (member.getDimension().equals(this.dimension)) { + Selection selection = + query.getSelectionFactory().createMemberSelection( + member, operator); + return selection; + } + return null; } /** @@ -161,7 +207,7 @@ public void include(Member member) { * supplied member name to include along. * @param member Root member to select and include. */ - public void include( + public Selection include( Selection.Operator operator, Member member) { @@ -170,7 +216,9 @@ public void include( query.getSelectionFactory().createMemberSelection( member, operator); this.include(selection); + return selection; } + return null; } /** @@ -437,10 +485,11 @@ public void clearSort() { } /** - * Returns the current mode of hierarchyzation, or null - * if no hierarchyzation is currently performed. - * @return Either a hierarchyzation mode value or null - * if no hierarchyzation is currently performed. + * Returns the current mode of hierarchization, or null + * if no hierarchization is currently performed. + * This capability is only available when a single dimension is selected on an axis. + * @return Either a hierarchization mode value or null + * if no hierarchization is currently performed. */ public HierarchizeMode getHierarchizeMode() { return hierarchizeMode; @@ -451,6 +500,7 @@ public HierarchizeMode getHierarchizeMode() { * QueryDimension. *

The dimension inclusions will be wrapped in an MDX Hierarchize * function call. + * This capability is only available when a single dimension is selected on an axis. * @param hierarchizeMode If parents should be included before or after * their children. (Equivalent to the POST/PRE MDX literal for the * Hierarchize() function) @@ -461,7 +511,8 @@ public void setHierarchizeMode(HierarchizeMode hierarchizeMode) { } /** - * Tells the QueryDimension not to hierarchyze it's included selections. + * Tells the QueryDimension not to hierarchize it's included selections. + * This capability is only available when a single dimension is selected on an axis. */ public void clearHierarchizeMode() { this.hierarchizeMode = null; diff --git a/src/org/olap4j/query/Selection.java b/src/org/olap4j/query/Selection.java index e1f203c..534b28b 100644 --- a/src/org/olap4j/query/Selection.java +++ b/src/org/olap4j/query/Selection.java @@ -9,6 +9,8 @@ */ package org.olap4j.query; +import java.util.List; + import org.olap4j.metadata.Dimension; import org.olap4j.metadata.Member; @@ -43,6 +45,17 @@ public interface Selection extends QueryNode { String getHierarchyName(); + /** + * The selection context includes selections from other dimensions that help determine the entire + * context of a selection, so drill down is possible. + * @return list of selections + */ + List getSelectionContext(); + + void addContext(Selection selection); + + void removeContext(Selection selection); + String getLevelName(); Operator getOperator(); diff --git a/src/org/olap4j/query/SelectionImpl.java b/src/org/olap4j/query/SelectionImpl.java index 0c41abf..d21d3ec 100644 --- a/src/org/olap4j/query/SelectionImpl.java +++ b/src/org/olap4j/query/SelectionImpl.java @@ -9,6 +9,9 @@ */ package org.olap4j.query; +import java.util.ArrayList; +import java.util.List; + import org.olap4j.metadata.Dimension; import org.olap4j.metadata.Member; @@ -28,6 +31,7 @@ class SelectionImpl extends QueryNodeImpl implements Selection { protected String memberName; protected Dimension dimension; protected Operator operator = Operator.MEMBER; + protected List selectionContext; /** * Creates a SelectionImpl. @@ -107,6 +111,21 @@ public void setOperator(Operator operator) { void tearDown() { } + + public List getSelectionContext() { + return selectionContext; + } + + public void addContext(Selection selection) { + if (selectionContext == null) { + selectionContext = new ArrayList(); + } + selectionContext.add(selection); + } + + public void removeContext(Selection selection) { + selectionContext.remove(selection); + } } // End SelectionImpl.java diff --git a/testsrc/org/olap4j/OlapTest.java b/testsrc/org/olap4j/OlapTest.java index d99606f..c68e6b4 100644 --- a/testsrc/org/olap4j/OlapTest.java +++ b/testsrc/org/olap4j/OlapTest.java @@ -17,6 +17,8 @@ import java.sql.Connection; import java.sql.DriverManager; +import java.util.ArrayList; +import java.util.List; import junit.framework.TestCase; @@ -347,10 +349,10 @@ public void testMultipleDimensionSelections() { TestContext.assertEqualsVerbose( "SELECT\n" + "{[Measures].[Store Sales]} ON COLUMNS,\n" - + "CrossJoin({[Product].[All Products].[Drink].Children}, " - + "CrossJoin({{[Store].[All Stores].[USA], " - + "[Store].[All Stores].[USA].Children}}, " - + "{[Time].[1997].Children})) ON ROWS\n" + + "CrossJoin([Product].[All Products].[Drink].Children, " + + "CrossJoin({[Store].[All Stores].[USA], " + + "[Store].[All Stores].[USA].Children}, " + + "[Time].[1997].Children)) ON ROWS\n" + "FROM [Sales]", mdxString); } catch (Exception e) { e.printStackTrace(); @@ -500,6 +502,114 @@ public void testSortDimension() { } } + public void testSelectionContext() { + try { + Cube cube = getFoodmartCube("Sales"); + if (cube == null) { + fail("Could not find Sales cube"); + } + Query query = new Query("my query", cube); + + // create selections + + QueryDimension productDimension = query.getDimension("Product"); + productDimension.include( + Selection.Operator.INCLUDE_CHILDREN, "Product", "All Products"); + + QueryDimension timeDimension = query.getDimension("Time"); + timeDimension.include(Selection.Operator.MEMBER, "Time", "Year", "1997"); + + Selection selection = timeDimension.include(Selection.Operator.CHILDREN, "Time", "Year", "1997"); + selection.addContext(productDimension.createSelection("Product", "All Products", "Drink")); + + // [Store].[All Stores] + QueryDimension storeDimension = query.getDimension("Store"); + storeDimension.include(Selection.Operator.MEMBER, "Store", "All Stores"); + + Selection children = storeDimension.include(Selection.Operator.CHILDREN, "Store", "All Stores"); + children.addContext(productDimension.createSelection("Product", "All Products", "Drink")); + children.addContext(timeDimension.createSelection("Time", "1997", "Q3")); + + QueryDimension measuresDimension = query.getDimension("Measures"); + measuresDimension.include("Measures", "Store Sales"); + + query.getAxis(Axis.ROWS).addDimension(productDimension); + query.getAxis(Axis.ROWS).addDimension(timeDimension); + query.getAxis(Axis.ROWS).addDimension(storeDimension); + + query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); + + query.validate(); + + assertEquals( + Axis.ROWS, + productDimension.getAxis().getLocation()); + assertEquals( + Axis.COLUMNS, + measuresDimension.getAxis().getLocation()); + + SelectNode mdx = query.getSelect(); + String mdxString = mdx.toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{[Measures].[Store Sales]} ON COLUMNS,\n" + + "Hierarchize(Union(CrossJoin({[Product].[All Products], [Product].[All Products].Children}, CrossJoin({[Time].[1997]}, {[Store].[All Stores]})), Union(CrossJoin({[Product].[All Products].[Drink]}, CrossJoin({[Time].[1997].[Q3]}, [Store].[All Stores].Children)), CrossJoin({[Product].[All Products].[Drink]}, CrossJoin([Time].[1997].Children, {[Store].[All Stores]}))))) ON ROWS\n" + + "FROM [Sales]", + mdxString); + + // Sort the rows in ascending order. + query.getAxis(Axis.ROWS).sort( + SortOrder.ASC, + "Measures", + "Store Sales"); + + SelectNode sortedMdx = query.getSelect(); + String sortedMdxString = sortedMdx.toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{[Measures].[Store Sales]} ON COLUMNS,\n" + + "Order(Hierarchize(Union(CrossJoin({[Product].[All Products], [Product].[All Products].Children}, CrossJoin({[Time].[1997]}, {[Store].[All Stores]})), Union(CrossJoin({[Product].[All Products].[Drink]}, CrossJoin({[Time].[1997].[Q3]}, [Store].[All Stores].Children)), CrossJoin({[Product].[All Products].[Drink]}, CrossJoin([Time].[1997].Children, {[Store].[All Stores]}))))), [Measures].[Store Sales], ASC) ON ROWS\n" + + "FROM [Sales]" + , + sortedMdxString); + + CellSet results = query.execute(); + String s = TestContext.toString(results); + TestContext.assertEqualsVerbose( + "Axis #0:\n" + + "{[Store Size in SQFT].[All Store Size in SQFTs], [Store Type].[All Store Types], [Promotion Media].[All Media], [Promotions].[All Promotions], [Customers].[All Customers], [Education Level].[All Education Levels], [Gender].[All Gender], [Marital Status].[All Marital Status], [Yearly Income].[All Yearly Incomes]}\n" + + "Axis #1:\n" + + "{[Measures].[Store Sales]}\n" + + "Axis #2:\n" + + "{[Product].[All Products], [Time].[1997], [Store].[All Stores]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997], [Store].[All Stores]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997].[Q1], [Store].[All Stores]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997].[Q2], [Store].[All Stores]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997].[Q3], [Store].[All Stores]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997].[Q3], [Store].[All Stores].[Canada]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997].[Q3], [Store].[All Stores].[Mexico]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997].[Q3], [Store].[All Stores].[USA]}\n" + + "{[Product].[All Products].[Drink], [Time].[1997].[Q4], [Store].[All Stores]}\n" + + "{[Product].[All Products].[Non-Consumable], [Time].[1997], [Store].[All Stores]}\n" + + "{[Product].[All Products].[Food], [Time].[1997], [Store].[All Stores]}\n" + + "Row #0: 565,238.13\n" + + "Row #1: 48,836.21\n" + + "Row #2: 11,585.80\n" + + "Row #3: 11,914.58\n" + + "Row #4: 11,994.00\n" + + "Row #5: \n" + + "Row #6: \n" + + "Row #7: 11,994.00\n" + + "Row #8: 13,341.83\n" + + "Row #9: 107,366.33\n" + + "Row #10: 409,035.59\n", + s); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + public void testSortAxis() { try { Cube cube = getFoodmartCube("Sales"); @@ -619,10 +729,10 @@ public void testDimensionsOrder() { TestContext.assertEqualsVerbose( "SELECT\n" + "{[Measures].[Store Sales]} ON COLUMNS,\n" - + "CrossJoin({[Product].[All Products].[Drink].Children}, " - + "CrossJoin({{[Store].[All Stores].[USA], " - + "[Store].[All Stores].[USA].Children}}, " - + "{[Time].[1997].Children})) ON ROWS\n" + + "CrossJoin([Product].[All Products].[Drink].Children, " + + "CrossJoin({[Store].[All Stores].[USA], " + + "[Store].[All Stores].[USA].Children}, " + + "[Time].[1997].Children)) ON ROWS\n" + "FROM [Sales]", mdxString); @@ -636,10 +746,10 @@ public void testDimensionsOrder() { TestContext.assertEqualsVerbose( "SELECT\n" + "{[Measures].[Store Sales]} ON COLUMNS,\n" - + "CrossJoin({{[Store].[All Stores].[USA], " - + "[Store].[All Stores].[USA].Children}}, " - + "CrossJoin({[Product].[All Products].[Drink].Children}, " - + "{[Time].[1997].Children})) ON ROWS\n" + + "CrossJoin({[Store].[All Stores].[USA], " + + "[Store].[All Stores].[USA].Children}, " + + "CrossJoin([Product].[All Products].[Drink].Children, " + + "[Time].[1997].Children)) ON ROWS\n" + "FROM [Sales]", mdxString); @@ -653,10 +763,10 @@ public void testDimensionsOrder() { TestContext.assertEqualsVerbose( "SELECT\n" + "{[Measures].[Store Sales]} ON COLUMNS,\n" - + "CrossJoin({{[Store].[All Stores].[USA], " - + "[Store].[All Stores].[USA].Children}}, " - + "CrossJoin({[Time].[1997].Children}, " - + "{[Product].[All Products].[Drink].Children})) ON ROWS\n" + + "CrossJoin({[Store].[All Stores].[USA], " + + "[Store].[All Stores].[USA].Children}, " + + "CrossJoin([Time].[1997].Children, " + + "[Product].[All Products].[Drink].Children)) ON ROWS\n" + "FROM [Sales]", mdxString); } catch (Exception e) { @@ -665,6 +775,9 @@ public void testDimensionsOrder() { } } + /** + * Note: hierarchize mode only works when a single dimension is selected + */ public void testDimensionsHierarchize() { try { Cube cube = getFoodmartCube("Sales"); @@ -675,26 +788,15 @@ public void testDimensionsHierarchize() { // create selections - QueryDimension productDimension = query.getDimension("Product"); - productDimension.include( - Selection.Operator.CHILDREN, "Product", "Drink"); - QueryDimension storeDimension = query.getDimension("Store"); storeDimension.include( Selection.Operator.INCLUDE_CHILDREN, "Store", "USA"); storeDimension.setHierarchizeMode(HierarchizeMode.POST); - QueryDimension timeDimension = query.getDimension("Time"); - - timeDimension.include(Selection.Operator.CHILDREN, "Time", "1997"); - QueryDimension measuresDimension = query.getDimension("Measures"); measuresDimension.include("Measures", "Store Sales"); - - query.getAxis(Axis.ROWS).addDimension(productDimension); query.getAxis(Axis.ROWS).addDimension(storeDimension); - query.getAxis(Axis.ROWS).addDimension(timeDimension); query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); query.validate(); @@ -704,10 +806,8 @@ public void testDimensionsHierarchize() { TestContext.assertEqualsVerbose( "SELECT\n" + "{[Measures].[Store Sales]} ON COLUMNS,\n" - + "CrossJoin({[Product].[All Products].[Drink].Children}, " - + "CrossJoin({Hierarchize({{[Store].[All Stores].[USA], " - + "[Store].[All Stores].[USA].Children}}, POST)}, " - + "{[Time].[1997].Children})) ON ROWS\n" + + "{Hierarchize({{[Store].[All Stores].[USA], " + + "[Store].[All Stores].[USA].Children}}, POST)} ON ROWS\n" + "FROM [Sales]", mdxString); @@ -720,10 +820,8 @@ public void testDimensionsHierarchize() { TestContext.assertEqualsVerbose( "SELECT\n" + "{[Measures].[Store Sales]} ON COLUMNS,\n" - + "CrossJoin({[Product].[All Products].[Drink].Children}, " - + "CrossJoin({Hierarchize({{[Store].[All Stores].[USA], " - + "[Store].[All Stores].[USA].Children}})}, " - + "{[Time].[1997].Children})) ON ROWS\n" + + "{Hierarchize({{[Store].[All Stores].[USA], " + + "[Store].[All Stores].[USA].Children}})} ON ROWS\n" + "FROM [Sales]", mdxString); } catch (Exception e) {