/*
* 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) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package mondrian.rolap;
import mondrian.olap.*;
import mondrian.olap.CacheControl.MemberEditCommand;
import mondrian.olap.Hierarchy;
import mondrian.rolap.agg.AggregationManager;
import mondrian.server.Execution;
import mondrian.server.Locus;
import mondrian.server.Statement;
import mondrian.test.*;
import org.apache.log4j.*;
import org.apache.log4j.Level;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
/**
* Unit tests for flushing member cache and editing cached member properties.
*
* <p>The purpose of the cache control API is to clear the cache so that
* changes made to the DBMS can be seen. However, it is difficult to write
* tests that modify the database. So these tests just check that the relevant
* caches have been cleared. It is assumed that the updated values will be
* loaded next time mondrian goes to the database.
*
* @author mberkowitz
* @since Jan 2008
*/
public class MemberCacheControlTest extends FoodMartTestCase {
private Locus locus;
// TODO: add multi-thread tests.
// TODO: test set properties negative: refer to invalid property
// TODO: test set properties negative: set prop to invalid value
// TODO: edit a different member not known to be in cache -- will it be
// fetched?
public MemberCacheControlTest() {
}
public MemberCacheControlTest(String name) {
super(name);
}
protected void setUp() throws Exception {
super.setUp();
propSaver.set(
MondrianProperties.instance().EnableRolapCubeMemberCache,
false);
RolapSchemaPool.instance().clear();
final RolapConnection conn = (RolapConnection) getConnection();
final Statement statement = conn.getInternalStatement();
final Execution execution = new Execution(statement, 0);
locus = new Locus(execution, getName(), null);
Locus.push(locus);
}
protected void tearDown() throws Exception {
super.tearDown();
RolapSchemaPool.instance().clear();
Locus.pop(locus);
locus = null;
}
// ~ Utility methods ------------------------------------------------------
DiffRepository getDiffRepos() {
return DiffRepository.lookup(MemberCacheControlTest.class);
}
public TestContext getTestContext() {
TestContext testContext = TestContext.instance().createSubstitutingCube(
"Sales",
// Reduced size Store dimension. Omits the 'Store Country' level,
// and adds properties to non-leaf levels.
" <Dimension name=\"Retail\" foreignKey=\"store_id\">\n"
+ " <Hierarchy hasAll=\"true\" primaryKey=\"store_id\">\n"
+ " <Table name=\"store\"/>\n"
+ " <Level name=\"State\" column=\"store_state\" uniqueMembers=\"true\">\n"
+ " <Property name=\"Country\" column=\"store_country\"/>\n"
+ " </Level>\n"
+ " <Level name=\"City\" column=\"store_city\" uniqueMembers=\"true\">\n"
+ " <Property name=\"Population\" column=\"store_postal_code\"/>\n"
+ " </Level>\n"
+ " <Level name=\"Name\" column=\"store_name\" uniqueMembers=\"true\">\n"
+ " <Property name=\"Store Type\" column=\"store_type\"/>\n"
+ " <Property name=\"Store Manager\" column=\"store_manager\"/>\n"
+ " <Property name=\"Store Sqft\" column=\"store_sqft\" type=\"Numeric\"/>\n"
+ " <Property name=\"Has coffee bar\" column=\"coffee_bar\" type=\"Boolean\"/>\n"
+ " <Property name=\"Street address\" column=\"store_street_address\" type=\"String\"/>\n"
+ " </Level>\n"
+ " </Hierarchy>\n"
+ " </Dimension>");
return testContext;
}
/**
* Creates a map.
*
* @param keys Keys
* @param values Values
* @return Map
*/
private static <K, V> Map<K, V> createMap(List<K> keys, List<V> values) {
assert keys.size() == values.size();
final Map<K, V> map = new HashMap<K, V>(keys.size());
for (int i = 0; i < keys.size(); ++i) {
map.put(keys.get(i), values.get(i));
}
return map;
}
/**
* Finds a Member by its name and the name of its containing cube.
*
* @param tc Test context
* @param cubeName Cube name
* @param names the full-qualified Member name
* @return the Member
* @throws MondrianException when not found.
*/
protected static RolapMember findMember(
TestContext tc,
String cubeName,
String... names)
{
Cube cube = tc.getConnection().getSchema().lookupCube(cubeName, true);
SchemaReader scr = cube.getSchemaReader(null).withLocus();
return (RolapMember)
scr.getMemberByUniqueName(Id.Segment.toList(names), true);
}
/**
* Prints all properties of a Member.
*
* @param pw Print writer
* @param member Member
* @return the same print writer
*/
private static PrintWriter printMemberProperties(
PrintWriter pw,
Member member)
{
pw.print(member.getUniqueName());
pw.print(" {");
int k = -1;
for (Property p : member.getLevel().getProperties()) {
if (++k > 0) {
pw.print(",");
}
pw.println();
String name = p.getName();
Object value = member.getPropertyValue(name);
// Fixup value for different database representations of boolean and
// numeric values.
if (value == null) {
// no fixup needed
} else if (name.equals("Has coffee bar")) {
if (value instanceof Number) {
value = ((Number) value).intValue() != 0;
}
} else if (name.endsWith(" Sqft")) {
Number number = (Number) value;
value =
number.equals(number.intValue())
? number.intValue()
: Math.round(number.floatValue());
}
pw.print(" [");
pw.print(name);
pw.print("]=[");
pw.print(value);
pw.print("]");
}
pw.println("}");
return pw;
}
/**
* Prints properties of all Members on an Axis.
*
* @param pw Print writer
* @param axis Axis
* @return the same print writer
*/
private static PrintWriter printMemberProperties(
PrintWriter pw,
Axis axis)
{
for (Position pos : axis.getPositions()) {
for (Member m : pos) {
printMemberProperties(pw, m).println();
}
}
return pw;
}
/**
* Prints properties of the Row Axis from a Result.
*
* @param pw Print writer
* @param result Result
* @return the same print writer
*/
private static PrintWriter printRowMemberProperties(
PrintWriter pw,
Result result)
{
return printMemberProperties(
pw,
result.getAxes()[
AxisOrdinal.StandardAxisOrdinal.ROWS.logicalOrdinal()]);
}
private static String getRowMemberPropertiesAsString(Result r) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
printRowMemberProperties(pw, r);
pw.flush();
return sw.toString();
}
private CacheControl.MemberSet createInterestingMemberSet(
TestContext tc, CacheControl cc)
{
return cc.createUnionSet(
// all stores in OR
cc.createMemberSet(findMember(tc, "Sales", "Retail", "OR"), true),
// all stores in Hidalgo, Zacatecas
cc.createMemberSet(
findMember(tc, "Sales", "Retail", "Zacatecas", "Hidalgo"),
true),
// a single store
cc.createMemberSet(
findMember(tc, "Sales", "Retail", "CA", "Alameda", "HQ"),
false),
// a range of stores
cc.createMemberSet(
true, findMember(tc, "Sales", "Retail", "WA", "Bremerton"),
true, findMember(tc, "Sales", "Retail", "Yucatan", "Merida"),
false),
// all stores in a range of states
cc.createMemberSet(
true, findMember(tc, "Sales", "Retail", "DF"),
true, findMember(tc, "Sales", "Retail", "Jalisco"),
true));
}
// ~ Tests ----------------------------------------------------------------
/**
* Tests operations on member sets, in particular the
* {@link mondrian.olap.CacheControl#filter} method.
*/
public void testFilter() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final DiffRepository dr = getDiffRepos();
final CacheControl cc = conn.getCacheControl(null);
CacheControl.MemberSet memberSet = createInterestingMemberSet(tc, cc);
dr.assertEquals("before", "${before}", memberSet.toString());
final Member orMember = findMember(tc, "Sales", "Retail", "OR");
final CacheControl.MemberSet filteredMemberSet =
cc.filter(orMember.getLevel(), memberSet);
dr.assertEquals("after", "${after}", filteredMemberSet.toString());
}
/**
* Tests that member operations fail if cache is enabled.
*/
public void testMemberOpsFailIfCacheEnabled() {
propSaver.set(
MondrianProperties.instance().EnableRolapCubeMemberCache,
true);
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final CacheControl cc = conn.getCacheControl(null);
final CacheControl.MemberEditCommand command =
cc.createDeleteCommand(findMember(tc, "Sales", "Retail", "OR"));
try {
cc.execute(command);
fail("expected exception");
} catch (IllegalArgumentException e) {
assertEquals(
"Member cache control operations are not allowed unless "
+ "property mondrian.rolap.EnableRolapCubeMemberCache is "
+ "false",
e.getMessage());
}
}
/**
* Test that edits the properties of a single leaf Member.
*/
public void testSetPropertyCommandOnLeafMember() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final DiffRepository dr = getDiffRepos();
final CacheControl cc = conn.getCacheControl(null);
// A query that refers to a single leaf Member fetches the Member.
// Changing Member properties does not affect Cell boundaries, so we
// check that the MDX results are invariant.
String mdx =
"SELECT {[Measures].[Unit Sales]} ON COLUMNS,"
+ " {[Store].[USA].[CA].[San Francisco].[Store 14]}"
+ " ON ROWS FROM [Sales]";
Query q = conn.parseQuery(mdx);
Result r = conn.execute(q);
dr.assertEquals(
"props before",
"${props before}",
getRowMemberPropertiesAsString(r));
final String resultString = TestContext.toString(r);
dr.assertEquals(
"result before",
"${result before}",
resultString);
// Change properties
Member m =
findMember(
tc, "Sales", "Store", "USA", "CA", "San Francisco", "Store 14");
cc.execute(cc.createSetPropertyCommand(m, "Store Manager", "Higgins"));
cc.execute(
cc.createCompoundCommand(
Arrays.asList(
cc.createSetPropertyCommand(
m, "Street address", "770 Mission St"),
cc.createSetPropertyCommand(m, "Store Sqft", 6000),
cc.createSetPropertyCommand(
m, "Has coffee bar", "false"))));
// Repeat same query; verify properties are changed.
// Changing properties does not affect measures, so results unchanged.
r = conn.execute(q);
dr.assertEquals(
"props after",
"${props after}",
getRowMemberPropertiesAsString(r));
assertEquals(
resultString,
TestContext.toString(r));
}
/**
* Test that edits properties of Members at various Levels (use Retail
* Dimension), but leaves grouping unchanged, so results not changed.
*/
public void testSetPropertyCommandOnNonLeafMember() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final DiffRepository dr = getDiffRepos();
final CacheControl cc = tc.getConnection().getCacheControl(null);
String mdx = "SELECT {[Measures].[Unit Sales]} ON COLUMNS,"
+ " {[Retail].Members} ON ROWS "
+ "FROM [Sales]";
Query q = conn.parseQuery(mdx);
Result r = conn.execute(q);
dr.assertEquals(
"props before",
"${props before}",
getRowMemberPropertiesAsString(r));
final String resultString = TestContext.toString(r);
dr.assertEquals(
"result before",
"${result before}",
resultString);
// Change some properties (TODO: change dimension table first)
// set 2 properties (TODO: set both with one command)
// try all ways to construct MemberSets
CacheControl.MemberSet memberSet = createInterestingMemberSet(tc, cc);
final Map<String, Object> propertyValues =
createMap(
Arrays.asList("Has coffee bar", "Store Sqft"),
Arrays.asList((Object) "true", 123));
CacheControl.MemberEditCommand command;
// first, the member set contains members of various levels
try {
command = cc.createSetPropertyCommand(memberSet, propertyValues);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals(
"all members in set must belong to same level",
e.getMessage());
}
// after we filter set to just members of store level we're ok
final Member hqMember =
findMember(tc, "Sales", "Retail", "CA", "Alameda", "HQ");
final CacheControl.MemberSet filteredMemberSet =
cc.filter(hqMember.getLevel(), memberSet);
command =
cc.createSetPropertyCommand(filteredMemberSet, propertyValues);
cc.execute(command);
// Repeat same query; verify properties were changed.
// Changing properties does not affect measures, so results unchanged.
r = conn.execute(q);
dr.assertEquals(
"props after",
"${props after}",
getRowMemberPropertiesAsString(r));
assertEquals(
resultString,
TestContext.toString(r));
}
public void testAddCommand() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final CacheControl cc = conn.getCacheControl(null);
final RolapCubeMember caCubeMember =
(RolapCubeMember) findMember(tc, "Sales", "Retail", "CA");
final RolapMember caMember = caCubeMember.member;
final RolapMember rootMember = caMember.getParentMember();
final RolapHierarchy hierarchy = caMember.getHierarchy();
final RolapMember berkeleyMember =
(RolapMember) hierarchy.createMember(
caMember,
caMember.getLevel().getChildLevel(),
"Berkeley",
null);
final RolapBaseCubeMeasure unitSalesCubeMember =
(RolapBaseCubeMeasure) findMember(
tc, "Sales", "Measures", "Unit Sales");
final RolapCubeMember yearCubeMember =
(RolapCubeMember) findMember(
tc, "Sales", "Time", "Year", "1997");
final Member[] cacheRegionMembers =
new Member[] {
unitSalesCubeMember,
caCubeMember,
yearCubeMember
};
tc.assertQueryReturns(
"select {[Retail].[City].Members} on columns from [Sales]",
"Axis #0:\n"
+ "{}\n"
+ "Axis #1:\n"
+ "{[Retail].[BC].[Vancouver]}\n"
+ "{[Retail].[BC].[Victoria]}\n"
+ "{[Retail].[CA].[Alameda]}\n"
+ "{[Retail].[CA].[Beverly Hills]}\n"
+ "{[Retail].[CA].[Los Angeles]}\n"
+ "{[Retail].[CA].[San Diego]}\n"
+ "{[Retail].[CA].[San Francisco]}\n"
+ "{[Retail].[DF].[Mexico City]}\n"
+ "{[Retail].[DF].[San Andres]}\n"
+ "{[Retail].[Guerrero].[Acapulco]}\n"
+ "{[Retail].[Jalisco].[Guadalajara]}\n"
+ "{[Retail].[OR].[Portland]}\n"
+ "{[Retail].[OR].[Salem]}\n"
+ "{[Retail].[Veracruz].[Orizaba]}\n"
+ "{[Retail].[WA].[Bellingham]}\n"
+ "{[Retail].[WA].[Bremerton]}\n"
+ "{[Retail].[WA].[Seattle]}\n"
+ "{[Retail].[WA].[Spokane]}\n"
+ "{[Retail].[WA].[Tacoma]}\n"
+ "{[Retail].[WA].[Walla Walla]}\n"
+ "{[Retail].[WA].[Yakima]}\n"
+ "{[Retail].[Yucatan].[Merida]}\n"
+ "{[Retail].[Zacatecas].[Camacho]}\n"
+ "{[Retail].[Zacatecas].[Hidalgo]}\n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: 21,333\n"
+ "Row #0: 25,663\n"
+ "Row #0: 25,635\n"
+ "Row #0: 2,117\n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: 26,079\n"
+ "Row #0: 41,580\n"
+ "Row #0: \n"
+ "Row #0: 2,237\n"
+ "Row #0: 24,576\n"
+ "Row #0: 25,011\n"
+ "Row #0: 23,591\n"
+ "Row #0: 35,257\n"
+ "Row #0: 2,203\n"
+ "Row #0: 11,491\n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n");
tc.assertAxisReturns(
"[Retail].[CA].Children",
"[Retail].[CA].[Alameda]\n"
+ "[Retail].[CA].[Beverly Hills]\n"
+ "[Retail].[CA].[Los Angeles]\n"
+ "[Retail].[CA].[San Diego]\n"
+ "[Retail].[CA].[San Francisco]");
final MemberReader memberReader = hierarchy.getMemberReader();
final MemberCache memberCache =
((SmartMemberReader) memberReader).getMemberCache();
List<RolapMember> caChildren =
memberCache.getChildrenFromCache(caMember, null);
assertEquals(5, caChildren.size());
// Load cell data and check it is in cache
executeQuery(
"select {[Measures].[Unit Sales]} on columns, {[Retail].[CA]} on rows from [Sales]");
final AggregationManager aggMgr =
((RolapConnection) conn).getServer().getAggregationManager();
assertEquals(
Double.valueOf("74748"),
aggMgr.getCellFromAllCaches(
AggregationManager.makeRequest(cacheRegionMembers)));
// Now tell the cache that [CA].[Berkeley] is new
final CacheControl.MemberEditCommand command =
cc.createAddCommand(berkeleyMember);
cc.execute(command);
// test that cells have been removed
assertNull(
aggMgr.getCellFromAllCaches(
AggregationManager.makeRequest(cacheRegionMembers)));
tc.assertAxisReturns(
"[Retail].[CA].Children",
"[Retail].[CA].[Alameda]\n"
+ "[Retail].[CA].[Beverly Hills]\n"
+ "[Retail].[CA].[Los Angeles]\n"
+ "[Retail].[CA].[San Diego]\n"
+ "[Retail].[CA].[San Francisco]\n"
+ "[Retail].[CA].[Berkeley]");
tc.assertQueryReturns(
"select {[Retail].[City].Members} on columns from [Sales]",
"Axis #0:\n"
+ "{}\n"
+ "Axis #1:\n"
+ "{[Retail].[BC].[Vancouver]}\n"
+ "{[Retail].[BC].[Victoria]}\n"
+ "{[Retail].[CA].[Alameda]}\n"
+ "{[Retail].[CA].[Berkeley]}\n"
+ "{[Retail].[CA].[Beverly Hills]}\n"
+ "{[Retail].[CA].[Los Angeles]}\n"
+ "{[Retail].[CA].[San Diego]}\n"
+ "{[Retail].[CA].[San Francisco]}\n"
+ "{[Retail].[DF].[Mexico City]}\n"
+ "{[Retail].[DF].[San Andres]}\n"
+ "{[Retail].[Guerrero].[Acapulco]}\n"
+ "{[Retail].[Jalisco].[Guadalajara]}\n"
+ "{[Retail].[OR].[Portland]}\n"
+ "{[Retail].[OR].[Salem]}\n"
+ "{[Retail].[Veracruz].[Orizaba]}\n"
+ "{[Retail].[WA].[Bellingham]}\n"
+ "{[Retail].[WA].[Bremerton]}\n"
+ "{[Retail].[WA].[Seattle]}\n"
+ "{[Retail].[WA].[Spokane]}\n"
+ "{[Retail].[WA].[Tacoma]}\n"
+ "{[Retail].[WA].[Walla Walla]}\n"
+ "{[Retail].[WA].[Yakima]}\n"
+ "{[Retail].[Yucatan].[Merida]}\n"
+ "{[Retail].[Zacatecas].[Camacho]}\n"
+ "{[Retail].[Zacatecas].[Hidalgo]}\n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: 21,333\n"
+ "Row #0: 25,663\n"
+ "Row #0: 25,635\n"
+ "Row #0: 2,117\n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: 26,079\n"
+ "Row #0: 41,580\n"
+ "Row #0: \n"
+ "Row #0: 2,237\n"
+ "Row #0: 24,576\n"
+ "Row #0: 25,011\n"
+ "Row #0: 23,591\n"
+ "Row #0: 35,257\n"
+ "Row #0: 2,203\n"
+ "Row #0: 11,491\n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n");
tc.assertQueryReturns(
"select [Retail].Children on 0 from [Sales]",
"Axis #0:\n"
+ "{}\n"
+ "Axis #1:\n"
+ "{[Retail].[BC]}\n"
+ "{[Retail].[CA]}\n"
+ "{[Retail].[DF]}\n"
+ "{[Retail].[Guerrero]}\n"
+ "{[Retail].[Jalisco]}\n"
+ "{[Retail].[OR]}\n"
+ "{[Retail].[Veracruz]}\n"
+ "{[Retail].[WA]}\n"
+ "{[Retail].[Yucatan]}\n"
+ "{[Retail].[Zacatecas]}\n"
+ "Row #0: \n"
+ "Row #0: 74,748\n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: \n"
+ "Row #0: 67,659\n"
+ "Row #0: \n"
+ "Row #0: 124,366\n"
+ "Row #0: \n"
+ "Row #0: \n");
List<RolapMember> rootChildren =
memberCache.getChildrenFromCache(rootMember, null);
if (rootChildren != null) { // might be null due to gc
assertEquals(
10, rootChildren.size());
}
}
public void testDeleteCommand() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final CacheControl cc = conn.getCacheControl(null);
final RolapCubeMember sfCubeMember =
(RolapCubeMember) findMember(
tc, "Sales", "Retail", "CA", "San Francisco");
final RolapMember caMember = sfCubeMember.member.getParentMember();
final RolapHierarchy hierarchy = caMember.getHierarchy();
final RolapBaseCubeMeasure unitSalesCubeMember =
(RolapBaseCubeMeasure) findMember(
tc, "Sales", "Measures", "Unit Sales");
final RolapCubeMember yearCubeMember =
(RolapCubeMember) findMember(
tc, "Sales", "Time", "Year", "1997");
final Member[] cacheRegionMembers =
new Member[] {
unitSalesCubeMember,
sfCubeMember,
yearCubeMember
};
tc.assertAxisReturns(
"[Retail].[CA].Children",
"[Retail].[CA].[Alameda]\n"
+ "[Retail].[CA].[Beverly Hills]\n"
+ "[Retail].[CA].[Los Angeles]\n"
+ "[Retail].[CA].[San Diego]\n"
+ "[Retail].[CA].[San Francisco]");
final MemberReader memberReader = hierarchy.getMemberReader();
final MemberCache memberCache =
((SmartMemberReader) memberReader).getMemberCache();
List<RolapMember> caChildren =
memberCache.getChildrenFromCache(caMember, null);
assertEquals(5, caChildren.size());
// Load cell data and check it is in cache
executeQuery(
"select {[Measures].[Unit Sales]} on columns, {[Retail].[CA].[Alameda]} on rows from [Sales]");
final AggregationManager aggMgr =
((RolapConnection) conn).getServer().getAggregationManager();
assertEquals(
Double.valueOf("2117"),
aggMgr.getCellFromAllCaches(
AggregationManager.makeRequest(cacheRegionMembers)));
// Now tell the cache that [CA].[San Francisco] has been removed.
final CacheControl.MemberEditCommand command =
cc.createDeleteCommand(sfCubeMember);
cc.execute(command);
// Children of CA should be 4
assertEquals(
4,
memberCache.getChildrenFromCache(caMember, null).size());
// test that cells have been removed
assertNull(
aggMgr.getCellFromAllCaches(
AggregationManager.makeRequest(cacheRegionMembers)));
// The list of children should be updated.
tc.assertAxisReturns(
"[Retail].[CA].Children",
"[Retail].[CA].[Alameda]\n"
+ "[Retail].[CA].[Beverly Hills]\n"
+ "[Retail].[CA].[Los Angeles]\n"
+ "[Retail].[CA].[San Diego]");
}
public void testMoveCommand() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final CacheControl cc = conn.getCacheControl(null);
final RolapCubeMember caCubeMember =
(RolapCubeMember) findMember(tc, "Sales", "Retail", "CA");
final RolapMember caMember = caCubeMember.member;
final RolapHierarchy hierarchy = caMember.getHierarchy();
final MemberReader memberReader = hierarchy.getMemberReader();
final MemberCache memberCache =
((SmartMemberReader) memberReader).getMemberCache();
final RolapMember alamedaMember =
(RolapMember) hierarchy.createMember(
caMember,
caMember.getLevel().getChildLevel(),
"Alameda",
null);
final RolapMember sfMember =
(RolapMember) hierarchy.createMember(
caMember,
caMember.getLevel().getChildLevel(),
"San Francisco",
null);
final RolapMember storeMember =
(RolapMember) hierarchy.createMember(
sfMember,
sfMember.getLevel().getChildLevel(),
"Store 14",
null);
// test axis contents
tc.assertAxisReturns(
"[Retail].[CA].Children",
"[Retail].[CA].[Alameda]\n"
+ "[Retail].[CA].[Beverly Hills]\n"
+ "[Retail].[CA].[Los Angeles]\n"
+ "[Retail].[CA].[San Diego]\n"
+ "[Retail].[CA].[San Francisco]");
tc.assertAxisReturns(
"[Retail].[CA].[Alameda].Children",
"[Retail].[CA].[Alameda].[HQ]");
tc.assertAxisReturns(
"[Retail].[CA].[San Francisco].Children",
"[Retail].[CA].[San Francisco].[Store 14]");
List<RolapMember> sfChildren =
memberCache.getChildrenFromCache(sfMember, null);
assertEquals(1, sfChildren.size());
List<RolapMember> alamedaChildren =
memberCache.getChildrenFromCache(alamedaMember, null);
assertEquals(1, alamedaChildren.size());
assertTrue(
storeMember.getParentMember().equals(sfMember));
// Now tell the cache that Store 14 moved to Alameda
final MemberEditCommand command =
cc.createMoveCommand(storeMember, alamedaMember);
cc.execute(command);
// The list of SF children should contain 0 elements
assertEquals(
0,
memberCache.getChildrenFromCache(sfMember, null).size());
// Check Alameda's children. It should be null as the parent's list
// should be cleared.
alamedaChildren =
memberCache.getChildrenFromCache(alamedaMember, null);
assertEquals(2, alamedaChildren.size());
// test axis contents
tc.assertAxisReturns(
"[Retail].[CA].[San Francisco].Children",
"");
tc.assertAxisReturns(
"[Retail].[CA].[Alameda].Children",
"[Retail].[CA].[Alameda].[HQ]\n"
+ "[Retail].[CA].[Alameda].[Store 14]");
// Test parent object
assertTrue(
storeMember.getParentMember().equals(alamedaMember));
}
public void testMoveFailBadLevel() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final CacheControl cc = conn.getCacheControl(null);
final RolapCubeMember caCubeMember =
(RolapCubeMember) findMember(tc, "Sales", "Retail", "CA");
final RolapMember caMember = caCubeMember.member;
final RolapHierarchy hierarchy = caMember.getHierarchy();
final MemberReader memberReader = hierarchy.getMemberReader();
final MemberCache memberCache =
((SmartMemberReader) memberReader).getMemberCache();
final RolapMember sfMember =
(RolapMember) hierarchy.createMember(
caMember,
caMember.getLevel().getChildLevel(),
"San Francisco",
null);
final RolapMember storeMember =
(RolapMember) hierarchy.createMember(
sfMember,
sfMember.getLevel().getChildLevel(),
"Store 14",
null);
// test axis contents
tc.assertAxisReturns(
"[Retail].[CA].Children",
"[Retail].[CA].[Alameda]\n"
+ "[Retail].[CA].[Beverly Hills]\n"
+ "[Retail].[CA].[Los Angeles]\n"
+ "[Retail].[CA].[San Diego]\n"
+ "[Retail].[CA].[San Francisco]");
tc.assertAxisReturns(
"[Retail].[CA].[San Francisco].Children",
"[Retail].[CA].[San Francisco].[Store 14]");
List<RolapMember> sfChildren =
memberCache.getChildrenFromCache(sfMember, null);
assertEquals(1, sfChildren.size());
assertTrue(
storeMember.getParentMember().equals(sfMember));
// Now tell the cache that Store 14 moved to CA
final MemberEditCommand command =
cc.createMoveCommand(storeMember, caMember);
try {
cc.execute(command);
fail("Should have failed due to improper level");
} catch (MondrianException e) {
assertEquals(
"new parent belongs to different level than old",
e.getCause().getMessage());
}
// The list of SF children should still contain 1 element
assertEquals(
1,
memberCache.getChildrenFromCache(sfMember, null).size());
// test axis contents. should not have been modified
tc.assertAxisReturns(
"[Retail].[CA].[San Francisco].Children",
"[Retail].[CA].[San Francisco].[Store 14]");
tc.assertAxisReturns(
"[Retail].[CA].Children",
"[Retail].[CA].[Alameda]\n"
+ "[Retail].[CA].[Beverly Hills]\n"
+ "[Retail].[CA].[Los Angeles]\n"
+ "[Retail].[CA].[San Diego]\n"
+ "[Retail].[CA].[San Francisco]");
// Test parent object. should be the same
assertTrue(
storeMember.getParentMember().equals(sfMember));
}
/**
* Tests a variety of negative cases including add/delete/move null members
* add/delete/move members in parent-child hierarchies.
*/
public void testAddCommandNegative() {
final TestContext tc = getTestContext();
final Connection conn = tc.getConnection();
final CacheControl cc = conn.getCacheControl(null);
CacheControl.MemberEditCommand command;
try {
command = cc.createAddCommand(null);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals("cannot add null member", e.getMessage());
}
final RolapCubeMember alamedaCubeMember =
(RolapCubeMember) findMember(
tc, "Sales", "Retail", "CA", "Alameda");
final RolapMember alamedaMember = alamedaCubeMember.member;
final RolapMember caMember = alamedaMember.getParentMember();
final RolapCubeMember empCubeMember =
(RolapCubeMember) findMember(
tc, "HR", "Employees", "Sheri Nowmer", "Michael Spence");
final RolapMember empMember = empCubeMember.member;
try {
command = cc.createMoveCommand(null, alamedaMember);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals("cannot move null member", e.getMessage());
}
try {
command = cc.createMoveCommand(alamedaMember, null);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals("cannot move member to null location", e.getMessage());
}
try {
command = cc.createDeleteCommand((Member) null);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals("cannot delete null member", e.getMessage());
}
try {
command = cc.createSetPropertyCommand(null, "foo", 1);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals(
"cannot set properties on null member",
e.getMessage());
}
try {
command = cc.createAddCommand(empMember);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals(
"add member not supported for parent-child hierarchy",
e.getMessage());
}
try {
command = cc.createMoveCommand(empMember, null);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals(
"move member not supported for parent-child hierarchy",
e.getMessage());
}
try {
command = cc.createDeleteCommand(empMember);
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals(
"delete member not supported for parent-child hierarchy",
e.getMessage());
}
try {
command = cc.createSetPropertyCommand(empMember, "foo", "bar");
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals(
"set properties not supported for parent-child hierarchy",
e.getMessage());
}
try {
command = cc.createSetPropertyCommand(
cc.createUnionSet(
cc.createMemberSet(alamedaMember, false),
cc.createMemberSet(caMember, false)),
Collections.<String, Object>emptyMap());
fail("expected exception, got " + command);
} catch (IllegalArgumentException e) {
assertEquals(
"all members in set must belong to same level",
e.getMessage());
}
}
/**
* Test case for bug
* <a href="http://jira.pentaho.com/browse/MONDRIAN-1076">MONDRIAN-1076,
* "Add CacheControl API to flush members from dimension cache"</a>.
*/
public void testFlushHierarchy() {
final TestContext testContext = getTestContext();
CacheControlTest.flushCache(testContext);
final CacheControl cacheControl =
testContext.getConnection().getCacheControl(null);
final Cube salesCube =
testContext.getConnection()
.getSchema().lookupCube("Sales", true);
final Logger logger = RolapUtil.SQL_LOGGER;
final Level level = logger.getLevel();
final StringWriter sw = new StringWriter();
final WriterAppender appender =
new WriterAppender(new SimpleLayout(), sw);
try {
logger.setLevel(Level.DEBUG);
logger.addAppender(appender);
final Hierarchy storeHierarchy =
salesCube.getDimensions()[1].getHierarchies()[0];
assertEquals("Store", storeHierarchy.getName());
final CacheControl.MemberSet storeMemberSet =
cacheControl.createMemberSet(
storeHierarchy.getAllMember(), true);
final Runnable storeFlusher =
new Runnable() {
public void run() {
cacheControl.flush(storeMemberSet);
}
};
final Result result =
testContext.executeQuery(
"select [Store].[Mexico].[Yucatan] on 0 from [Sales]");
final Member storeYucatanMember =
result.getAxes()[0].getPositions().get(0).get(0);
final CacheControl.MemberSet storeYucatanMemberSet =
cacheControl.createMemberSet(
storeYucatanMember, true);
final Runnable storeYucatanFlusher =
new Runnable() {
public void run() {
cacheControl.flush(storeYucatanMemberSet);
}
};
checkFlushHierarchy(
sw, true, storeFlusher,
new Runnable() {
public void run() {
// Check that <Member>.Children uses cache when applied
// to an 'all' member.
testContext.assertAxisReturns(
"[Store].Children",
"[Store].[Canada]\n"
+ "[Store].[Mexico]\n"
+ "[Store].[USA]");
}
});
checkFlushHierarchy(
sw, true, storeFlusher,
new Runnable() {
public void run() {
// Check that <Member>.Children uses cache when applied
// to regular member.
testContext.assertAxisReturns(
"[Store].[USA].[CA].Children",
"[Store].[USA].[CA].[Alameda]\n"
+ "[Store].[USA].[CA].[Beverly Hills]\n"
+ "[Store].[USA].[CA].[Los Angeles]\n"
+ "[Store].[USA].[CA].[San Diego]\n"
+ "[Store].[USA].[CA].[San Francisco]");
}
});
// In contrast to preceding, flushing Yucatan should not affect
// California.
checkFlushHierarchy(
sw, false, storeYucatanFlusher,
new Runnable() {
public void run() {
// Check that <Member>.Children uses cache when applied
// to regular member.
testContext.assertAxisReturns(
"[Store].[USA].[CA].Children",
"[Store].[USA].[CA].[Alameda]\n"
+ "[Store].[USA].[CA].[Beverly Hills]\n"
+ "[Store].[USA].[CA].[Los Angeles]\n"
+ "[Store].[USA].[CA].[San Diego]\n"
+ "[Store].[USA].[CA].[San Francisco]");
}
});
checkFlushHierarchy(
sw, true, storeFlusher, new Runnable() {
public void run() {
// Check that <Hierarchy>.Members uses cache.
testContext.assertExprReturns(
"Count([Store].Members)", "63");
}
});
checkFlushHierarchy(
sw, true, storeFlusher, new Runnable() {
public void run() {
// Check that <Level>.Members uses cache.
testContext.assertExprReturns(
"Count([Store].[Store Name].Members)", "25");
}
});
// Time hierarchy is interesting because it has public 'all' member.
// But you can still use the private all member for purposes like
// flushing.
final Hierarchy timeHierarchy =
salesCube.getDimensions()[4].getHierarchies()[0];
assertEquals("Time", timeHierarchy.getName());
final CacheControl.MemberSet timeMemberSet =
cacheControl.createMemberSet(
timeHierarchy.getAllMember(), true);
final Runnable timeFlusher =
new Runnable() {
public void run() {
cacheControl.flush(timeMemberSet);
}
};
checkFlushHierarchy(
sw, true, timeFlusher,
new Runnable() {
public void run() {
// Check that <Level>.Members uses cache.
testContext.assertExprReturns(
"Count([Time].[Month].Members)",
"24");
}
});
checkFlushHierarchy(
sw, true, timeFlusher,
new Runnable() {
public void run() {
// Check that <Level>.Members uses cache.
testContext.assertAxisReturns(
"[Time].[1997].[Q2].Children",
"[Time].[1997].[Q2].[4]\n"
+ "[Time].[1997].[Q2].[5]\n"
+ "[Time].[1997].[Q2].[6]");
}
});
} finally {
logger.setLevel(level);
logger.removeAppender(appender);
}
}
/**
* Runs the same command ({@code foo(testContext, k)}) three times. Between
* the 2nd and the 3rd, flushes the cache, and makes sure that the 3rd time
* causes SQL to be executed.
*
* @param writer Writer, written into each time a SQL statement is executed
* @param affected Whether the cache flush affects the command
* @param flusher Functor that performs cache flushing action to be tested
* @param command Command to execute that requires cache contents
*/
private void checkFlushHierarchy(
StringWriter writer,
boolean affected,
Runnable flusher,
Runnable command)
{
// Run command for first time.
command.run();
// Now cache is primed, running the command for second time should not
// require any additional SQL. (There is a small chance that GC will
// kick in and we'll lose the cache, but we've never seen that happen
// in the wild.)
int length1 = writer.getBuffer().length();
command.run();
final String since1 = writer.getBuffer().substring(length1);
assertEquals("", since1);
flusher.run();
// Now cache has been flushed, it should be impossible to execute the
// command without running additional SQL.
int length2 = writer.getBuffer().length();
command.run();
final String since2 = writer.getBuffer().substring(length2);
if (affected) {
assertNotSame("", since2);
}
}
}
// End MemberCacheControlTest.java