/*
// 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-2016 Pentaho Corporation.
// All Rights Reserved.
*/
package mondrian.rolap.cache;
import mondrian.olap.QueryCanceledException;
import mondrian.olap.Util;
import mondrian.rolap.BitKey;
import mondrian.rolap.RolapUtil;
import mondrian.rolap.agg.*;
import mondrian.server.Execution;
import mondrian.spi.*;
import mondrian.util.*;
import org.apache.log4j.Logger;
import java.io.PrintWriter;
import java.sql.Statement;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Future;
/**
* Data structure that identifies which segments contain cells.
*
* <p>Not thread safe.</p>
*
* @author Julian Hyde
*/
public class SegmentCacheIndexImpl implements SegmentCacheIndex {
private static final Logger LOGGER =
Logger.getLogger(SegmentCacheIndexImpl.class);
private final Map<List, List<SegmentHeader>> bitkeyMap =
new HashMap<List, List<SegmentHeader>>();
/**
* The fact map allows us to spot quickly which
* segments have facts relating to a given header.
*/
private final Map<List, FactInfo> factMap =
new HashMap<List, FactInfo>();
/**
* The fuzzy fact map allows us to spot quickly which
* segments have facts relating to a given header, but doesn't
* consider the compound predicates in the key. This allows
* flush operations to be consistent.
*/
// TODO Get rid of the fuzzy map once we have a way to parse
// compound predicates into rich objects that can be serialized
// as part of the SegmentHeader.
private final Map<List, FuzzyFactInfo> fuzzyFactMap =
new HashMap<List, FuzzyFactInfo>();
private final Map<SegmentHeader, HeaderInfo> headerMap =
new HashMap<SegmentHeader, HeaderInfo>();
private final Thread thread;
/**
* Creates a SegmentCacheIndexImpl.
*
* @param thread Thread that must be used to execute commands.
*/
public SegmentCacheIndexImpl(Thread thread) {
this.thread = thread;
assert thread != null;
}
public static List makeConverterKey(SegmentHeader header) {
return Arrays.asList(
header.schemaName,
header.schemaChecksum,
header.cubeName,
header.rolapStarFactTableName,
header.measureName,
header.compoundPredicates);
}
public static List makeConverterKey(CellRequest request, AggregationKey key)
{
return Arrays.asList(
request.getMeasure().getStar().getSchema().getName(),
request.getMeasure().getStar().getSchema().getChecksum(),
request.getMeasure().getCubeName(),
request.getMeasure().getStar().getFactTable().getAlias(),
request.getMeasure().getName(),
request.getCompoundPredicateStrings());
}
public List<SegmentHeader> locate(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String measureName,
String rolapStarFactTableName,
BitKey constrainedColsBitKey,
Map<String, Comparable> coordinates,
List<String> compoundPredicates)
{
checkThread();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(
"SegmentCacheIndexImpl("
+ System.identityHashCode(this)
+ ")locate:"
+ "\nschemaName:" + schemaName
+ "\nschemaChecksum:" + schemaChecksum
+ "\ncubeName:" + cubeName
+ "\nmeasureName:" + measureName
+ "\nrolapStarFactTableName:" + rolapStarFactTableName
+ "\nconstrainedColsBitKey:" + constrainedColsBitKey
+ "\ncoordinates:" + coordinates
+ "\ncompoundPredicates:" + compoundPredicates);
}
List<SegmentHeader> list = Collections.emptyList();
final List starKey =
makeBitkeyKey(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
constrainedColsBitKey,
measureName,
compoundPredicates);
final List<SegmentHeader> headerList = bitkeyMap.get(starKey);
if (headerList == null) {
LOGGER.trace(
"SegmentCacheIndexImpl("
+ System.identityHashCode(this)
+ ").locate:NOMATCH");
return Collections.emptyList();
}
for (SegmentHeader header : headerList) {
if (matches(header, coordinates, compoundPredicates)) {
// Be lazy. Don't allocate a list unless there is at least one
// entry.
if (list.isEmpty()) {
list = new ArrayList<SegmentHeader>();
}
list.add(header);
}
}
if (LOGGER.isTraceEnabled()) {
final StringBuilder sb =
new StringBuilder(
"SegmentCacheIndexImpl("
+ System.identityHashCode(this)
+ ").locate:MATCH");
for (SegmentHeader header : list) {
sb.append("\n");
sb.append(header.toString());
}
LOGGER.trace(sb.toString());
}
return list;
}
public void add(
SegmentHeader header,
SegmentBuilder.SegmentConverter converter,
boolean loading)
{
checkThread();
LOGGER.debug(
"SegmentCacheIndexImpl("
+ System.identityHashCode(this)
+ ").add:\n"
+ header.toString());
HeaderInfo headerInfo = headerMap.get(header);
if (headerInfo == null) {
headerInfo = new HeaderInfo();
if (loading) {
// We are currently loading this segment. It isnt' in cache.
// We put a slot into which the data will become available.
headerInfo.slot = new SlotFuture<SegmentBody>();
}
headerMap.put(header, headerInfo);
}
final List bitkeyKey = makeBitkeyKey(header);
List<SegmentHeader> headerList = bitkeyMap.get(bitkeyKey);
if (headerList == null) {
headerList = new ArrayList<SegmentHeader>();
bitkeyMap.put(bitkeyKey, headerList);
}
if (!headerList.contains(header)) {
headerList.add(header);
}
final List factKey = makeFactKey(header);
FactInfo factInfo = factMap.get(factKey);
if (factInfo == null) {
factInfo = new FactInfo();
factMap.put(factKey, factInfo);
}
if (!factInfo.headerList.contains(header)) {
factInfo.headerList.add(header);
}
if (!factInfo.bitkeyPoset
.contains(header.getConstrainedColumnsBitKey()))
{
factInfo.bitkeyPoset.add(header.getConstrainedColumnsBitKey());
}
if (converter != null) {
factInfo.converter = converter;
}
final List fuzzyFactKey = makeFuzzyFactKey(header);
FuzzyFactInfo fuzzyFactInfo = fuzzyFactMap.get(fuzzyFactKey);
if (fuzzyFactInfo == null) {
fuzzyFactInfo = new FuzzyFactInfo();
fuzzyFactMap.put(fuzzyFactKey, fuzzyFactInfo);
}
if (!fuzzyFactInfo.headerList.contains(header)) {
fuzzyFactInfo.headerList.add(header);
}
}
public void update(
SegmentHeader oldHeader,
SegmentHeader newHeader)
{
checkThread();
LOGGER.trace(
"SegmentCacheIndexImpl.update: Updating header from:\n"
+ oldHeader.toString()
+ "\n\nto\n\n"
+ newHeader.toString());
final HeaderInfo headerInfo = headerMap.get(oldHeader);
headerMap.remove(oldHeader);
headerMap.put(newHeader, headerInfo);
final List oldBitkeyKey = makeBitkeyKey(oldHeader);
List<SegmentHeader> headerList = bitkeyMap.get(oldBitkeyKey);
headerList.remove(oldHeader);
headerList.add(newHeader);
final List oldFactKey = makeFactKey(oldHeader);
final FactInfo factInfo = factMap.get(oldFactKey);
factInfo.headerList.remove(oldHeader);
factInfo.headerList.add(newHeader);
final List oldFuzzyFactKey = makeFuzzyFactKey(oldHeader);
final FuzzyFactInfo fuzzyFactInfo = fuzzyFactMap.get(oldFuzzyFactKey);
fuzzyFactInfo.headerList.remove(oldHeader);
fuzzyFactInfo.headerList.add(newHeader);
}
public void loadSucceeded(SegmentHeader header, SegmentBody body) {
checkThread();
final HeaderInfo headerInfo = headerMap.get(header);
assert headerInfo != null
: "segment header " + header.getUniqueID() + " is missing";
if (!headerInfo.slot.isDone()) {
headerInfo.slot.put(body);
}
if (headerInfo.removeAfterLoad) {
remove(header);
}
// Cleanup the HeaderInfo
headerInfo.stmt = null;
headerInfo.clients.clear();
}
public void loadFailed(SegmentHeader header, Throwable throwable) {
checkThread();
final HeaderInfo headerInfo = headerMap.get(header);
if (headerInfo == null) {
LOGGER.trace("loadFailed: Missing header " + header);
return;
}
assert headerInfo.slot != null
: "segment header " + header.getUniqueID() + " is not loading";
headerInfo.slot.fail(throwable);
remove(header);
// Cleanup the HeaderInfo
headerInfo.stmt = null;
headerInfo.clients.clear();
}
public void remove(SegmentHeader header) {
checkThread();
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(
"SegmentCacheIndexImpl("
+ System.identityHashCode(this)
+ ").remove:\n"
+ header.toString(),
new Throwable("Removal."));
} else {
LOGGER.debug(
"SegmentCacheIndexImpl.remove:\n"
+ header.toString());
}
final HeaderInfo headerInfo = headerMap.get(header);
if (headerInfo == null) {
LOGGER.debug(
"SegmentCacheIndexImpl("
+ System.identityHashCode(this)
+ ").remove:UNKNOWN HEADER");
return;
}
if (headerInfo.slot != null && !headerInfo.slot.isDone()) {
// Cannot remove while load is pending; flag for removal after load
headerInfo.removeAfterLoad = true;
LOGGER.debug(
"SegmentCacheIndexImpl("
+ System.identityHashCode(this)
+ ").remove:DEFFERED");
return;
}
headerMap.remove(header);
final List factKey = makeFactKey(header);
final FactInfo factInfo = factMap.get(factKey);
if (factInfo != null) {
factInfo.headerList.remove(header);
factInfo.bitkeyPoset.remove(header.getConstrainedColumnsBitKey());
if (factInfo.headerList.size() == 0) {
factMap.remove(factKey);
}
}
final List fuzzyFactKey = makeFuzzyFactKey(header);
final FuzzyFactInfo fuzzyFactInfo = fuzzyFactMap.get(fuzzyFactKey);
if (fuzzyFactInfo != null) {
fuzzyFactInfo.headerList.remove(header);
if (fuzzyFactInfo.headerList.size() == 0) {
fuzzyFactMap.remove(fuzzyFactKey);
}
}
final List bitkeyKey = makeBitkeyKey(header);
final List<SegmentHeader> headerList = bitkeyMap.get(bitkeyKey);
headerList.remove(header);
if (headerList.size() == 0) {
bitkeyMap.remove(bitkeyKey);
}
}
private void checkThread() {
assert thread == Thread.currentThread()
: "expected " + thread + ", but was " + Thread.currentThread();
}
public static boolean matches(
SegmentHeader header,
Map<String, Comparable> coords,
List<String> compoundPredicates)
{
if (!header.compoundPredicates.equals(compoundPredicates)) {
return false;
}
for (Map.Entry<String, Comparable> entry : coords.entrySet()) {
// Check if the segment explicitly excludes this coordinate.
final SegmentColumn excludedColumn =
header.getExcludedRegion(entry.getKey());
if (excludedColumn != null) {
final SortedSet<Comparable> values =
excludedColumn.getValues();
if (values == null || values.contains(entry.getValue())) {
return false;
}
}
// Check if the dimensionality of the segment intersects
// with the coordinate.
final SegmentColumn constrainedColumn =
header.getConstrainedColumn(entry.getKey());
if (constrainedColumn == null) {
// One of the required column/value pairs is not a constraining
// column for the header. This will not happen if the header
// has been acquired from bitkeyMap, but may happen if a list
// of mixed-dimensionality headers is being scanned.
return false;
}
final SortedSet<Comparable> values =
constrainedColumn.getValues();
if (values != null
&& !values.contains(entry.getValue()))
{
return false;
}
}
return true;
}
public List<SegmentHeader> intersectRegion(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String measureName,
String rolapStarFactTableName,
SegmentColumn[] region)
{
checkThread();
final List factKey = makeFuzzyFactKey(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
measureName);
final FuzzyFactInfo factInfo = fuzzyFactMap.get(factKey);
List<SegmentHeader> list = Collections.emptyList();
if (factInfo == null) {
return list;
}
for (SegmentHeader header : factInfo.headerList) {
// Don't return stale segments.
if (headerMap.get(header).removeAfterLoad) {
continue;
}
if (intersects(header, region)) {
// Be lazy. Don't allocate a list unless there is at least one
// entry.
if (list.isEmpty()) {
list = new ArrayList<SegmentHeader>();
}
list.add(header);
}
}
return list;
}
private boolean intersects(
SegmentHeader header,
SegmentColumn[] region)
{
// most selective condition first
if (region.length == 0) {
return true;
}
for (SegmentColumn regionColumn : region) {
final SegmentColumn headerColumn =
header.getConstrainedColumn(regionColumn.getColumnExpression());
if (headerColumn == null) {
// If the segment header doesn't contain a column specified
// by the region, then it always implicitly intersects.
// This allows flush operations to be valid.
return true;
}
final SortedSet<Comparable> regionValues =
regionColumn.getValues();
final SortedSet<Comparable> headerValues =
headerColumn.getValues();
if (headerValues == null || regionValues == null) {
// This is a wildcard, so it always intersects.
return true;
}
for (Comparable myValue : regionValues) {
if (headerValues.contains(myValue)) {
return true;
}
}
}
return false;
}
public void printCacheState(PrintWriter pw) {
checkThread();
final List<List<SegmentHeader>> values =
new ArrayList<List<SegmentHeader>>(
bitkeyMap.values());
Collections.sort(
values,
new Comparator<List<SegmentHeader>>() {
public int compare(
List<SegmentHeader> o1,
List<SegmentHeader> o2)
{
if (o1.size() == 0) {
return -1;
}
if (o2.size() == 0) {
return 1;
}
return o1.get(0).getUniqueID()
.compareTo(o2.get(0).getUniqueID());
}
});
for (List<SegmentHeader> key : values) {
final List<SegmentHeader> headerList =
new ArrayList<SegmentHeader>(key);
Collections.sort(
headerList,
new Comparator<SegmentHeader>() {
public int compare(SegmentHeader o1, SegmentHeader o2) {
return o1.getUniqueID().compareTo(o2.getUniqueID());
}
});
for (SegmentHeader header : headerList) {
pw.println(header.getDescription());
}
}
}
public Future<SegmentBody> getFuture(Execution exec, SegmentHeader header) {
checkThread();
HeaderInfo hi = headerMap.get(header);
if (!hi.clients.contains(exec)) {
hi.clients.add(exec);
}
return hi.slot;
}
public void linkSqlStatement(SegmentHeader header, Statement stmt) {
checkThread();
headerMap.get(header).stmt = stmt;
}
public boolean contains(SegmentHeader header) {
return headerMap.containsKey(header);
}
public void cancel(Execution exec) {
checkThread();
List<SegmentHeader> toRemove = new ArrayList<SegmentHeader>();
for (Entry<SegmentHeader, HeaderInfo> entry : headerMap.entrySet()) {
if (entry.getValue().clients.remove(exec)) {
if (entry.getValue().slot != null
&& !entry.getValue().slot.isDone()
&& entry.getValue().clients.isEmpty())
{
toRemove.add(entry.getKey());
}
}
}
// Make sure to cleanup the orphaned segments.
for (SegmentHeader header : toRemove) {
final Statement stmt = headerMap.get(header).stmt;
loadFailed(
header,
new QueryCanceledException(
"Canceling due to an absence of interested parties."));
// We only want to cancel the statement, but we can't close it.
// Some drivers will not notice the interruption flag on their
// own thread before a considerable time has passed. If we were
// using a pooling layer, calling close() would make the
// underlying connection available again, despite the first
// statement still being processed. Some drivers will fail
// there. It is therefore important to close and release the
// resources on the proper thread, namely, the thread which
// runs the actual statement.
Util.cancelStatement(stmt);
}
}
public SegmentBuilder.SegmentConverter getConverter(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String rolapStarFactTableName,
String measureName,
List<String> compoundPredicates)
{
checkThread();
final List factKey = makeFactKey(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
measureName,
compoundPredicates);
final FactInfo factInfo = factMap.get(factKey);
if (factInfo == null) {
return null;
}
return factInfo.converter;
}
public void setConverter(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String rolapStarFactTableName,
String measureName,
List<String> compoundPredicates,
SegmentBuilder.SegmentConverter converter)
{
checkThread();
final List factKey = makeFactKey(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
measureName,
compoundPredicates);
final FactInfo factInfo = factMap.get(factKey);
assert factInfo != null : "should have called 'add' first";
if (factInfo == null) {
return;
}
factInfo.converter = converter;
}
private List makeBitkeyKey(SegmentHeader header) {
return makeBitkeyKey(
header.schemaName,
header.schemaChecksum,
header.cubeName,
header.rolapStarFactTableName,
header.constrainedColsBitKey,
header.measureName,
header.compoundPredicates);
}
private List makeBitkeyKey(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String rolapStarFactTableName,
BitKey constrainedColsBitKey,
String measureName,
List<String> compoundPredicates)
{
return Arrays.asList(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
constrainedColsBitKey,
measureName,
compoundPredicates);
}
private List makeFactKey(SegmentHeader header) {
return makeFactKey(
header.schemaName,
header.schemaChecksum,
header.cubeName,
header.rolapStarFactTableName,
header.measureName,
header.compoundPredicates);
}
private List makeFactKey(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String rolapStarFactTableName,
String measureName,
List<String> compoundPredicates)
{
return Arrays.asList(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
measureName,
compoundPredicates);
}
private List makeFuzzyFactKey(SegmentHeader header) {
return makeFuzzyFactKey(
header.schemaName,
header.schemaChecksum,
header.cubeName,
header.rolapStarFactTableName,
header.measureName);
}
private List makeFuzzyFactKey(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String rolapStarFactTableName,
String measureName)
{
return Arrays.asList(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
measureName);
}
public List<List<SegmentHeader>> findRollupCandidates(
String schemaName,
ByteString schemaChecksum,
String cubeName,
String measureName,
String rolapStarFactTableName,
BitKey constrainedColsBitKey,
Map<String, Comparable> coordinates,
List<String> compoundPredicates)
{
final List factKey = makeFactKey(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
measureName,
compoundPredicates);
final FactInfo factInfo = factMap.get(factKey);
if (factInfo == null) {
return Collections.emptyList();
}
// Iterate over all dimensionalities that are a superset of the desired
// columns and for which a segment is known to exist.
//
// It helps that getAncestors returns dimensionalities with fewer bits
// set first. These will contain fewer cells, and therefore be less
// effort to roll up.
final List<List<SegmentHeader>> list =
new ArrayList<List<SegmentHeader>>();
final List<BitKey> ancestors =
factInfo.bitkeyPoset.getAncestors(constrainedColsBitKey);
for (BitKey bitKey : ancestors) {
final List bitkeyKey = makeBitkeyKey(
schemaName,
schemaChecksum,
cubeName,
rolapStarFactTableName,
bitKey,
measureName,
compoundPredicates);
final List<SegmentHeader> headers = bitkeyMap.get(bitkeyKey);
assert headers != null : "bitkeyPoset / bitkeyMap inconsistency";
// For columns that are still present after roll up, make sure that
// the required value is in the range covered by the segment.
// Of the columns that are being aggregated away, are all of
// them wildcarded? If so, this segment is a match. If not, we
// will need to combine with other segments later.
findRollupCandidatesAmong(coordinates, list, headers);
}
return list;
}
/**
* Finds rollup candidates among a list of headers with the same
* dimensionality.
*
* <p>For each column that is being aggregated away, we need to ensure that
* we have all values of that column. If the column is wildcarded, it's
* easy. For example, if we wish to roll up to create Segment1:</p>
*
* <pre>Segment1(Year=1997, MaritalStatus=*)</pre>
*
* <p>then clearly Segment2:</p>
*
* <pre>Segment2(Year=1997, MaritalStatus=*, Gender=*, Nation=*)</pre>
*
* <p>has all gender and Nation values. If the values are specified as a
* list:</p>
*
* <pre>Segment3(Year=1997, MaritalStatus=*, Gender={M, F}, Nation=*)</pre>
*
* <p>then we need to check the metadata. We see that Gender has two
* distinct values in the database, and we have two values, therefore we
* have all of them.</p>
*
* <p>What if we have multiple non-wildcard columns? Consider:</p>
*
* <pre>
* Segment4(Year=1997, MaritalStatus=*, Gender={M},
Nation={Mexico, USA})
* Segment5(Year=1997, MaritalStatus=*, Gender={F},
Nation={USA})
* Segment6(Year=1997, MaritalStatus=*, Gender={F, M},
Nation={Canada, Mexico, Honduras, Belize})
* </pre>
*
* <p>The problem is similar to finding whether a collection of rectangular
* regions covers a rectangle (or, generalizing to n dimensions, an
* n-cube). Or better, find a minimal collection of regions.</p>
*
* <p>Our algorithm solves it by iterating over all combinations of values.
* Those combinations are exponential in theory, but tractible in practice,
* using the following trick. The algorithm reduces the number of
* combinations by looking for values that are always treated the same. In
* the above, Canada, Honduras and Belize are always treated the same, so to
* prove covering, it is sufficient to prove that all combinations involving
* Canada are covered.</p>
*
* @param coordinates Coordinates
* @param list List to write candidates to
* @param headers Headers of candidate segments
*/
private void findRollupCandidatesAmong(
Map<String, Comparable> coordinates,
List<List<SegmentHeader>> list,
List<SegmentHeader> headers)
{
final List<Pair<SegmentHeader, List<SegmentColumn>>> matchingHeaders =
new ArrayList<Pair<SegmentHeader, List<SegmentColumn>>>();
headerLoop:
for (SegmentHeader header : headers) {
// Skip headers that have exclusions.
//
// TODO: This is a bit harsh.
if (!header.getExcludedRegions().isEmpty()) {
continue;
}
List<SegmentColumn> nonWildcards =
new ArrayList<SegmentColumn>();
for (SegmentColumn column : header.getConstrainedColumns()) {
final SegmentColumn constrainedColumn =
header.getConstrainedColumn(column.columnExpression);
// REVIEW: How are null key values represented in coordinates?
// Assuming that they are represented by null ref.
if (coordinates.containsKey(column.columnExpression)) {
// Matching column. Will not be aggregated away. Needs
// to be in range.
Comparable value =
coordinates.get(column.columnExpression);
if (value == null) {
value = RolapUtil.sqlNullValue;
}
if (constrainedColumn.values != null
&& !constrainedColumn.values.contains(value))
{
continue headerLoop;
}
} else {
// Non-matching column. Will be aggregated away. Needs
// to be wildcarded (or some more complicated conditions
// to be dealt with later).
if (constrainedColumn.values != null) {
nonWildcards.add(constrainedColumn);
}
}
}
if (nonWildcards.isEmpty()) {
list.add(Collections.singletonList(header));
} else {
matchingHeaders.add(Pair.of(header, nonWildcards));
}
}
// Find combinations of segments that can roll up. Need at least two.
if (matchingHeaders.size() < 2) {
return;
}
// Collect the list of non-wildcarded columns.
final List<SegmentColumn> columnList = new ArrayList<SegmentColumn>();
final List<String> columnNameList = new ArrayList<String>();
for (Pair<SegmentHeader, List<SegmentColumn>> pair : matchingHeaders) {
for (SegmentColumn column : pair.right) {
if (!columnNameList.contains(column.columnExpression)) {
final long valueCount = column.getValueCount();
if (valueCount <= 0) {
// Impossible to safely roll up. If we don't know the
// number of values, we don't know that we have all of
// them.
return;
}
columnList.add(column);
columnNameList.add(column.columnExpression);
}
}
}
// Gather known values of each column. For each value, remember which
// segments refer to it.
final List<List<Comparable>> valueLists =
new ArrayList<List<Comparable>>();
for (SegmentColumn column : columnList) {
// For each value, which equivalence class it belongs to.
final SortedMap<Comparable, BitSet> valueMap =
new TreeMap<Comparable, BitSet>(RolapUtil.ROLAP_COMPARATOR);
int h = -1;
for (SegmentHeader header : Pair.leftIter(matchingHeaders)) {
++h;
final SegmentColumn column1 =
header.getConstrainedColumn(
column.columnExpression);
if (column1.getValues() == null) {
// Wildcard. Mark all values as present.
for (Entry<Comparable, BitSet> entry : valueMap.entrySet())
{
for (int pos = 0;
pos < entry.getValue().cardinality();
pos++)
{
entry.getValue().set(pos);
}
}
} else {
for (Comparable value : column1.getValues()) {
BitSet bitSet = valueMap.get(value);
if (bitSet == null) {
bitSet = new BitSet();
valueMap.put(value, bitSet);
}
bitSet.set(h);
}
}
}
// Is the number of values discovered equal to the known cardinality
// of the column? If not, we can't cover the space.
if (valueMap.size() < column.valueCount) {
return;
}
// Build equivalence sets of values. These group together values
// that are used identically in segments.
//
// For instance, given segments Sx over column c,
//
// S1: c = {1, 2, 3, 4}
// S2: c = {3, 4, 5}
// S3: c = {3, 6, 7, 8}
//
// the equivalence classes are:
//
// E1 = {1, 2} used in {S1}
// E2 = {3} used in {S1, S2, S3}
// E3 = {4} used in {S1, S2}
// E4 = {6, 7, 8} used in {S3}
//
// The equivalence classes reduce the size of the search space. (In
// this case, from 8 values to 4 classes.) We can use any value in a
// class to stand for all values.
final Map<BitSet, Comparable> eqclassPrimaryValues =
new HashMap<BitSet, Comparable>();
for (Map.Entry<Comparable, BitSet> entry : valueMap.entrySet()) {
final BitSet bitSet = entry.getValue();
if (!eqclassPrimaryValues.containsKey(bitSet)) {
final Comparable value = entry.getKey();
eqclassPrimaryValues.put(bitSet, value);
}
}
valueLists.add(
new ArrayList<Comparable>(
eqclassPrimaryValues.values()));
}
// Iterate over every combination of values, and make sure that some
// segment can satisfy each.
//
// TODO: A greedy algorithm would probably be better. Rather than adding
// the first segment that contains a particular value combination, add
// the segment that contains the most value combinations that we are are
// not currently covering.
final CartesianProductList<Comparable> tuples =
new CartesianProductList<Comparable>(valueLists);
final List<SegmentHeader> usedSegments = new ArrayList<SegmentHeader>();
final List<SegmentHeader> unusedSegments =
new ArrayList<SegmentHeader>(Pair.left(matchingHeaders));
tupleLoop:
for (List<Comparable> tuple : tuples) {
// If the value combination is handled by one of the used segments,
// great!
for (SegmentHeader segment : usedSegments) {
if (contains(segment, tuple, columnNameList)) {
continue tupleLoop;
}
}
// Does one of the unused segments contain it? Use the first one we
// find.
for (SegmentHeader segment : unusedSegments) {
if (contains(segment, tuple, columnNameList)) {
unusedSegments.remove(segment);
usedSegments.add(segment);
continue tupleLoop;
}
}
// There was a value combination not contained in any of the
// segments. Fail.
return;
}
list.add(usedSegments);
}
private boolean contains(
SegmentHeader segment,
List<Comparable> values,
List<String> columns)
{
for (int i = 0; i < columns.size(); i++) {
String columnName = columns.get(i);
final SegmentColumn column =
segment.getConstrainedColumn(columnName);
final SortedSet<Comparable> valueSet = column.getValues();
if (valueSet != null && !valueSet.contains(values.get(i))) {
return false;
}
}
return true;
}
private static class FactInfo {
private static final PartiallyOrderedSet.Ordering<BitKey> ORDERING =
new PartiallyOrderedSet.Ordering<BitKey>() {
public boolean lessThan(BitKey e1, BitKey e2) {
return e2.isSuperSetOf(e1);
}
};
private final List<SegmentHeader> headerList =
new ArrayList<SegmentHeader>();
private final PartiallyOrderedSet<BitKey> bitkeyPoset =
new PartiallyOrderedSet<BitKey>(ORDERING);
private SegmentBuilder.SegmentConverter converter;
FactInfo() {
}
}
private static class FuzzyFactInfo {
private final List<SegmentHeader> headerList =
new ArrayList<SegmentHeader>();
FuzzyFactInfo() {
}
}
/**
* A private class that we use in the index to track who was interested in
* which headers, the SQL statement that is populating it and a future
* object which we pass to clients.
*/
private static class HeaderInfo {
/**
* The SQL statement populating this header.
* Will be null until the SQL thread calls us back to register it.
*/
private Statement stmt;
/**
* The future object to pass on to clients.
*/
private SlotFuture<SegmentBody> slot;
/**
* A list of clients interested in this segment.
*/
private final List<Execution> clients =
new CopyOnWriteArrayList<Execution>();
/**
* Whether this segment is already considered stale and must
* be deleted after it is done loading. This can happen
* when flushing.
*/
private boolean removeAfterLoad;
}
}
// End SegmentCacheIndexImpl.java