/*
// 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) 2004-2005 Julian Hyde
// Copyright (C) 2005-2015 Pentaho and others
// All Rights Reserved.
*/
package mondrian.rolap;
import mondrian.olap.*;
import mondrian.rolap.agg.*;
import mondrian.rolap.aggmatcher.AggGen;
import mondrian.rolap.aggmatcher.AggStar;
import mondrian.rolap.cache.SegmentCacheIndex;
import mondrian.rolap.cache.SegmentCacheIndexImpl;
import mondrian.server.Execution;
import mondrian.server.Locus;
import mondrian.spi.*;
import mondrian.util.*;
import org.apache.log4j.Logger;
import org.apache.log4j.MDC;
import java.util.*;
import java.util.concurrent.Future;
/**
* A <code>FastBatchingCellReader</code> doesn't really Read cells: when asked
* to look up the values of stored measures, it lies, and records the fact
* that the value was asked for. Later, we can look over the values which
* are required, fetch them in an efficient way, and re-run the evaluation
* with a real evaluator.
*
* <p>NOTE: When it doesn't know the answer, it lies by returning an error
* object. The calling code must be able to deal with that.</p>
*
* <p>This class tries to minimize the amount of storage needed to record the
* fact that a cell was requested.</p>
*/
public class FastBatchingCellReader implements CellReader {
private static final Logger LOGGER =
Logger.getLogger(FastBatchingCellReader.class);
private final int cellRequestLimit;
private final RolapCube cube;
/**
* Records the number of requests. The field is used for correctness: if
* the request count stays the same during an operation, you know that the
* FastBatchingCellReader has not told any lies during that operation, and
* therefore the result is true. The field is also useful for debugging.
*/
private int missCount;
/**
* Number of occasions that a requested cell was already in cache.
*/
private int hitCount;
/**
* Number of occasions that requested cell was in the process of being
* loaded into cache but not ready.
*/
private int pendingCount;
private final AggregationManager aggMgr;
private final boolean cacheEnabled;
private final SegmentCacheManager cacheMgr;
private final RolapAggregationManager.PinSet pinnedSegments;
/**
* Indicates that the reader has given incorrect results.
*/
private boolean dirty;
private final List<CellRequest> cellRequests = new ArrayList<CellRequest>();
private final Execution execution;
/**
* Creates a FastBatchingCellReader.
*
* @param execution Execution that calling statement belongs to. Allows us
* to check for cancel
* @param cube Cube that requests belong to
* @param aggMgr Aggregation manager
*/
public FastBatchingCellReader(
Execution execution,
RolapCube cube,
AggregationManager aggMgr)
{
this.execution = execution;
assert cube != null;
assert execution != null;
this.cube = cube;
this.aggMgr = aggMgr;
cacheMgr = aggMgr.cacheMgr;
pinnedSegments = this.aggMgr.createPinSet();
cacheEnabled = !MondrianProperties.instance().DisableCaching.get();
cellRequestLimit =
MondrianProperties.instance().CellBatchSize.get() <= 0
? 100000 // TODO Make this logic into a pluggable algorithm.
: MondrianProperties.instance().CellBatchSize.get();
}
public Object get(RolapEvaluator evaluator) {
final CellRequest request =
RolapAggregationManager.makeRequest(evaluator);
if (request == null || request.isUnsatisfiable()) {
return Util.nullValue; // request not satisfiable.
}
// Try to retrieve a cell and simultaneously pin the segment which
// contains it.
final Object o = aggMgr.getCellFromCache(request, pinnedSegments);
assert o != Boolean.TRUE : "getCellFromCache no longer returns TRUE";
if (o != null) {
++hitCount;
return o;
}
// If this query has not had any cache misses, it's worth doing a
// synchronous request for the cell segment. If it is in the cache, it
// will be worth the wait, because we can avoid the effort of batching
// up requests that could have been satisfied by the same segment.
if (cacheEnabled
&& missCount == 0)
{
SegmentWithData segmentWithData = cacheMgr.peek(request);
if (segmentWithData != null) {
segmentWithData.getStar().register(segmentWithData);
final Object o2 =
aggMgr.getCellFromCache(request, pinnedSegments);
if (o2 != null) {
++hitCount;
return o2;
}
}
}
// if there is no such cell, record that we need to fetch it, and
// return 'error'
recordCellRequest(request);
return RolapUtil.valueNotReadyException;
}
public int getMissCount() {
return missCount;
}
public int getHitCount() {
return hitCount;
}
public int getPendingCount() {
return pendingCount;
}
public final void recordCellRequest(CellRequest request) {
assert !request.isUnsatisfiable();
++missCount;
cellRequests.add(request);
if (cellRequests.size() % cellRequestLimit == 0) {
// Signal that it's time to ask the cache manager if it has cells
// we need in the cache. Not really an exception.
throw CellRequestQuantumExceededException.INSTANCE;
}
}
/**
* Returns whether this reader has told a lie. This is the case if there
* are pending batches to load or if {@link #setDirty(boolean)} has been
* called.
*/
public boolean isDirty() {
return dirty || !cellRequests.isEmpty();
}
/**
* Resolves any pending cell reads using the cache. After calling this
* method, all cells requested in a given batch are loaded into this
* statement's local cache.
*
* <p>The method is implemented by making an asynchronous call to the cache
* manager. The result is a list of segments that satisfies every cell
* request.</p>
*
* <p>The client should put the resulting segments into its "query local"
* cache, to ensure that future cells in that segment can be answered
* without a call to the cache manager. (That is probably 1000x faster.)</p>
*
* <p>The cache manager does not inform where client where each segment
* came from. There are several possibilities:</p>
*
* <ul>
* <li>Segment was already in cache (header and body)</li>
* <li>Segment is in the process of being loaded by executing a SQL
* statement (probably due to a request from another client)</li>
* <li>Segment is in an external cache (that is, header is in the cache,
* body is not yet)</li>
* <li>Segment can be created by rolling up one or more cache segments.
* (And of course each of these segments might be "paged out".)</li>
* <li>By executing a SQL {@code GROUP BY} statement</li>
* </ul>
*
* <p>Furthermore, segments in external cache may take some time to retrieve
* (a LAN round trip, say 1 millisecond, is a reasonable guess); and the
* request may fail. (It depends on the cache, but caches are at liberty
* to 'forget' segments.) So, any strategy that relies on cache segments
* should be able to fall back. Even if there are fall backs, only one call
* needs to be made to the cache manager.</p>
*
* @return Whether any aggregations were loaded.
*/
boolean loadAggregations() {
if (!isDirty()) {
return false;
}
// List of futures yielding segments populated by SQL statements. If
// loading requires several iterations, we just append to the list. We
// don't mind if it takes a while for SQL statements to return.
final List<Future<Map<Segment, SegmentWithData>>> sqlSegmentMapFutures =
new ArrayList<Future<Map<Segment, SegmentWithData>>>();
final List<CellRequest> cellRequests1 =
new ArrayList<CellRequest>(cellRequests);
preloadColumnCardinality(cellRequests1);
for (int iteration = 0;; ++iteration) {
final BatchLoader.LoadBatchResponse response =
cacheMgr.execute(
new BatchLoader.LoadBatchCommand(
Locus.peek(),
cacheMgr,
getDialect(),
cube,
Collections.unmodifiableList(cellRequests1)));
int failureCount = 0;
// Segments that have been retrieved from cache this cycle. Allows
// us to reduce calls to the external cache.
Map<SegmentHeader, SegmentBody> headerBodies =
new HashMap<SegmentHeader, SegmentBody>();
// Load each suggested segment from cache, and place it in
// thread-local cache. Note that this step can't be done by the
// cacheMgr -- it's our cache.
for (SegmentHeader header : response.cacheSegments) {
final SegmentBody body = cacheMgr.compositeCache.get(header);
if (body == null) {
// REVIEW: This is an async call. It will return before the
// index is informed that this header is there,
// so a LoadBatchCommand might still return
// it on the next iteration.
if (cube.getStar() != null) {
cacheMgr.remove(cube.getStar(), header);
}
++failureCount;
continue;
}
headerBodies.put(header, body);
final SegmentWithData segmentWithData =
response.convert(header, body);
segmentWithData.getStar().register(segmentWithData);
}
// Perform each suggested rollup.
//
// TODO this could be improved.
// See http://jira.pentaho.com/browse/MONDRIAN-1195
// Rollups that succeeded. Will tell cache mgr to put the headers
// into the index and the header/bodies in cache.
final Map<SegmentHeader, SegmentBody> succeededRollups =
new HashMap<SegmentHeader, SegmentBody>();
for (final BatchLoader.RollupInfo rollup : response.rollups) {
// Gather the required segments.
Map<SegmentHeader, SegmentBody> map =
findResidentRollupCandidate(headerBodies, rollup);
if (map == null) {
// None of the candidate segment-sets for this rollup was
// all present in the cache.
continue;
}
final Set<String> keepColumns = new HashSet<String>();
for (RolapStar.Column column : rollup.constrainedColumns) {
keepColumns.add(
column.getExpression().getGenericExpression());
}
Pair<SegmentHeader, SegmentBody> rollupHeaderBody =
SegmentBuilder.rollup(
map,
keepColumns,
rollup.constrainedColumnsBitKey,
rollup.measure.getAggregator().getRollup(),
rollup.measure.getDatatype());
final SegmentHeader header = rollupHeaderBody.left;
final SegmentBody body = rollupHeaderBody.right;
if (headerBodies.containsKey(header)) {
// We had already created this segment, somehow.
continue;
}
headerBodies.put(header, body);
succeededRollups.put(header, body);
final SegmentWithData segmentWithData =
response.convert(header, body);
// Register this segment with the local star.
segmentWithData.getStar().register(segmentWithData);
// Make sure that the cache manager knows about this new
// segment. First thing we do is to add it to the index.
// Then we insert the segment body into the SlotFuture.
// This has to be done on the SegmentCacheManager's
// Actor thread to ensure thread safety.
if (!MondrianProperties.instance().DisableCaching.get()) {
final Locus locus = Locus.peek();
cacheMgr.execute(
new SegmentCacheManager.Command<Void>() {
public Void call() throws Exception {
SegmentCacheIndex index =
cacheMgr.getIndexRegistry()
.getIndex(segmentWithData.getStar());
index.add(
segmentWithData.getHeader(),
response.converterMap.get(
SegmentCacheIndexImpl
.makeConverterKey(
segmentWithData.getHeader())),
true);
index.loadSucceeded(
segmentWithData.getHeader(), body);
return null;
}
public Locus getLocus() {
return locus;
}
});
}
}
// Wait for SQL statements to end -- but only if there are no
// failures.
//
// If there are failures, and its the first iteration, it's more
// urgent that we create and execute a follow-up request. We will
// wait for the pending SQL statements at the end of that.
//
// If there are failures on later iterations, wait for SQL
// statements to end. The cache might be porous. SQL might be the
// only way to make progress.
sqlSegmentMapFutures.addAll(response.sqlSegmentMapFutures);
if (failureCount == 0 || iteration > 0) {
// Wait on segments being loaded by someone else.
for (Map.Entry<SegmentHeader, Future<SegmentBody>> entry
: response.futures.entrySet())
{
final SegmentHeader header = entry.getKey();
final Future<SegmentBody> bodyFuture = entry.getValue();
final SegmentBody body = Util.safeGet(
bodyFuture,
"Waiting for someone else's segment to load via SQL");
final SegmentWithData segmentWithData =
response.convert(header, body);
segmentWithData.getStar().register(segmentWithData);
}
// Wait on segments being loaded by SQL statements we asked for.
for (Future<Map<Segment, SegmentWithData>> sqlSegmentMapFuture
: sqlSegmentMapFutures)
{
final Map<Segment, SegmentWithData> segmentMap =
Util.safeGet(
sqlSegmentMapFuture,
"Waiting for segment to load via SQL");
for (SegmentWithData segmentWithData : segmentMap.values())
{
segmentWithData.getStar().register(segmentWithData);
}
// TODO: also pass back SegmentHeader and SegmentBody,
// and add these to headerBodies. Might help?
}
}
if (failureCount == 0) {
break;
}
// Figure out which cell requests are not satisfied by any of the
// segments retrieved.
@SuppressWarnings("unchecked")
List<CellRequest> old = new ArrayList<CellRequest>(cellRequests1);
cellRequests1.clear();
for (CellRequest cellRequest : old) {
if (cellRequest.getMeasure().getStar()
.getCellFromCache(cellRequest, null) == null)
{
cellRequests1.add(cellRequest);
}
}
if (cellRequests1.isEmpty()) {
break;
}
if (cellRequests1.size() >= old.size()
&& iteration > 10)
{
throw Util.newError(
"Cache round-trip did not resolve any cell requests. "
+ "Iteration #" + iteration
+ "; request count " + cellRequests1.size()
+ "; requested headers: " + response.cacheSegments.size()
+ "; requested rollups: " + response.rollups.size()
+ "; requested SQL: "
+ response.sqlSegmentMapFutures.size());
}
// Continue loop; form and execute a new request with the smaller
// set of cell requests.
}
dirty = false;
cellRequests.clear();
return true;
}
/**
* Iterates through cell requests and makes sure .getCardinality has
* been called on all constrained columns. This is a workaround
* to an issue in which cardinality queries can be fired on the Actor
* thread, potentially causing a deadlock when interleaved with
* other threads that depend both on db connections and Actor responses.
*
*/
private void preloadColumnCardinality(List<CellRequest> cellRequests) {
List<BitKey> loaded = new ArrayList<BitKey>();
for (CellRequest req : cellRequests) {
if (!loaded.contains(req.getConstrainedColumnsBitKey())) {
for (RolapStar.Column col : req.getConstrainedColumns()) {
col.getCardinality();
}
loaded.add(req.getConstrainedColumnsBitKey());
}
}
}
/**
* Finds a segment-list among a list of candidate segment-lists
* for which the bodies of all segments are in cache. Returns a map
* from segment-to-body if found, or null if not found.
*
* @param headerBodies Cache of bodies previously retrieved from external
* cache
*
* @param rollup Specifies what segments to roll up, and the
* target dimensionality
*
* @return Collection of segment headers and bodies suitable for rollup,
* or null
*/
private Map<SegmentHeader, SegmentBody> findResidentRollupCandidate(
Map<SegmentHeader, SegmentBody> headerBodies,
BatchLoader.RollupInfo rollup)
{
candidateLoop:
for (List<SegmentHeader> headers : rollup.candidateLists) {
final Map<SegmentHeader, SegmentBody> map =
new HashMap<SegmentHeader, SegmentBody>();
for (SegmentHeader header : headers) {
SegmentBody body = loadSegmentFromCache(headerBodies, header);
if (body == null) {
// To proceed with a candidate, require all headers to
// be in cache.
continue candidateLoop;
}
map.put(header, body);
}
return map;
}
return null;
}
private SegmentBody loadSegmentFromCache(
Map<SegmentHeader, SegmentBody> headerBodies,
SegmentHeader header)
{
SegmentBody body = headerBodies.get(header);
if (body != null) {
return body;
}
body = cacheMgr.compositeCache.get(header);
if (body == null) {
if (cube.getStar() != null) {
cacheMgr.remove(cube.getStar(), header);
}
return null;
}
headerBodies.put(header, body);
return body;
}
/**
* Returns the SQL dialect. Overridden in some unit tests.
*
* @return Dialect
*/
Dialect getDialect() {
final RolapStar star = cube.getStar();
if (star != null) {
return star.getSqlQueryDialect();
} else {
return cube.getSchema().getDialect();
}
}
/**
* Sets the flag indicating that the reader has told a lie.
*/
void setDirty(boolean dirty) {
this.dirty = dirty;
}
}
/**
* Context for processing a request to the cache manager for segments matching a
* collection of cell requests. All methods except the constructor are executed
* by the cache manager's dedicated thread.
*/
class BatchLoader {
private static final Logger LOGGER =
Logger.getLogger(FastBatchingCellReader.class);
private final Locus locus;
private final SegmentCacheManager cacheMgr;
private final Dialect dialect;
private final RolapCube cube;
private final Map<AggregationKey, Batch> batches =
new HashMap<AggregationKey, Batch>();
private final Set<SegmentHeader> cacheHeaders =
new LinkedHashSet<SegmentHeader>();
private final Map<SegmentHeader, Future<SegmentBody>> futures =
new HashMap<SegmentHeader, Future<SegmentBody>>();
private final List<RollupInfo> rollups = new ArrayList<RollupInfo>();
private final Set<BitKey> rollupBitmaps = new HashSet<BitKey>();
private final Map<List, SegmentBuilder.SegmentConverter> converterMap =
new HashMap<List, SegmentBuilder.SegmentConverter>();
public BatchLoader(
Locus locus,
SegmentCacheManager cacheMgr,
Dialect dialect,
RolapCube cube)
{
this.locus = locus;
this.cacheMgr = cacheMgr;
this.dialect = dialect;
this.cube = cube;
}
final boolean shouldUseGroupingFunction() {
return MondrianProperties.instance().EnableGroupingSets.get()
&& dialect.supportsGroupingSets();
}
private void recordCellRequest2(final CellRequest request) {
// If there is a segment matching these criteria, write it to the list
// of found segments, and remove the cell request from the list.
final AggregationKey key = new AggregationKey(request);
final SegmentBuilder.SegmentConverterImpl converter =
new SegmentBuilder.SegmentConverterImpl(key, request);
boolean success =
loadFromCaches(request, key, converter);
// Skip the batch if we already have a rollup for it.
if (rollupBitmaps.contains(request.getConstrainedColumnsBitKey())) {
return;
}
// As a last resort, we load from SQL.
if (!success) {
loadFromSql(request, key, converter);
}
}
/**
* Loads a cell from caches. If the cell is successfully loaded,
* we return true.
*/
private boolean loadFromCaches(
final CellRequest request,
final AggregationKey key,
final SegmentBuilder.SegmentConverterImpl converter)
{
if (MondrianProperties.instance().DisableCaching.get()) {
// Caching is disabled. Return always false.
return false;
}
// Is request matched by one of the headers we intend to load?
final Map<String, Comparable> mappedCellValues =
request.getMappedCellValues();
final List<String> compoundPredicates =
request.getCompoundPredicateStrings();
for (SegmentHeader header : cacheHeaders) {
if (SegmentCacheIndexImpl.matches(
header,
mappedCellValues,
compoundPredicates))
{
// It's likely that the header will be in the cache, so this
// request will be satisfied. If not, the header will be removed
// from the segment index, and we'll be back.
return true;
}
}
final RolapStar.Measure measure = request.getMeasure();
final RolapStar star = measure.getStar();
final RolapSchema schema = star.getSchema();
final SegmentCacheIndex index =
cacheMgr.getIndexRegistry().getIndex(star);
final List<SegmentHeader> headersInCache =
index.locate(
schema.getName(),
schema.getChecksum(),
measure.getCubeName(),
measure.getName(),
star.getFactTable().getAlias(),
request.getConstrainedColumnsBitKey(),
mappedCellValues,
compoundPredicates);
// Ask for the first segment to be loaded from cache. (If it's no longer
// in cache, we'll be back, and presumably we'll try the second
// segment.)
if (!headersInCache.isEmpty()) {
for (SegmentHeader headerInCache : headersInCache) {
final Future<SegmentBody> future =
index.getFuture(locus.execution, headerInCache);
if (future != null) {
// Segment header is in cache, body is being loaded.
// Worker will need to wait for load to complete.
futures.put(headerInCache, future);
} else {
// Segment is in cache.
cacheHeaders.add(headerInCache);
}
index.setConverter(
headerInCache.schemaName,
headerInCache.schemaChecksum,
headerInCache.cubeName,
headerInCache.rolapStarFactTableName,
headerInCache.measureName,
headerInCache.compoundPredicates,
converter);
converterMap.put(
SegmentCacheIndexImpl.makeConverterKey(request, key),
converter);
}
return true;
}
// Try to roll up if the measure's rollup aggregator supports
// "fast" aggregation from raw objects.
//
// Do not try to roll up if this request has already chosen a rollup
// with the same target dimensionality. It is quite likely that the
// other rollup will satisfy this request, and it's complicated to be
// 100% sure. If we're wrong, we'll be back.
// Also make sure that we don't try to rollup a measure which
// doesn't support rollup from raw data, like a distinct count
// for example. Both the measure's aggregator and its rollup
// aggregator must support raw data aggregation. We call
// Aggregator.supportsFastAggregates() to verify.
if (MondrianProperties.instance()
.EnableInMemoryRollup.get()
&& measure.getAggregator().supportsFastAggregates(
measure.getDatatype())
&& measure.getAggregator().getRollup().supportsFastAggregates(
measure.getDatatype())
&& !isRequestCoveredByRollups(request))
{
// Don't even bother doing a segment lookup if we can't
// rollup that measure.
final List<List<SegmentHeader>> rollup =
index.findRollupCandidates(
schema.getName(),
schema.getChecksum(),
measure.getCubeName(),
measure.getName(),
star.getFactTable().getAlias(),
request.getConstrainedColumnsBitKey(),
mappedCellValues,
request.getCompoundPredicateStrings());
if (!rollup.isEmpty()) {
rollups.add(
new RollupInfo(
request,
rollup));
rollupBitmaps.add(request.getConstrainedColumnsBitKey());
converterMap.put(
SegmentCacheIndexImpl.makeConverterKey(request, key),
new SegmentBuilder.StarSegmentConverter(
measure,
key.getCompoundPredicateList()));
return true;
}
}
return false;
}
/**
* Checks if the request can be satisfied by a rollup already in place
* and moves that rollup to the top of the list if not there.
*/
private boolean isRequestCoveredByRollups(CellRequest request) {
BitKey bitKey = request.getConstrainedColumnsBitKey();
if (!rollupBitmaps.contains(bitKey)) {
return false;
}
List<SegmentHeader> firstOkList = null;
for (RollupInfo rollupInfo : rollups) {
if (!rollupInfo.constrainedColumnsBitKey.equals(bitKey)) {
continue;
}
int candidateListsIdx = 0;
// bitkey is the same, are the constrained values compatible?
candidatesLoop:
for (List<SegmentHeader> candList
: rollupInfo.candidateLists)
{
for (SegmentHeader header : candList) {
if (headerCoversRequest(header, request)) {
firstOkList = candList;
break candidatesLoop;
}
}
candidateListsIdx++;
}
if (firstOkList != null) {
if (candidateListsIdx > 0) {
// move good candidate list to first position
rollupInfo.candidateLists.remove(candidateListsIdx);
rollupInfo.candidateLists.set(0, firstOkList);
}
return true;
}
}
return false;
}
/**
* Check constraint compatibility
*/
private boolean headerCoversRequest(
SegmentHeader header,
CellRequest request)
{
BitKey bitKey = request.getConstrainedColumnsBitKey();
assert header.getConstrainedColumnsBitKey().cardinality()
>= bitKey.cardinality();
BitKey headerBitKey = header.getConstrainedColumnsBitKey();
// get all constrained values for relevant bitKey positions
List<SortedSet<Comparable>> headerValues =
new ArrayList<SortedSet<Comparable>>(bitKey.cardinality());
Map<Integer, Integer> valueIndexes = new HashMap<Integer, Integer>();
int relevantCCIdx = 0, keyValuesIdx = 0;
for (int bitPos : headerBitKey) {
if (bitKey.get(bitPos)) {
headerValues.add(
header.getConstrainedColumns().get(relevantCCIdx).values);
valueIndexes.put(bitPos, keyValuesIdx++);
}
relevantCCIdx++;
}
assert request.getConstrainedColumns().length
== request.getSingleValues().length;
// match header constraints against request values
for (int i = 0; i < request.getConstrainedColumns().length; i++) {
RolapStar.Column col = request.getConstrainedColumns()[i];
Integer valueIdx = valueIndexes.get(col.getBitPosition());
if (headerValues.get(valueIdx) != null
&& !headerValues.get(valueIdx).contains(
request.getSingleValues()[i]))
{
return false;
}
}
return true;
}
private void loadFromSql(
final CellRequest request,
final AggregationKey key,
final SegmentBuilder.SegmentConverterImpl converter)
{
// Finally, add to a batch. It will turn in to a SQL request.
Batch batch = batches.get(key);
if (batch == null) {
batch = new Batch(request);
batches.put(key, batch);
converterMap.put(
SegmentCacheIndexImpl.makeConverterKey(request, key),
converter);
if (LOGGER.isDebugEnabled()) {
StringBuilder buf = new StringBuilder(100);
buf.append("FastBatchingCellReader: bitkey=");
buf.append(request.getConstrainedColumnsBitKey());
buf.append(Util.nl);
for (RolapStar.Column column
: request.getConstrainedColumns())
{
buf.append(" ");
buf.append(column);
buf.append(Util.nl);
}
LOGGER.debug(buf.toString());
}
}
batch.add(request);
}
/**
* Determines which segments need to be loaded from external cache,
* created using roll up, or created using SQL to satisfy a given list
* of cell requests.
*
* @return List of segment futures. Each segment future may or may not be
* already present (it depends on the current location of the segment
* body). Each future will return a not-null segment (or throw).
*/
LoadBatchResponse load(List<CellRequest> cellRequests) {
// Check for cancel/timeout. The request might have been on the queue
// for a while.
if (locus.execution != null) {
locus.execution.checkCancelOrTimeout();
}
final long t1 = System.currentTimeMillis();
// Now we're inside the cache manager, we can see which of our cell
// requests can be answered from cache. Those that can will be added
// to the segments list; those that can not will be converted into
// batches and rolled up or loaded using SQL.
for (CellRequest cellRequest : cellRequests) {
recordCellRequest2(cellRequest);
}
// Sort the batches into deterministic order.
List<Batch> batchList =
new ArrayList<Batch>(batches.values());
Collections.sort(batchList, BatchComparator.instance);
final List<Future<Map<Segment, SegmentWithData>>> segmentMapFutures =
new ArrayList<Future<Map<Segment, SegmentWithData>>>();
if (shouldUseGroupingFunction()) {
LOGGER.debug("Using grouping sets");
List<CompositeBatch> groupedBatches = groupBatches(batchList);
for (CompositeBatch batch : groupedBatches) {
batch.load(segmentMapFutures);
}
} else {
// Load batches in turn.
for (Batch batch : batchList) {
batch.loadAggregation(segmentMapFutures);
}
}
if (LOGGER.isDebugEnabled()) {
final long t2 = System.currentTimeMillis();
LOGGER.debug("load (millis): " + (t2 - t1));
}
// Create a response and return it to the client. The response is a
// bunch of work to be done (waiting for segments to load from SQL, to
// come from cache, and so forth) on the client's time. Some of the bets
// may not come off, in which case, the client will send us another
// request.
return new LoadBatchResponse(
cellRequests,
new ArrayList<SegmentHeader>(cacheHeaders),
rollups,
converterMap,
segmentMapFutures,
futures);
}
static List<CompositeBatch> groupBatches(List<Batch> batchList) {
Map<AggregationKey, CompositeBatch> batchGroups =
new HashMap<AggregationKey, CompositeBatch>();
for (int i = 0; i < batchList.size(); i++) {
for (int j = i + 1; j < batchList.size();) {
final Batch iBatch = batchList.get(i);
final Batch jBatch = batchList.get(j);
if (iBatch.canBatch(jBatch)) {
batchList.remove(j);
addToCompositeBatch(batchGroups, iBatch, jBatch);
} else if (jBatch.canBatch(iBatch)) {
batchList.set(i, jBatch);
batchList.remove(j);
addToCompositeBatch(batchGroups, jBatch, iBatch);
j = i + 1;
} else {
j++;
}
}
}
wrapNonBatchedBatchesWithCompositeBatches(batchList, batchGroups);
final CompositeBatch[] compositeBatches =
batchGroups.values().toArray(
new CompositeBatch[batchGroups.size()]);
Arrays.sort(compositeBatches, CompositeBatchComparator.instance);
return Arrays.asList(compositeBatches);
}
private static void wrapNonBatchedBatchesWithCompositeBatches(
List<Batch> batchList,
Map<AggregationKey, CompositeBatch> batchGroups)
{
for (Batch batch : batchList) {
if (batchGroups.get(batch.batchKey) == null) {
batchGroups.put(batch.batchKey, new CompositeBatch(batch));
}
}
}
static void addToCompositeBatch(
Map<AggregationKey, CompositeBatch> batchGroups,
Batch detailedBatch,
Batch summaryBatch)
{
CompositeBatch compositeBatch = batchGroups.get(detailedBatch.batchKey);
if (compositeBatch == null) {
compositeBatch = new CompositeBatch(detailedBatch);
batchGroups.put(detailedBatch.batchKey, compositeBatch);
}
CompositeBatch compositeBatchOfSummaryBatch =
batchGroups.remove(summaryBatch.batchKey);
if (compositeBatchOfSummaryBatch != null) {
compositeBatch.merge(compositeBatchOfSummaryBatch);
} else {
compositeBatch.add(summaryBatch);
}
}
/**
* Command that loads the segments required for a collection of cell
* requests. Returns the collection of segments.
*/
public static class LoadBatchCommand
implements SegmentCacheManager.Command<LoadBatchResponse>
{
private final Locus locus;
private final SegmentCacheManager cacheMgr;
private final Dialect dialect;
private final RolapCube cube;
private final List<CellRequest> cellRequests;
private final Map<String, Object> mdc =
new HashMap<String, Object>();
public LoadBatchCommand(
Locus locus,
SegmentCacheManager cacheMgr,
Dialect dialect,
RolapCube cube,
List<CellRequest> cellRequests)
{
this.locus = locus;
this.cacheMgr = cacheMgr;
this.dialect = dialect;
this.cube = cube;
this.cellRequests = cellRequests;
if (MDC.getContext() != null) {
this.mdc.putAll(MDC.getContext());
}
}
public LoadBatchResponse call() {
if (MDC.getContext() != null) {
final Map<String, Object> old = MDC.getContext();
old.clear();
old.putAll(mdc);
}
return new BatchLoader(locus, cacheMgr, dialect, cube)
.load(cellRequests);
}
public Locus getLocus() {
return locus;
}
}
/**
* Set of Batches which can grouped together.
*/
static class CompositeBatch {
/** Batch with most number of constraint columns */
final Batch detailedBatch;
/** Batches whose data can be fetched using rollup on detailed batch */
final List<Batch> summaryBatches = new ArrayList<Batch>();
CompositeBatch(Batch detailedBatch) {
this.detailedBatch = detailedBatch;
}
void add(Batch summaryBatch) {
summaryBatches.add(summaryBatch);
}
void merge(CompositeBatch summaryBatch) {
summaryBatches.add(summaryBatch.detailedBatch);
summaryBatches.addAll(summaryBatch.summaryBatches);
}
public void load(
List<Future<Map<Segment, SegmentWithData>>> segmentFutures)
{
GroupingSetsCollector batchCollector =
new GroupingSetsCollector(true);
this.detailedBatch.loadAggregation(batchCollector, segmentFutures);
int cellRequestCount = 0;
for (Batch batch : summaryBatches) {
batch.loadAggregation(batchCollector, segmentFutures);
cellRequestCount += batch.cellRequestCount;
}
getSegmentLoader().load(
cellRequestCount,
batchCollector.getGroupingSets(),
detailedBatch.batchKey.getCompoundPredicateList(),
segmentFutures);
}
SegmentLoader getSegmentLoader() {
return new SegmentLoader(detailedBatch.getCacheMgr());
}
}
private static final Logger BATCH_LOGGER = Logger.getLogger(Batch.class);
public static class RollupInfo {
final RolapStar.Column[] constrainedColumns;
final BitKey constrainedColumnsBitKey;
final RolapStar.Measure measure;
final List<List<SegmentHeader>> candidateLists;
RollupInfo(
CellRequest request,
List<List<SegmentHeader>> candidateLists)
{
this.candidateLists = candidateLists;
constrainedColumns = request.getConstrainedColumns();
constrainedColumnsBitKey = request.getConstrainedColumnsBitKey();
measure = request.getMeasure();
}
}
/**
* Request sent from cache manager to a worker to load segments into
* the cache, create segments by rolling up, and to wait for segments
* being loaded via SQL.
*/
static class LoadBatchResponse {
/**
* List of segments that are being loaded using SQL.
*
* <p>Other workers are executing the SQL. When done, they will write a
* segment body or an error into the respective futures. The thread
* processing this request will wait on those futures, once all segments
* have successfully arrived from cache.</p>
*/
final List<Future<Map<Segment, SegmentWithData>>> sqlSegmentMapFutures;
/**
* List of segments we are trying to load from the cache.
*/
final List<SegmentHeader> cacheSegments;
/**
* List of cell requests that will be satisfied by segments we are
* trying to load from the cache (or create by rolling up).
*/
final List<CellRequest> cellRequests;
/**
* List of segments to be created from segments in the cache, provided
* that the cache segments come through.
*
* <p>If they do not, we will need to tell the cache manager to remove
* the pending segments.</p>
*/
final List<RollupInfo> rollups;
final Map<List, SegmentBuilder.SegmentConverter> converterMap;
final Map<SegmentHeader, Future<SegmentBody>> futures;
LoadBatchResponse(
List<CellRequest> cellRequests,
List<SegmentHeader> cacheSegments,
List<RollupInfo> rollups,
Map<List, SegmentBuilder.SegmentConverter> converterMap,
List<Future<Map<Segment, SegmentWithData>>> sqlSegmentMapFutures,
Map<SegmentHeader, Future<SegmentBody>> futures)
{
this.cellRequests = cellRequests;
this.sqlSegmentMapFutures = sqlSegmentMapFutures;
this.cacheSegments = cacheSegments;
this.rollups = rollups;
this.converterMap = converterMap;
this.futures = futures;
}
public SegmentWithData convert(
SegmentHeader header,
SegmentBody body)
{
final SegmentBuilder.SegmentConverter converter =
converterMap.get(
SegmentCacheIndexImpl.makeConverterKey(header));
return converter.convert(header, body);
}
}
public class Batch {
// the CellRequest's constrained columns
final RolapStar.Column[] columns;
final List<RolapStar.Measure> measuresList =
new ArrayList<RolapStar.Measure>();
final Set<StarColumnPredicate>[] valueSets;
final AggregationKey batchKey;
// string representation; for debug; set lazily in toString
private String string;
private int cellRequestCount;
private List<StarColumnPredicate[]> tuples =
new ArrayList<StarColumnPredicate[]>();
public Batch(CellRequest request) {
columns = request.getConstrainedColumns();
valueSets = new HashSet[columns.length];
for (int i = 0; i < valueSets.length; i++) {
valueSets[i] = new HashSet<StarColumnPredicate>();
}
batchKey = new AggregationKey(request);
}
public String toString() {
if (string == null) {
final StringBuilder buf = new StringBuilder();
buf.append("Batch {\n")
.append(" columns={").append(Arrays.toString(columns))
.append("}\n")
.append(" measures={").append(measuresList).append("}\n")
.append(" valueSets={").append(Arrays.toString(valueSets))
.append("}\n")
.append(" batchKey=").append(batchKey).append("}\n")
.append("}");
string = buf.toString();
}
return string;
}
public final void add(CellRequest request) {
++cellRequestCount;
final int valueCount = request.getNumValues();
final StarColumnPredicate[] tuple =
new StarColumnPredicate[valueCount];
for (int j = 0; j < valueCount; j++) {
final StarColumnPredicate value = request.getValueAt(j);
valueSets[j].add(value);
tuple[j] = value;
}
tuples.add(tuple);
final RolapStar.Measure measure = request.getMeasure();
if (!measuresList.contains(measure)) {
assert (measuresList.size() == 0)
|| (measure.getStar()
== (measuresList.get(0)).getStar())
: "Measure must belong to same star as other measures";
measuresList.add(measure);
}
}
/**
* Returns the RolapStar associated with the Batch's first Measure.
*
* <p>This method can only be called after the {@link #add} method has
* been called.
*
* @return the RolapStar associated with the Batch's first Measure
*/
private RolapStar getStar() {
RolapStar.Measure measure = measuresList.get(0);
return measure.getStar();
}
public BitKey getConstrainedColumnsBitKey() {
return batchKey.getConstrainedColumnsBitKey();
}
public SegmentCacheManager getCacheMgr() {
return cacheMgr;
}
public final void loadAggregation(
List<Future<Map<Segment, SegmentWithData>>> segmentFutures)
{
GroupingSetsCollector collectorWithGroupingSetsTurnedOff =
new GroupingSetsCollector(false);
loadAggregation(collectorWithGroupingSetsTurnedOff, segmentFutures);
}
final void loadAggregation(
GroupingSetsCollector groupingSetsCollector,
List<Future<Map<Segment, SegmentWithData>>> segmentFutures)
{
if (MondrianProperties.instance().GenerateAggregateSql.get()) {
generateAggregateSql();
}
final StarColumnPredicate[] predicates = initPredicates();
final long t1 = System.currentTimeMillis();
// TODO: optimize key sets; drop a constraint if more than x% of
// the members are requested; whether we should get just the cells
// requested or expand to a n-cube
// If the database cannot execute "count(distinct ...)", split the
// distinct aggregations out.
int distinctMeasureCount = getDistinctMeasureCount(measuresList);
boolean tooManyDistinctMeasures =
distinctMeasureCount > 0
&& !dialect.allowsCountDistinct()
|| distinctMeasureCount > 1
&& !dialect.allowsMultipleCountDistinct()
|| distinctMeasureCount > 0
&& !dialect.allowsCountDistinctWithOtherAggs();
if (tooManyDistinctMeasures) {
doSpecialHandlingOfDistinctCountMeasures(
predicates,
groupingSetsCollector,
segmentFutures);
}
// Load agg(distinct <SQL expression>) measures individually
// for DBs that does allow multiple distinct SQL measures.
if (!dialect.allowsMultipleDistinctSqlMeasures()) {
// Note that the intention was originally to capture the
// subquery SQL measures and separate them out; However,
// without parsing the SQL string, Mondrian cannot distinguish
// between "col1" + "col2" and subquery. Here the measure list
// contains both types.
// See the test case testLoadDistinctSqlMeasure() in
// mondrian.rolap.FastBatchingCellReaderTest
List<RolapStar.Measure> distinctSqlMeasureList =
getDistinctSqlMeasures(measuresList);
for (RolapStar.Measure measure : distinctSqlMeasureList) {
AggregationManager.loadAggregation(
cacheMgr,
cellRequestCount,
Collections.singletonList(measure),
columns,
batchKey,
predicates,
groupingSetsCollector,
segmentFutures);
measuresList.remove(measure);
}
}
final int measureCount = measuresList.size();
if (measureCount > 0) {
AggregationManager.loadAggregation(
cacheMgr,
cellRequestCount,
measuresList,
columns,
batchKey,
predicates,
groupingSetsCollector,
segmentFutures);
}
if (BATCH_LOGGER.isDebugEnabled()) {
final long t2 = System.currentTimeMillis();
BATCH_LOGGER.debug(
"Batch.load (millis) " + (t2 - t1));
}
}
private void doSpecialHandlingOfDistinctCountMeasures(
StarColumnPredicate[] predicates,
GroupingSetsCollector groupingSetsCollector,
List<Future<Map<Segment, SegmentWithData>>> segmentFutures)
{
while (true) {
// Scan for a measure based upon a distinct aggregation.
final RolapStar.Measure distinctMeasure =
getFirstDistinctMeasure(measuresList);
if (distinctMeasure == null) {
break;
}
final String expr =
distinctMeasure.getExpression().getGenericExpression();
final List<RolapStar.Measure> distinctMeasuresList =
new ArrayList<RolapStar.Measure>();
for (int i = 0; i < measuresList.size();) {
final RolapStar.Measure measure = measuresList.get(i);
if (measure.getAggregator().isDistinct()
&& measure.getExpression().getGenericExpression()
.equals(expr))
{
measuresList.remove(i);
distinctMeasuresList.add(distinctMeasure);
} else {
i++;
}
}
// Load all the distinct measures based on the same expression
// together
AggregationManager.loadAggregation(
cacheMgr,
cellRequestCount,
distinctMeasuresList,
columns,
batchKey,
predicates,
groupingSetsCollector,
segmentFutures);
}
}
private StarColumnPredicate[] initPredicates() {
StarColumnPredicate[] predicates =
new StarColumnPredicate[columns.length];
for (int j = 0; j < columns.length; j++) {
Set<StarColumnPredicate> valueSet = valueSets[j];
StarColumnPredicate predicate;
if (valueSet == null) {
predicate = LiteralStarPredicate.FALSE;
} else {
ValueColumnPredicate[] values =
valueSet.toArray(
new ValueColumnPredicate[valueSet.size()]);
// Sort array to achieve determinism in generated SQL.
Arrays.sort(
values,
ValueColumnConstraintComparator.instance);
predicate =
new ListColumnPredicate(
columns[j],
Arrays.asList((StarColumnPredicate[]) values));
}
predicates[j] = predicate;
}
return predicates;
}
private void generateAggregateSql() {
if (cube == null || cube.isVirtual()) {
final StringBuilder buf = new StringBuilder(64);
buf.append(
"AggGen: Sorry, can not create SQL for virtual Cube \"")
.append(cube == null ? null : cube.getName())
.append("\", operation not currently supported");
BATCH_LOGGER.error(buf.toString());
} else {
final AggGen aggGen =
new AggGen(cube.getName(), cube.getStar(), columns);
if (aggGen.isReady()) {
// PRINT TO STDOUT - DO NOT USE BATCH_LOGGER
System.out.println(
"createLost:" + Util.nl + aggGen.createLost());
System.out.println(
"insertIntoLost:" + Util.nl + aggGen.insertIntoLost());
System.out.println(
"createCollapsed:" + Util.nl
+ aggGen.createCollapsed());
System.out.println(
"insertIntoCollapsed:" + Util.nl
+ aggGen.insertIntoCollapsed());
} else {
BATCH_LOGGER.error("AggGen failed");
}
}
}
/**
* Returns the first measure based upon a distinct aggregation, or null
* if there is none.
*/
final RolapStar.Measure getFirstDistinctMeasure(
List<RolapStar.Measure> measuresList)
{
for (RolapStar.Measure measure : measuresList) {
if (measure.getAggregator().isDistinct()) {
return measure;
}
}
return null;
}
/**
* Returns the number of the measures based upon a distinct
* aggregation.
*/
private int getDistinctMeasureCount(
List<RolapStar.Measure> measuresList)
{
int count = 0;
for (RolapStar.Measure measure : measuresList) {
if (measure.getAggregator().isDistinct()) {
++count;
}
}
return count;
}
/**
* Returns the list of measures based upon a distinct aggregation
* containing SQL measure expressions(as opposed to column expressions).
*
* This method was initially intended for only those measures that are
* defined using subqueries(for DBs that support them). However, since
* Mondrian does not parse the SQL string, the method will count both
* queries as well as some non query SQL expressions.
*/
private List<RolapStar.Measure> getDistinctSqlMeasures(
List<RolapStar.Measure> measuresList)
{
List<RolapStar.Measure> distinctSqlMeasureList =
new ArrayList<RolapStar.Measure>();
for (RolapStar.Measure measure : measuresList) {
if (measure.getAggregator().isDistinct()
&& measure.getExpression() instanceof
MondrianDef.MeasureExpression)
{
MondrianDef.MeasureExpression measureExpr =
(MondrianDef.MeasureExpression) measure.getExpression();
MondrianDef.SQL measureSql = measureExpr.expressions[0];
// Checks if the SQL contains "SELECT" to detect the case a
// subquery is used to define the measure. This is not a
// perfect check, because a SQL expression on column names
// containing "SELECT" will also be detected. e,g,
// count("select beef" + "regular beef").
if (measureSql.cdata.toUpperCase().contains("SELECT")) {
distinctSqlMeasureList.add(measure);
}
}
}
return distinctSqlMeasureList;
}
/**
* Returns whether another Batch can be batched to this Batch.
*
* <p>This is possible if:
* <li>columns list is super set of other batch's constraint columns;
* and
* <li>both have same Fact Table; and
* <li>matching columns of this and other batch has the same value; and
* <li>non matching columns of this batch have ALL VALUES
* </ul>
*/
boolean canBatch(Batch other) {
return hasOverlappingBitKeys(other)
&& constraintsMatch(other)
&& hasSameMeasureList(other)
&& !hasDistinctCountMeasure()
&& !other.hasDistinctCountMeasure()
&& haveSameStarAndAggregation(other)
&& haveSameClosureColumns(other);
}
/**
* Returns whether the constraints on this Batch subsume the constraints
* on another Batch and therefore the other Batch can be subsumed into
* this one for GROUPING SETS purposes. Not symmetric.
*
* @param other Other batch
* @return Whether other batch can be subsumed into this one
*/
private boolean constraintsMatch(Batch other) {
if (areBothDistinctCountBatches(other)) {
if (getConstrainedColumnsBitKey().equals(
other.getConstrainedColumnsBitKey()))
{
return hasSameCompoundPredicate(other)
&& haveSameValues(other);
} else {
return hasSameCompoundPredicate(other)
|| (other.batchKey.getCompoundPredicateList().isEmpty()
|| equalConstraint(
batchKey.getCompoundPredicateList(),
other.batchKey.getCompoundPredicateList()))
&& haveSameValues(other);
}
} else {
return haveSameValues(other);
}
}
private boolean equalConstraint(
List<StarPredicate> predList1,
List<StarPredicate> predList2)
{
if (predList1.size() != predList2.size()) {
return false;
}
for (int i = 0; i < predList1.size(); i++) {
StarPredicate pred1 = predList1.get(i);
StarPredicate pred2 = predList2.get(i);
if (!pred1.equalConstraint(pred2)) {
return false;
}
}
return true;
}
private boolean areBothDistinctCountBatches(Batch other) {
return this.hasDistinctCountMeasure()
&& !this.hasNormalMeasures()
&& other.hasDistinctCountMeasure()
&& !other.hasNormalMeasures();
}
private boolean hasNormalMeasures() {
return getDistinctMeasureCount(measuresList)
!= measuresList.size();
}
private boolean hasSameMeasureList(Batch other) {
return this.measuresList.size() == other.measuresList.size()
&& this.measuresList.containsAll(other.measuresList);
}
boolean hasOverlappingBitKeys(Batch other) {
return getConstrainedColumnsBitKey()
.isSuperSetOf(other.getConstrainedColumnsBitKey());
}
boolean hasDistinctCountMeasure() {
return getDistinctMeasureCount(measuresList) > 0;
}
boolean hasSameCompoundPredicate(Batch other) {
final StarPredicate starPredicate = compoundPredicate();
final StarPredicate otherStarPredicate = other.compoundPredicate();
if (starPredicate == null && otherStarPredicate == null) {
return true;
} else if (starPredicate != null && otherStarPredicate != null) {
return starPredicate.equalConstraint(otherStarPredicate);
}
return false;
}
private StarPredicate compoundPredicate() {
StarPredicate predicate = null;
for (Set<StarColumnPredicate> valueSet : valueSets) {
StarPredicate orPredicate = null;
for (StarColumnPredicate starColumnPredicate : valueSet) {
if (orPredicate == null) {
orPredicate = starColumnPredicate;
} else {
orPredicate = orPredicate.or(starColumnPredicate);
}
}
if (predicate == null) {
predicate = orPredicate;
} else {
predicate = predicate.and(orPredicate);
}
}
for (StarPredicate starPredicate
: batchKey.getCompoundPredicateList())
{
if (predicate == null) {
predicate = starPredicate;
} else {
predicate = predicate.and(starPredicate);
}
}
return predicate;
}
boolean haveSameStarAndAggregation(Batch other) {
boolean rollup[] = {false};
boolean otherRollup[] = {false};
boolean hasSameAggregation =
getAgg(rollup) == other.getAgg(otherRollup);
boolean hasSameRollupOption = rollup[0] == otherRollup[0];
boolean hasSameStar = getStar().equals(other.getStar());
return hasSameStar && hasSameAggregation && hasSameRollupOption;
}
/**
* Returns whether this batch has the same closure columns as another.
*
* <p>Ensures that we do not group together a batch that includes a
* level of a parent-child closure dimension with a batch that does not.
* It is not safe to roll up from a parent-child closure level; due to
* multiple accounting, the 'all' level is less than the sum of the
* members of the closure level.
*
* @param other Other batch
* @return Whether batches have the same closure columns
*/
boolean haveSameClosureColumns(Batch other) {
final BitKey cubeClosureColumnBitKey = cube.closureColumnBitKey;
if (cubeClosureColumnBitKey == null) {
// Virtual cubes have a null bitkey. For now, punt; should do
// better.
return true;
}
final BitKey closureColumns =
this.batchKey.getConstrainedColumnsBitKey()
.and(cubeClosureColumnBitKey);
final BitKey otherClosureColumns =
other.batchKey.getConstrainedColumnsBitKey()
.and(cubeClosureColumnBitKey);
return closureColumns.equals(otherClosureColumns);
}
/**
* @param rollup Out parameter
* @return AggStar
*/
private AggStar getAgg(boolean[] rollup) {
return AggregationManager.findAgg(
getStar(),
getConstrainedColumnsBitKey(),
makeMeasureBitKey(),
rollup);
}
private BitKey makeMeasureBitKey() {
BitKey bitKey = getConstrainedColumnsBitKey().emptyCopy();
for (RolapStar.Measure measure : measuresList) {
bitKey.set(measure.getBitPosition());
}
return bitKey;
}
/**
* Return whether have same values for overlapping columns or
* has all children for others.
*/
boolean haveSameValues(
Batch other)
{
for (int j = 0; j < columns.length; j++) {
boolean isCommonColumn = false;
for (int i = 0; i < other.columns.length; i++) {
if (areSameColumns(other.columns[i], columns[j])) {
if (hasSameValues(other.valueSets[i], valueSets[j])) {
isCommonColumn = true;
break;
} else {
return false;
}
}
}
if (!isCommonColumn
&& !hasAllValues(columns[j], valueSets[j]))
{
return false;
}
}
return true;
}
private boolean hasAllValues(
RolapStar.Column column,
Set<StarColumnPredicate> valueSet)
{
return column.getCardinality() == valueSet.size();
}
private boolean areSameColumns(
RolapStar.Column otherColumn,
RolapStar.Column thisColumn)
{
return otherColumn.equals(thisColumn);
}
private boolean hasSameValues(
Set<StarColumnPredicate> otherValueSet,
Set<StarColumnPredicate> thisValueSet)
{
return otherValueSet.equals(thisValueSet);
}
}
private static class CompositeBatchComparator
implements Comparator<CompositeBatch>
{
static final CompositeBatchComparator instance =
new CompositeBatchComparator();
public int compare(CompositeBatch o1, CompositeBatch o2) {
return BatchComparator.instance.compare(
o1.detailedBatch,
o2.detailedBatch);
}
}
private static class BatchComparator implements Comparator<Batch> {
static final BatchComparator instance = new BatchComparator();
private BatchComparator() {
}
public int compare(
Batch o1, Batch o2)
{
if (o1.columns.length != o2.columns.length) {
return o1.columns.length - o2.columns.length;
}
for (int i = 0; i < o1.columns.length; i++) {
int c = o1.columns[i].getName().compareTo(
o2.columns[i].getName());
if (c != 0) {
return c;
}
}
for (int i = 0; i < o1.columns.length; i++) {
int c = compare(o1.valueSets[i], o2.valueSets[i]);
if (c != 0) {
return c;
}
}
return 0;
}
<T> int compare(Set<T> set1, Set<T> set2) {
if (set1.size() != set2.size()) {
return set1.size() - set2.size();
}
Iterator<T> iter1 = set1.iterator();
Iterator<T> iter2 = set2.iterator();
while (iter1.hasNext()) {
T v1 = iter1.next();
T v2 = iter2.next();
int c = Util.compareKey(v1, v2);
if (c != 0) {
return c;
}
}
return 0;
}
}
private static class ValueColumnConstraintComparator
implements Comparator<ValueColumnPredicate>
{
static final ValueColumnConstraintComparator instance =
new ValueColumnConstraintComparator();
private ValueColumnConstraintComparator() {
}
public int compare(
ValueColumnPredicate o1,
ValueColumnPredicate o2)
{
Object v1 = o1.getValue();
Object v2 = o2.getValue();
if (v1.getClass() == v2.getClass()
&& v1 instanceof Comparable)
{
return ((Comparable) v1).compareTo(v2);
} else {
return v1.toString().compareTo(v2.toString());
}
}
}
}
// End FastBatchingCellReader.java