/* // This software is subject to the terms of the Eclipse Public License v1.0 // Agreement, available at the following URL: // http://www.eclipse.org/legal/epl-v10.html. // You must accept the terms of that agreement to use this software. // // Copyright (C) 2005-2005 Julian Hyde // Copyright (C) 2005-2012 Pentaho // All Rights Reserved. */ package mondrian.test; import mondrian.olap.*; import mondrian.olap.Axis; import mondrian.olap.Cell; import mondrian.olap.type.*; import mondrian.spi.CellFormatter; import mondrian.spi.MemberFormatter; import mondrian.spi.PropertyFormatter; import mondrian.spi.*; import org.apache.log4j.MDC; import org.olap4j.*; import org.olap4j.metadata.Property; import java.sql.SQLException; import java.util.*; import java.util.regex.Pattern; /** * Unit-test for {@link UserDefinedFunction user-defined functions}. * Also tests {@link mondrian.spi.CellFormatter cell formatters} * and {@link mondrian.spi.MemberFormatter member formatters}. * * <p>TODO: * 1. test that function which does not return a name, description etc. * gets a sensible error * 2. document UDFs * * @author jhyde * @since Apr 29, 2005 */ public class UdfTest extends FoodMartTestCase { public UdfTest() { } public UdfTest(String name) { super(name); } /** * Test context which uses the local FoodMart schema, and adds a "PlusOne" * user-defined function. */ private TestContext tc; @Override protected void setUp() throws Exception { super.setUp(); tc = udfTestContext( "<UserDefinedFunction name=\"PlusOne\" className=\"" + PlusOneUdf.class.getName() + "\"/>\n"); } @Override protected void tearDown() throws Exception { super.tearDown(); tc = null; // allow gc } public TestContext getTestContext() { return tc; } /** * Shorthand for containing a test context that consists of the standard * FoodMart schema plus a UDF. * * @param xmlUdf UDF definition * @return Test context */ private TestContext udfTestContext(String xmlUdf) { return TestContext.instance().create( null, null, null, null, xmlUdf, null); } /** * Shorthand for containing a test context that consists of the standard * FoodMart Sales cube plus one measure. * * @param xmlMeasure Measure definition * @return Test context */ private TestContext measureTestContext(String xmlMeasure) { return TestContext.instance().createSubstitutingCube( "Sales", null, xmlMeasure, null, null); } /** * Shorthand for containing a test context that consists of the standard * FoodMart Sales cube plus one calculated member. * * @param xmlCalcMember Calculated member definition * @return Test context */ private TestContext calcMemberTestContext(String xmlCalcMember) { return TestContext.instance().createSubstitutingCube( "Sales", null, null, xmlCalcMember, null); } // ~ Tests follow ---------------------------------------------------------- public void testSanity() { // sanity check, make sure the schema is loading correctly assertQueryReturns( "SELECT {[Measures].[Store Sqft]} ON COLUMNS, {[Store Type]} ON ROWS FROM [Store]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Store Sqft]}\n" + "Axis #2:\n" + "{[Store Type].[All Store Types]}\n" + "Row #0: 571,596\n"); } public void testFun() { assertQueryReturns( "WITH MEMBER [Measures].[Sqft Plus One] AS 'PlusOne([Measures].[Store Sqft])'\n" + "SELECT {[Measures].[Sqft Plus One]} ON COLUMNS, \n" + " {[Store Type].children} ON ROWS \n" + "FROM [Store]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Sqft Plus One]}\n" + "Axis #2:\n" + "{[Store Type].[Deluxe Supermarket]}\n" + "{[Store Type].[Gourmet Supermarket]}\n" + "{[Store Type].[HeadQuarters]}\n" + "{[Store Type].[Mid-Size Grocery]}\n" + "{[Store Type].[Small Grocery]}\n" + "{[Store Type].[Supermarket]}\n" + "Row #0: 146,046\n" + "Row #1: 47,448\n" + "Row #2: \n" + "Row #3: 109,344\n" + "Row #4: 75,282\n" + "Row #5: 193,481\n"); } /** * Test case for bug * <a href="http://jira.pentaho.com/browse/MONDRIAN-1200">MONDRIAN-1200, * "User-defined function + profiling causes NPE in CalcWriter"</a>. * The bug only occurs if manually enable "mondrian.profile" logger before * running this test. (The bug requires olap4j, plus profiling, plus a * query that calls a UDF with one or more arguments on an axis.) * * @throws SQLException on error */ public void testFunWithProfiling() throws SQLException { OlapConnection connection = null; OlapStatement statement = null; CellSet x = null; try { connection = getTestContext().getOlap4jConnection(); statement = connection.createStatement(); x = statement.executeOlapQuery( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[yyyy]\\.[Qq]\\.[m]\", BEFORE)} " + "ON COLUMNS FROM [Sales]"); Util.discard(TestContext.toString(x)); } finally { Util.close(x, statement, connection); } } public void testLastNonEmpty() { assertQueryReturns( "WITH MEMBER [Measures].[Last Unit Sales] AS \n" + " '([Measures].[Unit Sales], \n" + " LastNonEmpty(Descendants([Time].[Time]), [Measures].[Unit Sales]))'\n" + "SELECT {[Measures].[Last Unit Sales]} ON COLUMNS,\n" + " CrossJoin(\n" + " {[Time].[1997], [Time].[1997].[Q1], [Time].[1997].[Q1].Children},\n" + " {[Product].[All Products].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].children}) ON ROWS\n" + "FROM [Sales]\n" + "WHERE ([Store].[All Stores].[USA].[OR].[Portland].[Store 11])", "Axis #0:\n" + "{[Store].[USA].[OR].[Portland].[Store 11]}\n" + "Axis #1:\n" + "{[Measures].[Last Unit Sales]}\n" + "Axis #2:\n" + "{[Time].[1997], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Good]}\n" + "{[Time].[1997], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Pearl]}\n" + "{[Time].[1997], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Portsmouth]}\n" + "{[Time].[1997], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Top Measure]}\n" + "{[Time].[1997], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Walrus]}\n" + "{[Time].[1997].[Q1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Good]}\n" + "{[Time].[1997].[Q1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Pearl]}\n" + "{[Time].[1997].[Q1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Portsmouth]}\n" + "{[Time].[1997].[Q1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Top Measure]}\n" + "{[Time].[1997].[Q1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Walrus]}\n" + "{[Time].[1997].[Q1].[1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Good]}\n" + "{[Time].[1997].[Q1].[1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Pearl]}\n" + "{[Time].[1997].[Q1].[1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Portsmouth]}\n" + "{[Time].[1997].[Q1].[1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Top Measure]}\n" + "{[Time].[1997].[Q1].[1], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Walrus]}\n" + "{[Time].[1997].[Q1].[2], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Good]}\n" + "{[Time].[1997].[Q1].[2], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Pearl]}\n" + "{[Time].[1997].[Q1].[2], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Portsmouth]}\n" + "{[Time].[1997].[Q1].[2], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Top Measure]}\n" + "{[Time].[1997].[Q1].[2], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Walrus]}\n" + "{[Time].[1997].[Q1].[3], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Good]}\n" + "{[Time].[1997].[Q1].[3], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Pearl]}\n" + "{[Time].[1997].[Q1].[3], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Portsmouth]}\n" + "{[Time].[1997].[Q1].[3], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Top Measure]}\n" + "{[Time].[1997].[Q1].[3], [Product].[Drink].[Alcoholic Beverages].[Beer and Wine].[Beer].[Walrus]}\n" + "Row #0: 2\n" + "Row #1: 7\n" + "Row #2: 6\n" + "Row #3: 7\n" + "Row #4: 4\n" + "Row #5: 3\n" + "Row #6: 4\n" + "Row #7: 3\n" + "Row #8: 4\n" + "Row #9: 2\n" + "Row #10: \n" + "Row #11: 4\n" + "Row #12: \n" + "Row #13: 2\n" + "Row #14: \n" + "Row #15: \n" + "Row #16: 2\n" + "Row #17: \n" + "Row #18: 4\n" + "Row #19: \n" + "Row #20: 3\n" + "Row #21: 4\n" + "Row #22: 3\n" + "Row #23: 4\n" + "Row #24: 2\n"); } /** * Tests a performance issue with LastNonEmpty (bug 1533677). The naive * implementation of LastNonEmpty crawls backward one period at a time, * generates a cache miss, and the next iteration reads precisely one cell. * So the query soon exceeds the {@link MondrianProperties#MaxEvalDepth} * property. */ public void testLastNonEmptyBig() { assertQueryReturns( "with\n" + " member\n" + " [Measures].[Last Sale] as ([Measures].[Unit Sales],\n" + " LastNonEmpty(Descendants([Time].[Time].CurrentMember, [Time].[Month]),\n" + " [Measures].[Unit Sales]))\n" + "select\n" + " NON EMPTY {[Measures].[Last Sale]} ON columns,\n" + " NON EMPTY Order([Store].[All Stores].Children,\n" + " [Measures].[Last Sale], DESC) ON rows\n" + "from [Sales]\n" + "where [Time].[Time].LastSibling", "Axis #0:\n" + "{[Time].[1998]}\n" + "Axis #1:\n" + "Axis #2:\n"); } public void testBadFun() { final TestContext tc = udfTestContext( "<UserDefinedFunction name=\"BadPlusOne\" className=\"" + BadPlusOneUdf.class.getName() + "\"/>\n"); try { tc.executeQuery("SELECT {} ON COLUMNS FROM [Sales]"); fail("Expected exception"); } catch (Exception e) { final String s = e.getMessage(); assertEquals( "Mondrian Error:Internal error: Invalid " + "user-defined function 'BadPlusOne': return type is null", s); } } public void testGenericFun() { final TestContext tc = udfTestContext( "<UserDefinedFunction name=\"GenericPlusOne\" className=\"" + PlusOrMinusOneUdf.class.getName() + "\"/>\n" + "<UserDefinedFunction name=\"GenericMinusOne\" className=\"" + PlusOrMinusOneUdf.class.getName() + "\"/>\n"); tc.assertExprReturns("GenericPlusOne(3)", "4"); tc.assertExprReturns("GenericMinusOne(3)", "2"); } public void testComplexFun() { assertQueryReturns( "WITH MEMBER [Measures].[InverseNormal] AS 'InverseNormal([Measures].[Grocery Sqft] / [Measures].[Store Sqft])', FORMAT_STRING = \"0.000\"\n" + "SELECT {[Measures].[InverseNormal]} ON COLUMNS, \n" + " {[Store Type].children} ON ROWS \n" + "FROM [Store]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[InverseNormal]}\n" + "Axis #2:\n" + "{[Store Type].[Deluxe Supermarket]}\n" + "{[Store Type].[Gourmet Supermarket]}\n" + "{[Store Type].[HeadQuarters]}\n" + "{[Store Type].[Mid-Size Grocery]}\n" + "{[Store Type].[Small Grocery]}\n" + "{[Store Type].[Supermarket]}\n" + "Row #0: 0.467\n" + "Row #1: 0.463\n" + "Row #2: \n" + "Row #3: 0.625\n" + "Row #4: 0.521\n" + "Row #5: 0.504\n"); } public void testException() { Result result = executeQuery( "WITH MEMBER [Measures].[InverseNormal] " + " AS 'InverseNormal([Measures].[Store Sqft] / [Measures].[Grocery Sqft])'," + " FORMAT_STRING = \"0.000000\"\n" + "SELECT {[Measures].[InverseNormal]} ON COLUMNS, \n" + " {[Store Type].children} ON ROWS \n" + "FROM [Store]"); Axis rowAxis = result.getAxes()[0]; assertTrue(rowAxis.getPositions().size() == 1); Axis colAxis = result.getAxes()[1]; assertTrue(colAxis.getPositions().size() == 6); Cell cell = result.getCell(new int[]{0, 0}); assertTrue(cell.isError()); getTestContext().assertMatchesVerbose( Pattern.compile( "(?s).*Invalid value for inverse normal distribution: 1.4708.*"), cell.getValue().toString()); cell = result.getCell(new int[]{0, 5}); assertTrue(cell.isError()); getTestContext().assertMatchesVerbose( Pattern.compile( "(?s).*Invalid value for inverse normal distribution: 1.4435.*"), cell.getValue().toString()); } public void testCurrentDateString() { String actual = executeExpr("CurrentDateString(\"Ddd mmm dd yyyy\")"); Date currDate = new Date(); String dateString = currDate.toString(); String expected = dateString.substring(0, 11) + dateString.substring(dateString.length() - 4); assertEquals(expected, actual); } public void testCurrentDateMemberBefore() { assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[yyyy]\\.[Qq]\\.[m]\", BEFORE)} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Time].[1998].[Q4].[12]}\n" + "Row #0: \n"); } public void testCurrentDateMemberBeforeUsingQuotes() { assertAxisReturns( MondrianProperties.instance().SsasCompatibleNaming.get() ? "CurrentDateMember([Time].[Time], " + "'\"[Time].[Time].[\"yyyy\"].[Q\"q\"].[\"m\"]\"', BEFORE)" : "CurrentDateMember([Time], " + "'\"[Time].[\"yyyy\"].[Q\"q\"].[\"m\"]\"', BEFORE)", "[Time].[1998].[Q4].[12]"); } public void testCurrentDateMemberAfter() { // CurrentDateMember will return null member since the latest date in // FoodMart is from '98 assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[yyyy]\\.[Qq]\\.[m]\", AFTER)} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n"); } public void testCurrentDateMemberExact() { // CurrentDateMember will return null member since the latest date in // FoodMart is from '98; apply a function on the return value to // ensure null member instead of null is returned assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[yyyy]\\.[Qq]\\.[m]\", EXACT).lag(1)} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n"); } public void testCurrentDateMemberNoFindArg() { // CurrentDateMember will return null member since the latest date in // FoodMart is from '98 assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[yyyy]\\.[Qq]\\.[m]\")} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n"); } public void testCurrentDateMemberHierarchy() { final String query = MondrianProperties.instance().SsasCompatibleNaming.get() ? "SELECT { CurrentDateMember([Time.Weekly], " + "\"[Ti\\me\\.Weekl\\y]\\.[All Weekl\\y\\s]\\.[yyyy]\\.[ww]\", BEFORE)} " + "ON COLUMNS FROM [Sales]" : "SELECT { CurrentDateMember([Time.Weekly], " + "\"[Ti\\me\\.Weekl\\y]\\.[All Ti\\me\\.Weekl\\y\\s]\\.[yyyy]\\.[ww]\", BEFORE)} " + "ON COLUMNS FROM [Sales]"; assertQueryReturns( query, "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Time].[Weekly].[1998].[52]}\n" + "Row #0: \n"); } public void testCurrentDateMemberHierarchyNullReturn() { // CurrentDateMember will return null member since the latest date in // FoodMart is from '98; note that first arg is a hierarchy rather // than a dimension assertQueryReturns( "SELECT { CurrentDateMember([Time.Weekly], " + "\"[Ti\\me]\\.[yyyy]\\.[Qq]\\.[m]\")} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n"); } public void testCurrentDateMemberRealAfter() { // omit formatting characters from the format so the current date // is hard-coded to actual value in the database so we can test the // after logic assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[1996]\\.[Q4]\", after)} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Time].[1997].[Q1]}\n" + "Row #0: 66,291\n"); } public void testCurrentDateMemberRealExact1() { // omit formatting characters from the format so the current date // is hard-coded to actual value in the database so we can test the // exact logic assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[1997]\")} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Time].[1997]}\n" + "Row #0: 266,773\n"); } public void testCurrentDateMemberRealExact2() { // omit formatting characters from the format so the current date // is hard-coded to actual value in the database so we can test the // exact logic assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[1997]\\.[Q2]\\.[5]\")} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Time].[1997].[Q2].[5]}\n" + "Row #0: 21,081\n"); } public void testCurrentDateMemberPrev() { // apply a function on the result of the UDF assertQueryReturns( "SELECT { CurrentDateMember([Time].[Time], " + "\"[Ti\\me]\\.[yyyy]\\.[Qq]\\.[m]\", BEFORE).PrevMember} " + "ON COLUMNS FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Time].[1998].[Q4].[11]}\n" + "Row #0: \n"); } public void testCurrentDateLag() { // Also, try a different style of quoting, because single quote followed // by double quote (used in other examples) is difficult to read. assertQueryReturns( "SELECT\n" + " { [Measures].[Unit Sales] } ON COLUMNS,\n" + " { CurrentDateMember([Time].[Time], '[\"Time\"]\\.[yyyy]\\.[\"Q\"q]\\.[m]', BEFORE).Lag(3) : " + " CurrentDateMember([Time].[Time], '[\"Time\"]\\.[yyyy]\\.[\"Q\"q]\\.[m]', BEFORE) } ON ROWS\n" + "FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "Axis #2:\n" + "{[Time].[1998].[Q3].[9]}\n" + "{[Time].[1998].[Q4].[10]}\n" + "{[Time].[1998].[Q4].[11]}\n" + "{[Time].[1998].[Q4].[12]}\n" + "Row #0: \n" + "Row #1: \n" + "Row #2: \n" + "Row #3: \n"); } public void testMatches() { assertQueryReturns( "SELECT {[Measures].[Org Salary]} ON COLUMNS, " + "Filter({[Employees].MEMBERS}, " + "[Employees].CurrentMember.Name MATCHES '(?i)sam.*') ON ROWS " + "FROM [HR]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Org Salary]}\n" + "Axis #2:\n" + "{[Employees].[Sheri Nowmer].[Derrick Whelply].[Beverly Baker].[Jacqueline Wyllie].[Ralph Mccoy].[Anne Tuck].[Samuel Johnson]}\n" + "{[Employees].[Sheri Nowmer].[Derrick Whelply].[Pedro Castillo].[Jose Bernard].[Mary Hunt].[Bonnie Bruno].[Sam Warren]}\n" + "{[Employees].[Sheri Nowmer].[Derrick Whelply].[Pedro Castillo].[Charles Macaluso].[Barbara Wallin].[Michael Suggs].[Sam Adair]}\n" + "{[Employees].[Sheri Nowmer].[Derrick Whelply].[Pedro Castillo].[Lois Wood].[Dell Gras].[Kristine Aldred].[Sam Zeller]}\n" + "{[Employees].[Sheri Nowmer].[Derrick Whelply].[Laurie Borges].[Cody Goldey].[Shanay Steelman].[Neal Hasty].[Sam Wheeler]}\n" + "{[Employees].[Sheri Nowmer].[Maya Gutierrez].[Brenda Blumberg].[Wayne Banack].[Samuel Agcaoili]}\n" + "{[Employees].[Sheri Nowmer].[Maya Gutierrez].[Jonathan Murraiin].[James Thompson].[Samantha Weller]}\n" + "Row #0: $40.62\n" + "Row #1: $40.31\n" + "Row #2: $75.60\n" + "Row #3: $40.35\n" + "Row #4: $47.52\n" + "Row #5: \n" + "Row #6: \n"); } public void testNotMatches() { assertQueryReturns( "SELECT {[Measures].[Store Sales]} ON COLUMNS, " + "Filter({[Store Type].MEMBERS}, " + "[Store Type].CurrentMember.Name NOT MATCHES " + "'.*Grocery.*') ON ROWS " + "FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Store Sales]}\n" + "Axis #2:\n" + "{[Store Type].[All Store Types]}\n" + "{[Store Type].[Deluxe Supermarket]}\n" + "{[Store Type].[Gourmet Supermarket]}\n" + "{[Store Type].[HeadQuarters]}\n" + "{[Store Type].[Supermarket]}\n" + "Row #0: 565,238.13\n" + "Row #1: 162,062.24\n" + "Row #2: 45,750.24\n" + "Row #3: \n" + "Row #4: 319,210.04\n"); } public void testIn() { assertQueryReturns( "SELECT {[Measures].[Unit Sales]} ON COLUMNS, " + "FILTER([Product].[Product Family].MEMBERS, " + "[Product].[Product Family].CurrentMember IN " + "{[Product].[All Products].firstChild, " + "[Product].[All Products].lastChild}) ON ROWS " + "FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "Axis #2:\n" + "{[Product].[Drink]}\n" + "{[Product].[Non-Consumable]}\n" + "Row #0: 24,597\n" + "Row #1: 50,236\n"); } public void testNotIn() { assertQueryReturns( "SELECT {[Measures].[Unit Sales]} ON COLUMNS, " + "FILTER([Product].[Product Family].MEMBERS, " + "[Product].[Product Family].CurrentMember NOT IN " + "{[Product].[All Products].firstChild, " + "[Product].[All Products].lastChild}) ON ROWS " + "FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "Axis #2:\n" + "{[Product].[Food]}\n" + "Row #0: 191,940\n"); } public void testChildMemberIn() { assertQueryReturns( "SELECT {[Measures].[Store Sales]} ON COLUMNS, " + "{[Store].[Store Name].MEMBERS} ON ROWS " + "FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Store Sales]}\n" + "Axis #2:\n" + "{[Store].[Canada].[BC].[Vancouver].[Store 19]}\n" + "{[Store].[Canada].[BC].[Victoria].[Store 20]}\n" + "{[Store].[Mexico].[DF].[Mexico City].[Store 9]}\n" + "{[Store].[Mexico].[DF].[San Andres].[Store 21]}\n" + "{[Store].[Mexico].[Guerrero].[Acapulco].[Store 1]}\n" + "{[Store].[Mexico].[Jalisco].[Guadalajara].[Store 5]}\n" + "{[Store].[Mexico].[Veracruz].[Orizaba].[Store 10]}\n" + "{[Store].[Mexico].[Yucatan].[Merida].[Store 8]}\n" + "{[Store].[Mexico].[Zacatecas].[Camacho].[Store 4]}\n" + "{[Store].[Mexico].[Zacatecas].[Hidalgo].[Store 12]}\n" + "{[Store].[Mexico].[Zacatecas].[Hidalgo].[Store 18]}\n" + "{[Store].[USA].[CA].[Alameda].[HQ]}\n" + "{[Store].[USA].[CA].[Beverly Hills].[Store 6]}\n" + "{[Store].[USA].[CA].[Los Angeles].[Store 7]}\n" + "{[Store].[USA].[CA].[San Diego].[Store 24]}\n" + "{[Store].[USA].[CA].[San Francisco].[Store 14]}\n" + "{[Store].[USA].[OR].[Portland].[Store 11]}\n" + "{[Store].[USA].[OR].[Salem].[Store 13]}\n" + "{[Store].[USA].[WA].[Bellingham].[Store 2]}\n" + "{[Store].[USA].[WA].[Bremerton].[Store 3]}\n" + "{[Store].[USA].[WA].[Seattle].[Store 15]}\n" + "{[Store].[USA].[WA].[Spokane].[Store 16]}\n" + "{[Store].[USA].[WA].[Tacoma].[Store 17]}\n" + "{[Store].[USA].[WA].[Walla Walla].[Store 22]}\n" + "{[Store].[USA].[WA].[Yakima].[Store 23]}\n" + "Row #0: \n" + "Row #1: \n" + "Row #2: \n" + "Row #3: \n" + "Row #4: \n" + "Row #5: \n" + "Row #6: \n" + "Row #7: \n" + "Row #8: \n" + "Row #9: \n" + "Row #10: \n" + "Row #11: \n" + "Row #12: 45,750.24\n" + "Row #13: 54,545.28\n" + "Row #14: 54,431.14\n" + "Row #15: 4,441.18\n" + "Row #16: 55,058.79\n" + "Row #17: 87,218.28\n" + "Row #18: 4,739.23\n" + "Row #19: 52,896.30\n" + "Row #20: 52,644.07\n" + "Row #21: 49,634.46\n" + "Row #22: 74,843.96\n" + "Row #23: 4,705.97\n" + "Row #24: 24,329.23\n"); // test when the member arg is at a different level // from the set argument assertQueryReturns( "SELECT {[Measures].[Store Sales]} ON COLUMNS, " + "Filter({[Store].[Store Name].MEMBERS}, " + "[Store].[Store Name].CurrentMember IN " + "{[Store].[All Stores].[Mexico], " + "[Store].[All Stores].[USA]}) ON ROWS " + "FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Store Sales]}\n" + "Axis #2:\n"); } /** * Tests that the inferred return type is correct for a UDF whose return * type is not the same as would be guessed by the default implementation * of {@link mondrian.olap.fun.FunDefBase#getResultType}, which simply * guesses based on the type of the first argument. */ public void testNonGuessableReturnType() { TestContext tc = udfTestContext( "<UserDefinedFunction name=\"StringMult\" className=\"" + StringMultUdf.class.getName() + "\"/>\n"); // The default implementation of getResultType would assume that // StringMult(int, string) returns an int, whereas it returns a string. tc.assertExprReturns( "StringMult(5, 'foo') || 'bar'", "foofoofoofoofoobar"); } /** * Test case for the problem where a string expression gave a * ClassCastException because it was evaluating to a member, whereas the * member should have been evaluated to a scalar. */ public void testUdfToString() { TestContext tc = udfTestContext( "<UserDefinedFunction name=\"StringMult\" className=\"" + StringMultUdf.class.getName() + "\"/>\n"); tc.assertQueryReturns( "with member [Measures].[ABC] as StringMult(1, 'A')\n" + "member [Measures].[Unit Sales Formatted] as\n" + " [Measures].[Unit Sales],\n" + " FORMAT_STRING = '#,###|color=' ||\n" + " Iif([Measures].[ABC] = 'A', 'red', 'green')\n" + "select [Measures].[Unit Sales Formatted] on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales Formatted]}\n" + "Row #0: 266,773|color=red\n"); } /** * Tests a UDF whose return type is not the same as its first * parameter. The return type needs to have full dimensional information; * in this case, HierarchyType(dimension=Time, hierarchy=unknown). * * <p>Also tests applying a UDF to arguments of coercible type. In this * case, applies f(member,dimension) to args(member,hierarchy). */ public void testAnotherMemberFun() { final TestContext tc = udfTestContext( "<UserDefinedFunction name=\"PlusOne\" className=\"" + PlusOneUdf.class.getName() + "\"/>\n" + "<UserDefinedFunction name=\"AnotherMemberError\" className=\"" + AnotherMemberErrorUdf.class.getName() + "\"/>"); tc.assertQueryReturns( "WITH MEMBER [Measures].[Test] AS " + "'([Measures].[Store Sales],[Product].[Food],AnotherMemberError([Product].[Drink],[Time].[Time]))'\n" + "SELECT {[Measures].[Test]} ON COLUMNS, \n" + " {[Customers].DefaultMember} ON ROWS \n" + "FROM [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Test]}\n" + "Axis #2:\n" + "{[Customers].[All Customers]}\n" + "Row #0: 409,035.59\n"); } public void testCachingCurrentDate() { assertQueryReturns( "SELECT {filter([Time].[Month].Members, " + "[Time].[Time].CurrentMember in {CurrentDateMember([Time]" + ".[Time], '[\"Time\"]\\.[yyyy]\\.[\"Q\"q]\\.[m]', " + "BEFORE)})} ON COLUMNS " + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Time].[1998].[Q4].[12]}\n" + "Row #0: \n"); } /** * Test case for a UDF that returns a list. * * <p>Test case for bug * <a href="http://jira.pentaho.com/browse/MONDRIAN-588">MONDRIAN-588, * "UDF returning List works under 2.4, fails under 3.1.1"</a>. * * <p>Also test case for bug * <a href="http://jira.pentaho.com/browse/MONDRIAN-589">MONDRIAN-589, * "UDF expecting List gets anonymous * mondrian.rolap.RolapNamedSetEvaluator$1 instead"</a>. */ public void testListUdf() { checkListUdf(ReverseFunction.class); checkListUdf(ReverseIterableFunction.class); } /** * Helper for {@link #testListUdf()}. * * @param functionClass Class that implements the "Reverse" function. */ private void checkListUdf( final Class<? extends ReverseFunction> functionClass) { TestContext tc = udfTestContext( "<UserDefinedFunction name=\"Reverse\" className=\"" + functionClass.getName() + "\"/>\n"); final String expectedResult = "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Gender].[M]}\n" + "{[Gender].[F]}\n" + "{[Gender].[All Gender]}\n" + "Row #0: 135,215\n" + "Row #0: 131,558\n" + "Row #0: 266,773\n"; // UDF called directly in axis expression. tc.assertQueryReturns( "select Reverse([Gender].Members) on 0\n" + "from [Sales]", expectedResult); // UDF as calc set definition tc.assertQueryReturns( "with set [Foo] as Reverse([Gender].Members)\n" + "select [Foo] on 0\n" + "from [Sales]", expectedResult); // UDF applied to calc set -- exhibited MONDRIAN-589 tc.assertQueryReturns( "with set [Foo] as [Gender].Members\n" + "select Reverse([Foo]) on 0\n" + "from [Sales]", expectedResult); } /** * Tests that a non-static function gives an error. */ public void testNonStaticUdfFails() { TestContext tc = udfTestContext( "<UserDefinedFunction name=\"Reverse2\" className=\"" + ReverseFunctionNotStatic.class.getName() + "\"/>\n"); tc.assertQueryThrows( "select Reverse2([Gender].Members) on 0\n" + "from [Sales]", "Failed to load user-defined function 'Reverse2': class " + "'mondrian.test.UdfTest$ReverseFunctionNotStatic' must be " + "public " + "and static"); } /** * Tests a function that takes a member as argument. Want to make sure that * Mondrian leaves it as a member, does not try to evaluate it to a scalar * value. */ public void testMemberUdfDoesNotEvaluateToScalar() { TestContext tc = udfTestContext( "<UserDefinedFunction name=\"MemberName\" className=\"" + MemberNameFunction.class.getName() + "\"/>\n"); tc.assertExprReturns( "MemberName([Gender].[F])", "F"); } /** * Unit test that ensures that a UDF has either a script or a className. */ public void testUdfNeitherScriptNorClassname() { TestContext tc = udfTestContext( "<UserDefinedFunction name='StringMult'/>\n"); tc.assertQueryThrows( "select from [Sales]", "Must specify either className attribute or Script element"); } /** * Unit test that ensures that a UDF does not have both a script * and a className. */ public void testUdfBothScriptAndClassname() { TestContext tc = udfTestContext( "<UserDefinedFunction name='StringMult' className='foo'>\n" + " <Script>bar</Script>\n" + "</UserDefinedFunction>"); tc.assertQueryThrows( "select from [Sales]", "Must not specify both className attribute and Script element"); } /** * Unit test that ensures that a UDF has either a script or a className. */ public void testUdfScriptBadLanguage() { TestContext tc = udfTestContext( "<UserDefinedFunction name='StringMult'>\n" + " <Script language='bad'>bar</Script>\n" + "</UserDefinedFunction>"); tc.assertQueryThrows( "select from [Sales]", "Invalid script language 'bad'"); } /** * Unit test for a UDF defined in JavaScript. */ public void testScriptUdf() { TestContext tc = udfTestContext( "<UserDefinedFunction name='StringMult'>\n" + " <Script language='JavaScript'>\n" + " function getParameterTypes() {\n" + " return new Array(\n" + " new mondrian.olap.type.NumericType(),\n" + " new mondrian.olap.type.StringType());\n" + " }\n" + " function getReturnType(parameterTypes) {\n" + " return new mondrian.olap.type.StringType();\n" + " }\n" + " function execute(evaluator, arguments) {\n" + " var n = arguments[0].evaluateScalar(evaluator);\n" + " var s = arguments[1].evaluateScalar(evaluator);\n" + " var r = \"\";\n" + " while (n-- > 0) {\n" + " r = r + s;\n" + " }\n" + " return r;\n" + " }\n" + " </Script>\n" + "</UserDefinedFunction>\n"); tc.assertQueryReturns( "with member [Measures].[ABC] as StringMult(1, 'A')\n" + "member [Measures].[Unit Sales Formatted] as\n" + " [Measures].[Unit Sales],\n" + " FORMAT_STRING = '#,###|color=' ||\n" + " Iif([Measures].[ABC] = 'A', 'red', 'green')\n" + "select [Measures].[Unit Sales Formatted] on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales Formatted]}\n" + "Row #0: 266,773|color=red\n"); } /** * Unit test for a UDF defined in JavaScript, this time the factorial * function. We also use 'CDATA' section to mask the '<' symbol. */ public void testScriptUdfFactorial() { TestContext tc = udfTestContext( "<UserDefinedFunction name='Factorial'>\n" + " <Script language='JavaScript'><![CDATA[\n" + " function getParameterTypes() {\n" + " return new Array(\n" + " new mondrian.olap.type.NumericType());\n" + " }\n" + " function getReturnType(parameterTypes) {\n" + " return new mondrian.olap.type.NumericType();\n" + " }\n" + " function execute(evaluator, arguments) {\n" + " var n = arguments[0].evaluateScalar(evaluator);\n" + " return factorial(n);\n" + " }\n" + " function factorial(n) {\n" + " return n <= 1 ? 1 : n * factorial(n - 1);\n" + " }\n" + " ]]>\n" + " </Script>\n" + "</UserDefinedFunction>\n"); tc.assertExprReturns( "Factorial(4 + 2)", "720"); } /** * Unit test that we get a nice error if a script UDF contains an error. */ public void testScriptUdfInvalid() { TestContext tc = udfTestContext( "<UserDefinedFunction name='Factorial'>\n" + " <Script language='JavaScript'><![CDATA[\n" + " function getParameterTypes() {\n" + " return new Array(\n" + " new mondrian.olap.type.NumericType());\n" + " }\n" + " function getReturnType(parameterTypes) {\n" + " return new mondrian.olap.type.NumericType();\n" + " }\n" + " function execute(evaluator, arguments) {\n" + " var n = arguments[0].evaluateScalar(evaluator);\n" + " return factorial(n);\n" + " }\n" + " function factorial(n) {\n" + " return n <= 1 ? 1 : n * factorial_xx(n - 1);\n" + " }\n" + " ]]>\n" + " </Script>\n" + "</UserDefinedFunction>\n"); final Cell cell = tc.executeExprRaw("Factorial(4 + 2)"); getTestContext().assertMatchesVerbose( Pattern.compile( "(?s).*ReferenceError: \"factorial_xx\" is not defined..*"), cell.getValue().toString()); } /** * Unit test for a cell formatter defined in the old way -- a 'formatter' * attribute of a Measure element. */ public void testCellFormatter() { // Note that // formatString="Standard" // is ignored. TestContext tc = measureTestContext( "<Measure name='Unit Sales Foo Bar' column='unit_sales'\n" + " aggregator='sum' formatString='Standard' formatter='" + FooBarCellFormatter.class.getName() + "'/>"); tc.assertQueryReturns( "select {[Measures].[Unit Sales],\n" + " [Measures].[Unit Sales Foo Bar]} on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "{[Measures].[Unit Sales Foo Bar]}\n" + "Row #0: 266,773\n" + "Row #0: foo266773.0bar\n"); } /** * As {@link #testCellFormatter()}, but using new-style nested * CellFormatter element. */ public void testCellFormatterNested() { // Note that // formatString="Standard" // is ignored. TestContext tc = measureTestContext( "<Measure name='Unit Sales Foo Bar' column='unit_sales'\n" + " aggregator='sum' formatString='Standard'>\n" + " <CellFormatter className='" + FooBarCellFormatter.class.getName() + "'/>\n" + "</Measure>"); tc.assertQueryReturns( "select {[Measures].[Unit Sales],\n" + " [Measures].[Unit Sales Foo Bar]} on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "{[Measures].[Unit Sales Foo Bar]}\n" + "Row #0: 266,773\n" + "Row #0: foo266773.0bar\n"); } /** * As {@link #testCellFormatterNested()}, but using a script. */ public void testCellFormatterScript() { TestContext tc = measureTestContext( "<Measure name='Unit Sales Foo Bar' column='unit_sales'\n" + " aggregator='sum' formatString='Standard'>\n" + " <CellFormatter>\n" + " <Script>\n" + " return \"foo\" + value + \"bar\";\n" + " </Script>\n" + " </CellFormatter>\n" + "</Measure>"); // Note that the result is slightly different to above (a missing ".0"). // Not a great concern -- in fact it proves that the scripted UDF is // being used. tc.assertQueryReturns( "select {[Measures].[Unit Sales],\n" + " [Measures].[Unit Sales Foo Bar]} on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "{[Measures].[Unit Sales Foo Bar]}\n" + "Row #0: 266,773\n" + "Row #0: foo266773bar\n"); } /** * Unit test for a cell formatter defined against a calculated member, * using the old syntax (a member property called "CELL_FORMATTER"). */ public void testCellFormatterOnCalcMember() { TestContext tc = calcMemberTestContext( "<CalculatedMember\n" + " name='Unit Sales Foo Bar'\n" + " dimension='Measures'>\n" + " <Formula>[Measures].[Unit Sales]</Formula>\n" + " <CalculatedMemberProperty name='CELL_FORMATTER' value='" + FooBarCellFormatter.class.getName() + "'/>\n" + "</CalculatedMember>"); tc.assertQueryReturns( "select {[Measures].[Unit Sales],\n" + " [Measures].[Unit Sales Foo Bar]} on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "{[Measures].[Unit Sales Foo Bar]}\n" + "Row #0: 266,773\n" + "Row #0: foo266773.0bar\n"); } /** * Unit test for a cell formatter defined against a calculated member, * using the new syntax (a nested CellFormatter element). */ public void testCellFormatterOnCalcMemberNested() { TestContext tc = calcMemberTestContext( "<CalculatedMember\n" + " name='Unit Sales Foo Bar'\n" + " dimension='Measures'>\n" + " <Formula>[Measures].[Unit Sales]</Formula>\n" + " <CellFormatter className='" + FooBarCellFormatter.class.getName() + "'/>\n" + "</CalculatedMember>"); tc.assertQueryReturns( "select {[Measures].[Unit Sales],\n" + " [Measures].[Unit Sales Foo Bar]} on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "{[Measures].[Unit Sales Foo Bar]}\n" + "Row #0: 266,773\n" + "Row #0: foo266773.0bar\n"); } /** * Unit test for a cell formatter defined against a calculated member, * using a script. */ public void testCellFormatterOnCalcMemberScript() { TestContext tc = calcMemberTestContext( "<CalculatedMember\n" + " name='Unit Sales Foo Bar'\n" + " dimension='Measures'>\n" + " <Formula>[Measures].[Unit Sales]</Formula>\n" + " <CellFormatter>\n" + " <Script>\n" + " return \"foo\" + value + \"bar\";\n" + " </Script>\n" + " </CellFormatter>\n" + "</CalculatedMember>"); tc.assertQueryReturns( "select {[Measures].[Unit Sales],\n" + " [Measures].[Unit Sales Foo Bar]} on 0\n" + "from [Sales]", "Axis #0:\n" + "{}\n" + "Axis #1:\n" + "{[Measures].[Unit Sales]}\n" + "{[Measures].[Unit Sales Foo Bar]}\n" + "Row #0: 266,773\n" + "Row #0: foo266773bar\n"); } /** * Unit test for a member formatter defined in the old way -- a 'formatter' * attribute of a Measure element. */ public void testMemberFormatter() { TestContext tc = TestContext.instance().createSubstitutingCube( "Sales", " <Dimension name='Promotion Media2' foreignKey='promotion_id'>\n" + " <Hierarchy hasAll='true' allMemberName='All Media' primaryKey='promotion_id'>\n" + " <Table name='promotion'/>\n" + " <Level name='Media Type' column='media_type'\n" + " uniqueMembers='true' formatter='" + FooBarMemberFormatter.class.getName() + "'/>\n" + " </Hierarchy>\n" + " </Dimension>"); tc.assertExprReturns( "[Promotion Media2].FirstChild.Caption", "fooBulk Mailbar"); } /** * As {@link #testMemberFormatter()}, but using new-style nested * memberFormatter element. */ public void testMemberFormatterNested() { TestContext tc = TestContext.instance().createSubstitutingCube( "Sales", " <Dimension name='Promotion Media2' foreignKey='promotion_id'>\n" + " <Hierarchy hasAll='true' allMemberName='All Media' primaryKey='promotion_id'>\n" + " <Table name='promotion'/>\n" + " <Level name='Media Type' column='media_type'\n" + " uniqueMembers='true'>\n" + " <MemberFormatter className='" + FooBarMemberFormatter.class.getName() + "'/>\n" + " </Level>\n" + " </Hierarchy>\n" + " </Dimension>"); tc.assertExprReturns( "[Promotion Media2].FirstChild.Caption", "fooBulk Mailbar"); } /** * As {@link #testMemberFormatterNested()}, but using a script. */ public void testMemberFormatterScript() { TestContext tc = TestContext.instance().createSubstitutingCube( "Sales", " <Dimension name='Promotion Media2' foreignKey='promotion_id'>\n" + " <Hierarchy hasAll='true' allMemberName='All Media' primaryKey='promotion_id'>\n" + " <Table name='promotion'/>\n" + " <Level name='Media Type' column='media_type'\n" + " uniqueMembers='true'>\n" + " <MemberFormatter>\n" + " <Script language='JavaScript'>\n" + " return \"foo\" + member.getName() + \"bar\"\n" + " </Script>\n" + " </MemberFormatter>\n" + " </Level>\n" + " </Hierarchy>\n" + " </Dimension>"); tc.assertExprReturns( "[Promotion Media2].FirstChild.Caption", "fooBulk Mailbar"); } /** * Unit test for a property formatter defined in the old way -- a * 'formatter' attribute of a Property element. * * @throws java.sql.SQLException on error */ public void testPropertyFormatter() throws SQLException { TestContext tc = TestContext.instance().createSubstitutingCube( "Sales", "<Dimension name='Promotions2' foreignKey='promotion_id'>\n" + " <Hierarchy hasAll='true' allMemberName='All Promotions' primaryKey='promotion_id' defaultMember='[All Promotions]'>\n" + " <Table name='promotion'/>\n" + " <Level name='Promotion Name' column='promotion_id' uniqueMembers='true'>\n" + " <Property name='Medium' column='media_type' formatter='" + FooBarPropertyFormatter.class.getName() + "'/>\n" + " </Level>\n" + " </Hierarchy>\n" + "</Dimension>"); final CellSet result = tc.executeOlap4jQuery( "select [Promotions2].Children on 0\n" + "from [Sales]"); final org.olap4j.metadata.Member member = result.getAxes().get(0).getPositions().get(0).getMembers().get(0); final Property property = member.getProperties().get("Medium"); assertEquals( "foo0/Medium/No Mediabar", member.getPropertyFormattedValue(property)); } /** * As {@link #testPropertyFormatter()}, but using new-style nested * PropertyFormatter element. * * @throws java.sql.SQLException on error */ public void testPropertyFormatterNested() throws SQLException { TestContext tc = TestContext.instance().createSubstitutingCube( "Sales", "<Dimension name='Promotions2' foreignKey='promotion_id'>\n" + " <Hierarchy hasAll='true' allMemberName='All Promotions' primaryKey='promotion_id' defaultMember='[All Promotions]'>\n" + " <Table name='promotion'/>\n" + " <Level name='Promotion Name' column='promotion_id' uniqueMembers='true'>\n" + " <Property name='Medium' column='media_type'>\n" + " <PropertyFormatter className='" + FooBarPropertyFormatter.class.getName() + "'/>\n" + " </Property>\n" + " </Level>\n" + " </Hierarchy>\n" + "</Dimension>"); final CellSet result = tc.executeOlap4jQuery( "select [Promotions2].Children on 0\n" + "from [Sales]"); final org.olap4j.metadata.Member member = result.getAxes().get(0).getPositions().get(0).getMembers().get(0); final Property property = member.getProperties().get("Medium"); assertEquals( "foo0/Medium/No Mediabar", member.getPropertyFormattedValue(property)); } /** * As {@link #testPropertyFormatterNested()}, but using a script. * * @throws java.sql.SQLException on error */ public void testPropertyFormatterScript() throws SQLException { TestContext tc = TestContext.instance().createSubstitutingCube( "Sales", "<Dimension name='Promotions2' foreignKey='promotion_id'>\n" + " <Hierarchy hasAll='true' allMemberName='All Promotions' primaryKey='promotion_id' defaultMember='[All Promotions]'>\n" + " <Table name='promotion'/>\n" + " <Level name='Promotion Name' column='promotion_id' uniqueMembers='true'>\n" + " <Property name='Medium' column='media_type'>\n" + " <PropertyFormatter>\n" + " <Script language='JavaScript'>\n" + " return \"foo\" + member.getName() + \"/\"\n" + " + propertyName + \"/\"\n" + " + propertyValue + \"bar\";\n" + " </Script>\n" + " </PropertyFormatter>\n" + " </Property>\n" + " </Level>\n" + " </Hierarchy>\n" + "</Dimension>"); final CellSet result = tc.executeOlap4jQuery( "select [Promotions2].Children on 0\n" + "from [Sales]"); final org.olap4j.metadata.Member member = result.getAxes().get(0).getPositions().get(0).getMembers().get(0); final Property property = member.getProperties().get("Medium"); assertEquals( "foo0/Medium/No Mediabar", member.getPropertyFormattedValue(property)); } private static String MDC_KEY = "Chunky Bacon!"; private static Object MDC_OBJECT = new Object(); /** * This is a test for * <a href="http://jira.pentaho.com/browse/MONDRIAN-994">MONDRIAN-994</a>. * It checks that the MDC logging context is passed through all the * threads. */ public void testMdc() { final TestContext context = udfTestContext( "<UserDefinedFunction name=\"Mdc\" className=\"" + MdcUdf.class.getName() + "\"/>\n"); MDC.put(MDC_KEY, MDC_OBJECT); try { context.executeQuery( "with member [Measures].[MDC] as 'Mdc([Measures].[Unit Sales])' " + "select {[Measures].[MDC]} on columns from [Sales]"); } finally { MDC.remove(MDC_KEY); } } // ~ Inner classes -------------------------------------------------------- /** * A simple UDF that checks the object inside of the MDC logging context. */ public static class MdcUdf implements UserDefinedFunction { public String getName() { return "Mdc"; } public String getDescription() { return ""; } public Syntax getSyntax() { return Syntax.Function; } public Type getReturnType(Type[] parameterTypes) { return new NumericType(); } public Type[] getParameterTypes() { return new Type[] {new NumericType()}; } public Object execute(Evaluator evaluator, Argument[] arguments) { Map<String, Object> context = org.apache.log4j.MDC.getContext(); if (!context.containsKey(MDC_KEY) || context.get(MDC_KEY) != MDC_OBJECT) { fail(); } return arguments[0].evaluateScalar(evaluator); } public String[] getReservedWords() { return null; } } /** * A simple user-defined function which adds one to its argument. */ public static class PlusOneUdf implements UserDefinedFunction { public String getName() { return "PlusOne"; } public String getDescription() { return "Returns its argument plus one"; } public Syntax getSyntax() { return Syntax.Function; } public Type getReturnType(Type[] parameterTypes) { return new NumericType(); } public Type[] getParameterTypes() { return new Type[] {new NumericType()}; } public Object execute(Evaluator evaluator, Argument[] arguments) { final Object argValue = arguments[0].evaluateScalar(evaluator); if (argValue instanceof Number) { return ((Number) argValue).doubleValue() + 1.0; } else { // Argument might be a RuntimeException indicating that // the cache does not yet have the required cell value. The // function will be called again when the cache is loaded. return null; } } public String[] getReservedWords() { return null; } } /** * A simple user-defined function which adds one to its argument. */ public static class BadPlusOneUdf extends PlusOneUdf { private final String name; public BadPlusOneUdf(String name) { this.name = name; } public String getName() { return name; } public Type getReturnType(Type[] parameterTypes) { // Will cause error. return null; } } /** * A user-defined function which, depending on its given name, either adds * one to, or subtracts one from, its argument. */ public static class PlusOrMinusOneUdf implements UserDefinedFunction { private final String name; public PlusOrMinusOneUdf(String name) { if (!(name.equals("GenericPlusOne") || name.equals("GenericMinusOne"))) { throw new IllegalArgumentException(); } this.name = name; } public String getName() { return name; } public String getDescription() { return "A user-defined function which, depending on its given name, " + "either addsone to, or subtracts one from, its argument"; } public Syntax getSyntax() { return Syntax.Function; } public Type getReturnType(Type[] parameterTypes) { return new NumericType(); } public String[] getReservedWords() { return null; } public Type[] getParameterTypes() { return new Type[] {new NumericType()}; } public Object execute(Evaluator evaluator, Argument[] arguments) { final Object argValue = arguments[0].evaluateScalar(evaluator); if (argValue instanceof Number) { return ((Number) argValue).doubleValue() + (name.equals("GenericPlusOne") ? 1.0 : -1.0); } else { // Argument might be a RuntimeException indicating that // the cache does not yet have the required cell value. The // function will be called again when the cache is loaded. return null; } } } /** * The "TimesString" user-defined function. We wanted a function whose * actual return type (string) is not the same as the guessed return type * (integer). */ public static class StringMultUdf implements UserDefinedFunction { public String getName() { return "StringMult"; } public String getDescription() { return "Returns N copies of its string argument"; } public Syntax getSyntax() { return Syntax.Function; } public Type getReturnType(Type[] parameterTypes) { return new StringType(); } public Type[] getParameterTypes() { return new Type[] { new NumericType(), new StringType() }; } public Object execute(Evaluator evaluator, Argument[] arguments) { final Object argValue = arguments[0].evaluateScalar(evaluator); int n; if (argValue instanceof Number) { n = ((Number) argValue).intValue(); } else { // Argument might be a RuntimeException indicating that // the cache does not yet have the required cell value. The // function will be called again when the cache is loaded. return null; } String s; final Object argValue2 = arguments[1].evaluateScalar(evaluator); if (argValue2 instanceof String) { s = (String) argValue2; } else { return null; } if (n < 0) { return null; } StringBuilder buf = new StringBuilder(s.length() * n); for (int i = 0; i < n; i++) { buf.append(s); } return buf.toString(); } public String[] getReservedWords() { return null; } } /** * A user-defined function which returns ignores its first parameter (a * member) and returns the default member from the second parameter (a * hierarchy). */ public static class AnotherMemberErrorUdf implements UserDefinedFunction { public String getName() { return "AnotherMemberError"; } public String getDescription() { return "Returns default member from hierarchy, " + "specified as a second parameter. " + "First parameter - any member from any hierarchy"; } public Syntax getSyntax() { return Syntax.Function; } public Type getReturnType(Type[] parameterTypes) { HierarchyType hierType = (HierarchyType) parameterTypes[1]; return MemberType.forType(hierType); } public Type[] getParameterTypes() { return new Type[] { // The first argument must be a member. MemberType.Unknown, // The second argument must be a hierarchy. HierarchyType.Unknown }; } public Object execute(Evaluator evaluator, Argument[] arguments) { // Simply ignore first parameter Member member = (Member)arguments[0].evaluate(evaluator); Util.discard(member); Hierarchy hierarchy = (Hierarchy)arguments[1].evaluate(evaluator); return hierarchy.getDefaultMember(); } public String[] getReservedWords() { return null; } } /** * Function that reverses a list of members. */ public static class ReverseFunction implements UserDefinedFunction { public Object execute(Evaluator eval, Argument[] args) { // Note: must call Argument.evaluateList. If we call // Argument.evaluate we may get an Iterable. List<?> list = args[0].evaluateList(eval); // We do need to copy before we reverse. The list is not guaranteed // to be mutable. list = new ArrayList(list); Collections.reverse(list); return list; } public String getDescription() { return "Reverses the order of a set"; } public String getName() { return "Reverse"; } public Type[] getParameterTypes() { return new Type[] {new SetType(MemberType.Unknown)}; } public String[] getReservedWords() { return null; } public Type getReturnType(Type[] arg0) { return arg0[0]; } public Syntax getSyntax() { return Syntax.Function; } } /** * Function that is non-static. */ public class ReverseFunctionNotStatic extends ReverseFunction { } /** * Function that takes a set of members as argument, and returns a set of * members. */ public static class ReverseIterableFunction extends ReverseFunction { public Object execute(Evaluator eval, Argument[] args) { // Note: must call Argument.evaluateList. If we call // Argument.evaluate we may get an Iterable. Iterable iterable = args[0].evaluateIterable(eval); List<Object> list = new ArrayList<Object>(); for (Object o : iterable) { list.add(o); } Collections.reverse(list); return list; } } /** * Function that takes a member and returns a name. */ public static class MemberNameFunction implements UserDefinedFunction { public Object execute(Evaluator eval, Argument[] args) { Member member = (Member) args[0].evaluate(eval); return member.getName(); } public String getDescription() { return "Returns the name of a member"; } public String getName() { return "MemberName"; } public Type[] getParameterTypes() { return new Type[] {MemberType.Unknown}; } public String[] getReservedWords() { return null; } public Type getReturnType(Type[] arg0) { return new StringType(); } public Syntax getSyntax() { return Syntax.Function; } } /** * Member formatter for test purposes. Returns name of the member prefixed * with "foo" and suffixed with "bar". */ public static class FooBarMemberFormatter implements MemberFormatter { public String formatMember(Member member) { return "foo" + member.getName() + "bar"; } } /** * Cell formatter for test purposes. Returns value of the cell prefixed * with "foo" and suffixed with "bar". */ public static class FooBarCellFormatter implements CellFormatter { public String formatCell(Object value) { return "foo" + value + "bar"; } } /** * Property formatter for test purposes. Returns name of the member and * property, then the value, prefixed with "foo" and suffixed with "bar". */ public static class FooBarPropertyFormatter implements PropertyFormatter { public String formatProperty( Member member, String propertyName, Object propertyValue) { return "foo" + member.getName() + "/" + propertyName + "/" + propertyValue + "bar"; } } } // End UdfTest.java