From d07acc9ad380717b4190dc813dd4ba80a4bada56 Mon Sep 17 00:00:00 2001 From: pstoellberger Date: Tue, 22 Jan 2013 15:05:16 +0100 Subject: [PATCH] implement limit function on axis (topcount, bottompercent, ...) --- src/org/olap4j/query/LimitFunction.java | 58 +++++++++++ src/org/olap4j/query/Olap4jNodeConverter.java | 38 +++++++- src/org/olap4j/query/QueryAxis.java | 71 +++++++++++++- testsrc/org/olap4j/OlapTest.java | 97 +++++++++++++++++++ 4 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 src/org/olap4j/query/LimitFunction.java diff --git a/src/org/olap4j/query/LimitFunction.java b/src/org/olap4j/query/LimitFunction.java new file mode 100644 index 0000000..afcadb1 --- /dev/null +++ b/src/org/olap4j/query/LimitFunction.java @@ -0,0 +1,58 @@ +/* +// Licensed to Julian Hyde under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. +// +// Julian Hyde licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except in +// compliance with the License. You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +*/ +package org.olap4j.query; + +/** + * Defines in what order to perform sort operations. + * + * @author Paul Stoellberger + * @since 1.0.1 + */ +public enum LimitFunction { + /** + * Returns a specified number of items from the + * top of a set, optionally ordering the set first. + */ + TopCount, + /** + * Sorts a set and returns the top N elements + * whose cumulative total is at least a specified percentage. + */ + TopPercent, + /** + * Sorts a set and returns the top N elements + * whose cumulative total is at least a specified value. + */ + TopSum, + /** + * Returns a specified number of items from the + * bottom of a set, optionally ordering the set first. + */ + BottomCount, + /** + * Sorts a set and returns the bottom N elements + * whose cumulative total is at least a specified percentage. + */ + BottomPercent, + /** + * Sorts a set and returns the bottom N elements + * whose cumulative total is at least a specified value. + */ + BottomSum +} +// End LimitFunction.java diff --git a/src/org/olap4j/query/Olap4jNodeConverter.java b/src/org/olap4j/query/Olap4jNodeConverter.java index 9101ca8..39dc73b 100644 --- a/src/org/olap4j/query/Olap4jNodeConverter.java +++ b/src/org/olap4j/query/Olap4jNodeConverter.java @@ -299,6 +299,40 @@ private static AxisNode toOlap4j(QueryAxis axis) { } } + // We might need to limit the axis set + ParseTreeNode limitedNode = null; + if (axis.getLimitFunction() != null) { + ParseTreeNode n = + LiteralNode.createNumeric( + null, + axis.getLimitFunctionN(), + false); + if (axis.getLimitFunctionSortLiteral() != null) { + LiteralNode evaluatorNode = null; + evaluatorNode = + LiteralNode.createSymbol( + null, + axis.getLimitFunctionSortLiteral()); + limitedNode = + new CallNode( + null, + axis.getLimitFunction().toString(), + Syntax.Function, + callNode, + n, + evaluatorNode); + } else { + limitedNode = + new CallNode( + null, + axis.getLimitFunction().toString(), + Syntax.Function, + callNode, + n); + } + } else { + limitedNode = callNode; + } // We might need to sort the whole axis. ParseTreeNode sortedNode = null; if (axis.getSortOrder() != null) { @@ -311,12 +345,12 @@ private static AxisNode toOlap4j(QueryAxis axis) { null, "Order", Syntax.Function, - callNode, + limitedNode, evaluatorNode, LiteralNode.createSymbol( null, axis.getSortOrder().name())); } else { - sortedNode = callNode; + sortedNode = limitedNode; } return new AxisNode( diff --git a/src/org/olap4j/query/QueryAxis.java b/src/org/olap4j/query/QueryAxis.java index 4cc328f..606d770 100644 --- a/src/org/olap4j/query/QueryAxis.java +++ b/src/org/olap4j/query/QueryAxis.java @@ -23,6 +23,7 @@ import org.olap4j.metadata.Measure; import org.olap4j.metadata.Member; +import java.math.BigDecimal; import java.util.*; /** @@ -43,7 +44,9 @@ public class QueryAxis extends QueryNodeImpl { private boolean nonEmpty; private SortOrder sortOrder = null; private String sortEvaluationLiteral = null; - + private LimitFunction limitFunction = null; + private BigDecimal limitFunctionN = null; + private String limitFunctionSortLiteral = null; /** * Creates a QueryAxis. * @@ -351,6 +354,72 @@ public SortOrder getSortOrder() { public String getSortIdentifierNodeName() { return sortEvaluationLiteral; } + + /** + * Returns a specified number of items from the + * top of the axis set + * @param n + */ + public void topCount(BigDecimal n) { + this.limitFunction = LimitFunction.TopCount; + this.limitFunctionN = n; + } + + /** + * Returns a specified number of items from the + * bottom of the axis set + * @param n + */ + public void bottomCount(BigDecimal n) { + this.limitFunction = LimitFunction.BottomCount; + this.limitFunctionN = n; + } + + /** + * Limit the axis set to a specified number of items depending + * on the sortLiteral and {@link LimitFunction} + * @param n - number of items/cumulative sum/percentage + */ + public void limit(LimitFunction function, BigDecimal n, String limitSortLiteral) { + this.limitFunction = function; + this.limitFunctionN = n; + this.limitFunctionSortLiteral = limitSortLiteral; + } + + /** + * Clears the limit parameters of that axis + */ + public void clearLimitFunction() { + this.limitFunction = null; + this.limitFunctionN = null; + this.limitFunctionSortLiteral = null; + } + + /** + * @return The {@link LimitFunction} + */ + public LimitFunction getLimitFunction() { + return limitFunction; + } + + /** + * @return the number of items/cumulative sum/percentage + * being used by the {@link LimitFunction)} + */ + public BigDecimal getLimitFunctionN() { + return limitFunctionN; + } + + /** + * Returns the current sort literal being used by the + * limit functionMight return null of none + * is currently specified. + * @return sort literal of the limit function + */ public String getLimitFunctionSortLiteral() { + return limitFunctionSortLiteral; + } + + } // End QueryAxis.java diff --git a/testsrc/org/olap4j/OlapTest.java b/testsrc/org/olap4j/OlapTest.java index 0f892e0..849169c 100644 --- a/testsrc/org/olap4j/OlapTest.java +++ b/testsrc/org/olap4j/OlapTest.java @@ -27,6 +27,7 @@ import junit.framework.TestCase; +import java.math.BigDecimal; import java.sql.Connection; import java.sql.DriverManager; @@ -1428,6 +1429,102 @@ public void testMultipleHierarchyConsistency() throws Exception { resultsString); query.validate(); } + public void testLimitFunction() throws Exception { + 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"); + NamedList productLevels = + productDimension.getDimension() + .getDefaultHierarchy().getLevels(); + + Level productLevel = productLevels.get("Product Category"); + productDimension.include(productLevel); + + QueryDimension measuresDimension = query.getDimension("Measures"); + measuresDimension.include(nameList("Measures", "Sales Count")); + + query.getAxis(Axis.ROWS).addDimension(productDimension); + query.getAxis(Axis.COLUMNS).addDimension(measuresDimension); + + query.getAxis(Axis.ROWS).topCount(new BigDecimal(6)); + + query.validate(); + + // Validate the generated MDX + String mdxString = query.getSelect().toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{[Measures].[Sales Count]} ON COLUMNS,\n" + + "TopCount({[Product].[Product].[Product Category].Members}, 6) 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" + + "{[Measures].[Sales Count]}\n" + + "Axis #2:\n" + + "{[Product].[Product].[Drink].[Alcoholic Beverages].[Beer and Wine]}\n" + + "{[Product].[Product].[Drink].[Beverages].[Carbonated Beverages]}\n" + + "{[Product].[Product].[Drink].[Beverages].[Drinks]}\n" + + "{[Product].[Product].[Drink].[Beverages].[Hot Beverages]}\n" + + "{[Product].[Product].[Drink].[Beverages].[Pure Juice Beverages]}\n" + + "{[Product].[Product].[Drink].[Dairy].[Dairy]}\n" + + "Row #0: 2,219\n" + + "Row #1: 1,107\n" + + "Row #2: 798\n" + + "Row #3: 1,391\n" + + "Row #4: 1,096\n" + + "Row #5: 1,367\n", + resultsString); + + query.getAxis(Axis.ROWS).limit( + LimitFunction.TopCount, + new BigDecimal(6), + "[Measures].[Sales Count]"); + + query.validate(); + + // Validate the generated MDX + mdxString = query.getSelect().toString(); + TestContext.assertEqualsVerbose( + "SELECT\n" + + "{[Measures].[Sales Count]} ON COLUMNS,\n" + + "TopCount({[Product].[Product].[Product Category].Members}, 6, [Measures].[Sales Count]) ON ROWS\n" + + "FROM [Sales]", + mdxString); + + // Validate the returned results + results = query.execute(); + resultsString = TestContext.toString(results); + TestContext.assertEqualsVerbose( + "Axis #0:\n" + + "{}\n" + + "Axis #1:\n" + + "{[Measures].[Sales Count]}\n" + + "Axis #2:\n" + + "{[Product].[Product].[Food].[Snack Foods].[Snack Foods]}\n" + + "{[Product].[Product].[Food].[Produce].[Vegetables]}\n" + + "{[Product].[Product].[Food].[Dairy].[Dairy]}\n" + + "{[Product].[Product].[Food].[Baking Goods].[Jams and Jellies]}\n" + + "{[Product].[Product].[Food].[Produce].[Fruit]}\n" + + "{[Product].[Product].[Food].[Deli].[Meat]}\n" + + "Row #0: 9,957\n" + + "Row #1: 6,751\n" + + "Row #2: 4,189\n" + + "Row #3: 3,868\n" + + "Row #4: 3,836\n" + + "Row #5: 3,064\n", + resultsString); + } public void testHierarchyConsistency() throws Exception { Cube cube = getFoodmartCube("Sales"); if (cube == null) {