com.hazelcast.query.impl.AttributeIndexRegistry Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.hazelcast.query.impl;
import com.hazelcast.core.TypeConverter;
import com.hazelcast.monitor.impl.PerIndexStats;
import com.hazelcast.nio.serialization.Data;
import com.hazelcast.query.Predicate;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static com.hazelcast.query.impl.AbstractIndex.NULL;
import static com.hazelcast.query.impl.Comparison.GREATER;
import static com.hazelcast.query.impl.Comparison.GREATER_OR_EQUAL;
import static com.hazelcast.query.impl.Comparison.LESS;
import static com.hazelcast.query.impl.CompositeValue.NEGATIVE_INFINITY;
import static com.hazelcast.query.impl.CompositeValue.POSITIVE_INFINITY;
import static java.util.Collections.emptySet;
/**
* Maintains a registry of single-attribute indexes.
*
* The main purpose of this class is to maintain decorators around ordered
* composite indexes to allow transparent querying on their first components.
*/
public class AttributeIndexRegistry {
private final ConcurrentMap registry = new ConcurrentHashMap();
/**
* Registers the given index in this registry.
*
* This method expects external thread access synchronization applied, so
* there is no more than one writer at any given time.
*
* @param index the index to register.
* @see Indexes#addOrGetIndex
*/
public void register(InternalIndex index) {
String[] components = index.getComponents();
String firstComponent = components[0];
Record record = registry.get(firstComponent);
if (record == null) {
record = new Record();
registry.put(firstComponent, record);
}
if (index.isOrdered()) {
if (record.orderedWorseThan(index)) {
record.ordered = components.length == 1 ? index : new FirstComponentDecorator(index);
}
} else {
if (record.unorderedWorseThan(index)) {
record.unordered = index;
}
}
}
/**
* Matches an index for the given attribute and match hint.
*
* @param attribute the attribute to match an index for.
* @param matchHint the match hint; {@link QueryContext.IndexMatchHint#EXACT_NAME}
* is not supported by this method.
* @return the matched index or {@code null} if nothing matched.
* @see QueryContext.IndexMatchHint
*/
public InternalIndex match(String attribute, QueryContext.IndexMatchHint matchHint) {
Record record = registry.get(attribute);
if (record == null) {
return null;
}
switch (matchHint) {
case NONE:
// Intentional fallthrough. We still prefer ordered indexes
// under the cover since they are more universal in terms of
// supported fast queries.
case PREFER_ORDERED:
InternalIndex ordered = record.ordered;
return ordered == null ? record.unordered : ordered;
case PREFER_UNORDERED:
InternalIndex unordered = record.unordered;
return unordered == null ? record.ordered : unordered;
default:
throw new IllegalStateException("unexpected match hint: " + matchHint);
}
}
/**
* Clears this registry by unregistering all previously registered indexes.
*/
public void clear() {
registry.clear();
}
private static class Record {
// It's enough to have volatile non-atomic mutations here, just to ensure
// the visibility, since only a single thread may mutate a record at any
// given time. All invocations are coming from Indexes.addOrGetIndex
// which is synchronized.
volatile InternalIndex unordered;
volatile InternalIndex ordered;
public boolean unorderedWorseThan(InternalIndex candidate) {
assert !candidate.isOrdered();
// we have no index and the unordered candidate is not composite
return unordered == null && candidate.getComponents().length == 1;
}
public boolean orderedWorseThan(InternalIndex candidate) {
assert candidate.isOrdered();
InternalIndex current = ordered;
if (current == null) {
// any ordered index is better than nothing
return true;
}
if (current instanceof FirstComponentDecorator) {
// the current index is composite
String[] candidateComponents = candidate.getComponents();
if (candidateComponents.length > 1) {
// if the current index has more components, replace it
FirstComponentDecorator currentDecorator = (FirstComponentDecorator) current;
return currentDecorator.width > candidateComponents.length;
}
// any non-composite candidate is better than composite index
return true;
}
return false;
}
}
/**
* Decorates first component of ordered composite indexes to allow
* transparent querying on it
*
* Exposed as a package-private class only for testing purposes.
*/
static final class FirstComponentDecorator implements InternalIndex {
// See CompositeValue docs for more details on what is going on in the
// index querying methods.
// exposed as a package-private field only for testing purposes
final InternalIndex delegate;
private final int width;
private final String[] components;
public FirstComponentDecorator(InternalIndex delegate) {
assert delegate.getComponents().length > 1;
assert delegate.isOrdered();
this.delegate = delegate;
this.width = delegate.getComponents().length;
this.components = new String[]{delegate.getComponents()[0]};
}
@Override
public String getName() {
throw newUnsupportedException();
}
@Override
public String[] getComponents() {
return components;
}
@Override
public boolean isOrdered() {
return delegate.isOrdered();
}
@Override
public String getUniqueKey() {
return delegate.getUniqueKey();
}
@Override
public TypeConverter getConverter() {
CompositeConverter converter = (CompositeConverter) delegate.getConverter();
return converter == null ? null : converter.getComponentConverter(0);
}
@Override
public void putEntry(QueryableEntry entry, Object oldValue, OperationSource operationSource) {
throw newUnsupportedException();
}
@Override
public void removeEntry(Data key, Object value, OperationSource operationSource) {
throw newUnsupportedException();
}
@Override
public boolean isEvaluateOnly() {
return delegate.isEvaluateOnly();
}
@Override
public boolean canEvaluate(Class extends Predicate> predicateClass) {
return delegate.canEvaluate(predicateClass);
}
@Override
public Set evaluate(Predicate predicate) {
return delegate.evaluate(predicate);
}
@Override
public Set getRecords(Comparable value) {
Comparable from = new CompositeValue(width, value, NEGATIVE_INFINITY);
Comparable to = new CompositeValue(width, value, POSITIVE_INFINITY);
return delegate.getRecords(from, false, to, false);
}
@SuppressWarnings("checkstyle:npathcomplexity")
@Override
public Set getRecords(Comparable[] values) {
if (values.length == 0) {
return emptySet();
}
TypeConverter converter = getConverter();
if (converter == null) {
return emptySet();
}
if (values.length == 1) {
return getRecords(values[0]);
}
Set convertedValues = new HashSet();
for (Comparable value : values) {
Comparable converted = converter.convert(value);
convertedValues.add(canonicalizeQueryArgumentScalar(converted));
}
if (convertedValues.size() == 1) {
return getRecords(convertedValues.iterator().next());
}
Set result = new HashSet();
for (Comparable value : convertedValues) {
result.addAll(getRecords(value));
}
return result;
}
@Override
public Set getRecords(Comparable from, boolean fromInclusive, Comparable to, boolean toInclusive) {
Comparable compositeFrom = new CompositeValue(width, from, fromInclusive ? NEGATIVE_INFINITY : POSITIVE_INFINITY);
Comparable compositeTo = new CompositeValue(width, to, toInclusive ? POSITIVE_INFINITY : NEGATIVE_INFINITY);
return delegate.getRecords(compositeFrom, false, compositeTo, false);
}
@Override
public Set getRecords(Comparison comparison, Comparable value) {
switch (comparison) {
case NOT_EQUAL:
Set result = new HashSet();
result.addAll(delegate.getRecords(LESS, new CompositeValue(width, value, NEGATIVE_INFINITY)));
result.addAll(delegate.getRecords(GREATER, new CompositeValue(width, value, POSITIVE_INFINITY)));
return result;
case LESS:
CompositeValue lessFrom = new CompositeValue(width, NULL, POSITIVE_INFINITY);
CompositeValue lessTo = new CompositeValue(width, value, NEGATIVE_INFINITY);
return delegate.getRecords(lessFrom, false, lessTo, false);
case GREATER:
return delegate.getRecords(GREATER, new CompositeValue(width, value, POSITIVE_INFINITY));
case LESS_OR_EQUAL:
CompositeValue greaterOrEqualFrom = new CompositeValue(width, NULL, POSITIVE_INFINITY);
CompositeValue greaterOrEqualTo = new CompositeValue(width, value, POSITIVE_INFINITY);
return delegate.getRecords(greaterOrEqualFrom, false, greaterOrEqualTo, false);
case GREATER_OR_EQUAL:
return delegate.getRecords(GREATER_OR_EQUAL, new CompositeValue(width, value, NEGATIVE_INFINITY));
default:
throw new IllegalStateException("unexpected comparison: " + comparison);
}
}
@Override
public void clear() {
throw newUnsupportedException();
}
@Override
public void destroy() {
throw newUnsupportedException();
}
@Override
public Comparable canonicalizeQueryArgumentScalar(Comparable value) {
return delegate.canonicalizeQueryArgumentScalar(value);
}
@Override
public boolean hasPartitionIndexed(int partitionId) {
throw newUnsupportedException();
}
@Override
public boolean allPartitionsIndexed(int ownedPartitionCount) {
return delegate.allPartitionsIndexed(ownedPartitionCount);
}
@Override
public void markPartitionAsIndexed(int partitionId) {
throw newUnsupportedException();
}
@Override
public void markPartitionAsUnindexed(int partitionId) {
throw newUnsupportedException();
}
@Override
public PerIndexStats getPerIndexStats() {
return delegate.getPerIndexStats();
}
private RuntimeException newUnsupportedException() {
return new UnsupportedOperationException("decorated composite indexes support only querying");
}
}
}