All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hazelcast.query.impl.OrderedIndexStore Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2008-2024, 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.internal.serialization.Data;
import com.hazelcast.query.Predicate;

import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableMap;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.stream.Stream;

import static com.hazelcast.query.impl.AbstractIndex.NULL;
import static com.hazelcast.query.impl.CompositeValue.POSITIVE_INFINITY;
import static java.util.Collections.emptyIterator;
import static java.util.Collections.emptySet;

/**
 * Store indexes rankly.
 */
@SuppressWarnings("rawtypes")
public class OrderedIndexStore extends BaseSingleValueIndexStore {
    public static final Comparator DATA_COMPARATOR = new DataComparator();

    public static final Comparator SPECIAL_AWARE_COMPARATOR = (left, right) -> {
        // compare to explicit instances of special Comparables to avoid infinite loop
        // NEGATIVE_INFINITY should not be used in the index or queries
        // - the same result can be achieved by inclusive NULL or null.
        if (right == NULL) {
            return -NULL.compareTo(left);
        } else if (right == POSITIVE_INFINITY) {
            return -POSITIVE_INFINITY.compareTo(left);
        } else if (left == null && right == null) {
            return 0;
        } else if (left != null && right != null) {
            return Comparables.compare(left, right);
        } else if (left == null) {
            return -1;
        } else {
            return 1;
        }
    };

    private final ConcurrentSkipListMap> recordMap =
            new ConcurrentSkipListMap<>(SPECIAL_AWARE_COMPARATOR);

    private final IndexFunctor addFunctor;
    private final IndexFunctor removeFunctor;

    public OrderedIndexStore(IndexCopyBehavior copyOn) {
        super(copyOn, true);
        assert copyOn != null;
        if (copyOn == IndexCopyBehavior.COPY_ON_WRITE) {
            addFunctor = new CopyOnWriteAddFunctor();
            removeFunctor = new CopyOnWriteRemoveFunctor();
        } else {
            addFunctor = new AddFunctor();
            removeFunctor = new RemoveFunctor();
        }
    }

    @Override
    Object insertInternal(Comparable value, QueryableEntry record) {
        return addFunctor.invoke(value, record);
    }

    @Override
    Object removeInternal(Comparable value, Data recordKey) {
        return removeFunctor.invoke(value, recordKey);
    }

    @Override
    public Comparable canonicalizeQueryArgumentScalar(Comparable value) {
        // We still need to canonicalize query arguments for ordered indexes to
        // support InPredicate queries.
        return Comparables.canonicalizeForHashLookup(value);
    }

    @Override
    public Comparable canonicalizeScalarForStorage(Comparable value) {
        // Returning the original value since ordered indexes are not supporting
        // hash lookups on their stored values, so there is no need in providing
        // canonical representations.
        return value;
    }

    @Override
    public void clear() {
        takeWriteLock();
        try {
            recordMap.clear();
        } finally {
            releaseWriteLock();
        }
    }

    @Override
    public boolean isEvaluateOnly() {
        return false;
    }

    @Override
    public boolean canEvaluate(Class predicateClass) {
        return false;
    }

    @Override
    public Set evaluate(Predicate predicate, TypeConverter converter) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Iterator getSqlRecordIterator(boolean descending) {
        return new IteratorFromBatch(getSqlRecordIteratorBatch(descending));
    }

    @Override
    public Iterator getSqlRecordIterator(@Nonnull Comparable value) {
        return new IteratorFromBatch(getSqlRecordIteratorBatch(value, false));
    }

    @Override
    public Iterator getSqlRecordIterator(Comparison comparison, Comparable searchedValue, boolean descending) {
        return new IteratorFromBatch(getSqlRecordIteratorBatch(comparison, searchedValue, descending));
    }

    @Override
    public Iterator getSqlRecordIterator(
            Comparable from,
            boolean fromInclusive,
            Comparable to,
            boolean toInclusive,
            boolean descending
    ) {
        return new IteratorFromBatch(getSqlRecordIteratorBatch(from, fromInclusive, to, toInclusive, descending));
    }

    @Override
    public Iterator getSqlRecordIteratorBatch(@Nonnull Comparable value, boolean descending) {
        return getSqlRecordIteratorBatch(value, descending, null);
    }

    @Override
    public Iterator getSqlRecordIteratorBatch(
            @Nonnull Comparable value,
            boolean descending,
            Data lastEntryKeyData
    ) {
        var entries = recordMap.get(value);

        if (entries == null) {
            return emptyIterator();
        }
        entries = descending ? entries.descendingMap() : entries;

        if (lastEntryKeyData != null) {
            entries = entries.tailMap(lastEntryKeyData, false);
        }

        return Stream.of(
                new IndexKeyEntries(
                        value,
                        entries.values().iterator()
                )
        ).iterator();

    }

    @Override
    public Iterator getSqlRecordIteratorBatch(boolean descending) {
        if (descending) {
            return recordMap.descendingMap().entrySet()
                    .stream()
                    .map((Entry> es) ->
                            new IndexKeyEntries(es.getKey(), es.getValue().descendingMap().values().iterator()))
                    .iterator();
        } else {
            return recordMap.entrySet()
                    .stream()
                    .map((Entry> es) ->
                            new IndexKeyEntries(es.getKey(), es.getValue().values().iterator()))
                    .iterator();
        }
    }

    @Override
    public Iterator getSqlRecordIteratorBatch(
            @Nonnull Comparison comparison,
            @Nonnull Comparable searchedValue,
            boolean descending
    ) {
        return getSqlRecordIteratorBatch(comparison, searchedValue, descending, null);
    }

    @Override
    public Iterator getSqlRecordIteratorBatch(
            @Nonnull Comparison comparison,
            @Nonnull Comparable searchedValue,
            boolean descending,
            Data lastEntryKeyData
    ) {
        switch (comparison) {
            case LESS:
                return getSqlRecordIteratorBatch(NULL, false, searchedValue, false, descending, lastEntryKeyData);
            case LESS_OR_EQUAL:
                return getSqlRecordIteratorBatch(NULL, false, searchedValue, true, descending, lastEntryKeyData);
            case GREATER:
                return getSqlRecordIteratorBatch(searchedValue, false, POSITIVE_INFINITY, true, descending, lastEntryKeyData);
            case GREATER_OR_EQUAL:
                return getSqlRecordIteratorBatch(searchedValue, true, POSITIVE_INFINITY, true, descending, lastEntryKeyData);
            default:
                throw new IllegalArgumentException("Unrecognized comparison: " + comparison);
        }
    }

    @Override
    @SuppressWarnings("checkstyle:NPathComplexity")
    public Iterator getSqlRecordIteratorBatch(
            @Nonnull Comparable from,
            boolean fromInclusive,
            @Nonnull Comparable to,
            boolean toInclusive,
            boolean descending
    ) {
        return getSqlRecordIteratorBatch(from, fromInclusive, to, toInclusive, descending, null);
    }

    @Override
    @SuppressWarnings({"checkstyle:NPathComplexity", "checkstyle:CyclomaticComplexity"})
    public Iterator getSqlRecordIteratorBatch(
            @Nonnull Comparable from,
            boolean fromInclusive,
            @Nonnull Comparable to,
            boolean toInclusive,
            boolean descending,
            Data lastEntryKeyData
    ) {
        boolean useCursor = lastEntryKeyData != null;
        if (useCursor && !descending && !fromInclusive) {
            throw new IllegalArgumentException("If `lastEntryKeyData` is not null then `from` must be inclusive");
        }
        if (useCursor && descending && !toInclusive) {
            throw new IllegalArgumentException("If `lastEntryKeyData` is not null then `to` must be inclusive");
        }

        int order = SPECIAL_AWARE_COMPARATOR.compare(from, to);
        if (order == 0) {
            if (!fromInclusive || !toInclusive) {
                return emptyIterator();
            }
            return getSqlRecordIteratorBatch(from, descending, lastEntryKeyData);
        } else if (order > 0) {
            return emptyIterator();
        }

        var subMap = descending
                ? recordMap.subMap(from, fromInclusive, to, toInclusive).descendingMap()
                : recordMap.subMap(from, fromInclusive, to, toInclusive);


        var indexKeyForLastEntryKeyData = descending ? to : from;
        return subMap.entrySet()
                .stream()
                .map(
                        (Entry> es) -> {
                            var map = descending ? es.getValue().descendingMap() : es.getValue();
                            if (useCursor && SPECIAL_AWARE_COMPARATOR.compare(indexKeyForLastEntryKeyData, es.getKey()) == 0) {
                                map = map.tailMap(lastEntryKeyData, false);
                            }
                            return new IndexKeyEntries(es.getKey(), map.values().iterator());
                        }
                )
                .iterator();
    }


    @Override
    public Set getRecords(Comparable value) {
        takeReadLock();
        try {
            return toSingleResultSet(recordMap.get(value));
        } finally {
            releaseReadLock();
        }
    }

    @Override
    public Set getRecords(Set values) {
        takeReadLock();
        try {
            MultiResultSet results = createMultiResultSet();
            for (Comparable value : values) {
                Map records;
                records = recordMap.get(value);
                if (records != null) {
                    copyToMultiResultSet(results, records);
                }
            }
            return results;
        } finally {
            releaseReadLock();
        }
    }

    @Override
    public Set getRecords(Comparison comparison, Comparable searchedValue) {
        switch (comparison) {
            case LESS:
                return getRecords(NULL, false, searchedValue, false);
            case LESS_OR_EQUAL:
                return getRecords(NULL, false, searchedValue, true);
            case GREATER:
                return getRecords(searchedValue, false, POSITIVE_INFINITY, true);
            case GREATER_OR_EQUAL:
                return getRecords(searchedValue, true, POSITIVE_INFINITY, true);
            default:
                throw new IllegalArgumentException("Unrecognized comparison: " + comparison);
        }
    }

    @Override
    public Set getRecords(Comparable from, boolean fromInclusive, Comparable to, boolean toInclusive) {
        takeReadLock();
        try {
            int order = SPECIAL_AWARE_COMPARATOR.compare(from, to);
            if (order == 0) {
                if (!fromInclusive || !toInclusive) {
                    return emptySet();
                }
                return toSingleResultSet(recordMap.get(from));
            } else if (order > 0) {
                return emptySet();
            }
            MultiResultSet results = createMultiResultSet();
            SortedMap> subMap =
                    recordMap.subMap(from, fromInclusive, to, toInclusive);
            for (Map value : subMap.values()) {
                copyToMultiResultSet(results, value);
            }
            return results;
        } finally {
            releaseReadLock();
        }
    }

    /**
     * Adds entry to the given index map without copying it.
     * Needs to be invoked in a thread-safe way.
     *
     * @see IndexCopyBehavior
     */
    private class AddFunctor implements IndexFunctor {

        @Override
        public Object invoke(Comparable value, QueryableEntry entry) {
            return recordMap.computeIfAbsent(value, x -> new ConcurrentSkipListMap<>(DATA_COMPARATOR)).put(entry.getKeyData(),
                    entry);
        }

    }

    /**
     * Adds entry to the given index map copying it to secure exclusive access.
     * Needs to be invoked in a thread-safe way.
     *
     * @see IndexCopyBehavior
     */
    private class CopyOnWriteAddFunctor implements IndexFunctor {

        @Override
        public Object invoke(Comparable value, QueryableEntry entry) {
            Object oldValue;
            NavigableMap records = recordMap.get(value);
            if (records == null) {
                records = new TreeMap<>(DATA_COMPARATOR);
            }

            records = new TreeMap<>(records);
            oldValue = records.put(entry.getKeyData(), entry);

            recordMap.put(value, records);
            return oldValue;
        }
    }

    /**
     * Removes entry from the given index map without copying it.
     * Needs to be invoked in a thread-safe way.
     *
     * @see IndexCopyBehavior
     */
    private class RemoveFunctor implements IndexFunctor {

        @Override
        public Object invoke(Comparable value, Data indexKey) {
            Object oldValue;
            Map records = recordMap.get(value);
            if (records != null) {
                oldValue = records.remove(indexKey);
                if (records.isEmpty()) {
                    recordMap.remove(value);
                }
            } else {
                oldValue = null;
            }

            return oldValue;
        }

    }

    /**
     * Removes entry from the given index map copying it to secure exclusive access.
     * Needs to be invoked in a thread-safe way.
     *
     * @see IndexCopyBehavior
     */
    private class CopyOnWriteRemoveFunctor implements IndexFunctor {

        @Override
        public Object invoke(Comparable value, Data indexKey) {
            Object oldValue;
            NavigableMap records = recordMap.get(value);
            if (records != null) {
                records = new TreeMap<>(records);
                oldValue = records.remove(indexKey);

                if (records.isEmpty()) {
                    recordMap.remove(value);
                } else {
                    recordMap.put(value, records);
                }
            } else {
                oldValue = null;
            }

            return oldValue;
        }

    }

    private static final class IteratorFromBatch implements Iterator {
        private final Iterator iterator;
        private Iterator indexKeyIterator;

        private IteratorFromBatch(@Nonnull Iterator iterator) {
            this.iterator = iterator;
            this.indexKeyIterator = iterator.hasNext() ? iterator.next().getEntries() : null;
        }

        @Override
        public boolean hasNext() {
            if (indexKeyIterator == null) {
                return false;
            }
            if (indexKeyIterator.hasNext()) {
                return true;
            } else {
                while (iterator.hasNext()) {
                    indexKeyIterator = iterator.next().getEntries();
                    if (indexKeyIterator.hasNext()) {
                        return true;
                    }
                }
                return false;
            }
        }

        @Override
        public QueryableEntry next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            return indexKeyIterator.next();
        }
    }

    private static class DataComparator implements Comparator {

        @Override
        public int compare(Data o1, Data o2) {
            byte[] thisBytes = o1.toByteArray();
            byte[] thatBytes = o2.toByteArray();
            return Arrays.compare(thisBytes, thatBytes);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy