/*
* Copyright (c) 2008-2017, 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.config.MapConfig;
import com.hazelcast.core.PartitioningStrategy;
import com.hazelcast.internal.serialization.InternalSerializationService;
import com.hazelcast.internal.serialization.impl.DefaultSerializationServiceBuilder;
import com.hazelcast.map.impl.record.AbstractRecord;
import com.hazelcast.map.impl.record.DataRecordFactory;
import com.hazelcast.map.impl.record.Record;
import com.hazelcast.map.impl.record.Records;
import com.hazelcast.nio.ObjectDataInput;
import com.hazelcast.nio.ObjectDataOutput;
import com.hazelcast.nio.serialization.Data;
import com.hazelcast.nio.serialization.DataSerializable;
import com.hazelcast.nio.serialization.Portable;
import com.hazelcast.nio.serialization.PortableFactory;
import com.hazelcast.nio.serialization.PortableReader;
import com.hazelcast.nio.serialization.PortableWriter;
import com.hazelcast.partition.strategy.DefaultPartitioningStrategy;
import com.hazelcast.query.QueryConstants;
import com.hazelcast.query.QueryException;
import com.hazelcast.query.impl.getters.Extractors;
import com.hazelcast.query.impl.getters.ReflectionHelper;
import com.hazelcast.query.impl.predicates.AndPredicate;
import com.hazelcast.query.impl.predicates.EqualPredicate;
import com.hazelcast.test.HazelcastSerialClassRunner;
import com.hazelcast.test.annotation.QuickTest;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static com.hazelcast.instance.TestUtil.toData;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@RunWith(HazelcastSerialClassRunner.class)
@Category(QuickTest.class)
public class IndexTest {
static final short FACTORY_ID = 1;
final InternalSerializationService ss = new DefaultSerializationServiceBuilder()
.addPortableFactory(FACTORY_ID, new TestPortableFactory()).build();
private PartitioningStrategy partitionStrategy = new DefaultPartitioningStrategy();
final DataRecordFactory recordFactory = new DataRecordFactory(new MapConfig(), ss, partitionStrategy);
@Test
public void testBasics() {
testIt(true);
testIt(false);
}
private QueryRecord newRecord(Object key, final Comparable attributeValue) {
return new QueryRecord(toData(key), attributeValue);
}
@Test
public void testRemoveEnumIndex() {
Indexes is = new Indexes(ss, Extractors.empty());
is.addOrGetIndex("favoriteCity", false);
Data key = ss.toData(1);
Data value = ss.toData(new SerializableWithEnum(SerializableWithEnum.City.ISTANBUL));
is.saveEntryIndex(new QueryEntry(ss, key, value, Extractors.empty()), null);
assertNotNull(is.getIndex("favoriteCity"));
Record record = recordFactory.newRecord(value);
((AbstractRecord) record).setKey(key);
is.removeEntryIndex(key, Records.getValueOrCachedValue(record, ss));
assertEquals(0, is.getIndex("favoriteCity").getRecords(SerializableWithEnum.City.ISTANBUL).size());
}
@Test
public void testUpdateEnumIndex() {
Indexes is = new Indexes(ss, Extractors.empty());
is.addOrGetIndex("favoriteCity", false);
Data key = ss.toData(1);
Data value = ss.toData(new SerializableWithEnum(SerializableWithEnum.City.ISTANBUL));
is.saveEntryIndex(new QueryEntry(ss, key, value, Extractors.empty()), null);
Data newValue = ss.toData(new SerializableWithEnum(SerializableWithEnum.City.KRAKOW));
is.saveEntryIndex(new QueryEntry(ss, key, newValue, Extractors.empty()), value);
assertEquals(0, is.getIndex("favoriteCity").getRecords(SerializableWithEnum.City.ISTANBUL).size());
assertEquals(1, is.getIndex("favoriteCity").getRecords(SerializableWithEnum.City.KRAKOW).size());
}
@Test
public void testIndex() throws QueryException {
Indexes is = new Indexes(ss, Extractors.empty());
Index dIndex = is.addOrGetIndex("d", false);
Index boolIndex = is.addOrGetIndex("bool", false);
Index strIndex = is.addOrGetIndex("str", false);
for (int i = 0; i < 1000; i++) {
Data key = ss.toData(i);
Data value = ss.toData(new MainPortable(i % 2 == 0, -10.34d, "joe" + i));
is.saveEntryIndex(new QueryEntry(ss, key, value, Extractors.empty()), null);
}
assertEquals(1000, dIndex.getRecords(-10.34d).size());
assertEquals(1, strIndex.getRecords("joe23").size());
assertEquals(500, boolIndex.getRecords(true).size());
clearIndexes(dIndex, boolIndex, strIndex);
for (int i = 0; i < 1000; i++) {
Data key = ss.toData(i);
Data value = ss.toData(new MainPortable(false, 11.34d, "joe"));
is.saveEntryIndex(new QueryEntry(ss, key, value, Extractors.empty()), null);
}
assertEquals(0, dIndex.getRecords(-10.34d).size());
assertEquals(0, strIndex.getRecords("joe23").size());
assertEquals(1000, strIndex.getRecords("joe").size());
assertEquals(1000, boolIndex.getRecords(false).size());
assertEquals(0, boolIndex.getRecords(true).size());
clearIndexes(dIndex, boolIndex, strIndex);
for (int i = 0; i < 1000; i++) {
Data key = ss.toData(i);
Data value = ss.toData(new MainPortable(false, -1 * (i + 1), "joe" + i));
is.saveEntryIndex(new QueryEntry(ss, key, value, Extractors.empty()), null);
}
assertEquals(0, dIndex.getSubRecordsBetween(1d, 1001d).size());
assertEquals(1000, dIndex.getSubRecordsBetween(-1d, -1001d).size());
clearIndexes(dIndex, boolIndex, strIndex);
for (int i = 0; i < 1000; i++) {
Data key = ss.toData(i);
Data value = ss.toData(new MainPortable(false, 1 * (i + 1), "joe" + i));
is.saveEntryIndex(new QueryEntry(ss, key, value, Extractors.empty()), null);
}
assertEquals(1000, dIndex.getSubRecordsBetween(1d, 1001d).size());
assertEquals(0, dIndex.getSubRecordsBetween(-1d, -1001d).size());
assertEquals(400, dIndex.getSubRecords(ComparisonType.GREATER, 600d).size());
assertEquals(401, dIndex.getSubRecords(ComparisonType.GREATER_EQUAL, 600d).size());
assertEquals(9, dIndex.getSubRecords(ComparisonType.LESSER, 10d).size());
assertEquals(10, dIndex.getSubRecords(ComparisonType.LESSER_EQUAL, 10d).size());
assertEquals(1, is.query(new AndPredicate(new EqualPredicate("d", 1d), new EqualPredicate("bool", "false"))).size());
assertEquals(1, is.query(new AndPredicate(new EqualPredicate("d", 1), new EqualPredicate("bool", Boolean.FALSE))).size());
assertEquals(1, is.query(new AndPredicate(new EqualPredicate("d", "1"), new EqualPredicate("bool", false))).size());
}
private void clearIndexes(Index... indexes) {
for (Index index : indexes) {
index.clear();
}
}
@Test
public void testIndexWithNull() throws QueryException {
Indexes is = new Indexes(ss, Extractors.empty());
Index strIndex = is.addOrGetIndex("str", true);
Data value = ss.toData(new MainPortable(false, 1, null));
Data key1 = ss.toData(0);
is.saveEntryIndex(new QueryEntry(ss, key1, value, Extractors.empty()), null);
value = ss.toData(new MainPortable(false, 2, null));
Data key2 = ss.toData(1);
is.saveEntryIndex(new QueryEntry(ss, key2, value, Extractors.empty()), null);
for (int i = 2; i < 1000; i++) {
Data key = ss.toData(i);
value = ss.toData(new MainPortable(false, 1 * (i + 1), "joe" + i));
is.saveEntryIndex(new QueryEntry(ss, key, value, Extractors.empty()), null);
}
assertEquals(2, strIndex.getRecords((Comparable) null).size());
assertEquals(998, strIndex.getSubRecords(ComparisonType.NOT_EQUAL, null).size());
}
private class TestPortableFactory implements PortableFactory {
public Portable create(int classId) {
switch (classId) {
case MainPortable.CLASS_ID:
return new MainPortable();
}
return null;
}
public int getFactoryId() {
return FACTORY_ID;
}
}
private static class SerializableWithEnum implements DataSerializable {
enum City {
ISTANBUL,
RIZE,
KRAKOW,
TRABZON
}
private City favoriteCity;
private SerializableWithEnum() {
}
private SerializableWithEnum(City favoriteCity) {
this.favoriteCity = favoriteCity;
}
@Override
public void writeData(ObjectDataOutput out) throws IOException {
out.writeObject(favoriteCity);
}
@Override
public void readData(ObjectDataInput in) throws IOException {
favoriteCity = in.readObject();
}
}
private static class MainPortable implements Portable {
public static final int CLASS_ID = 1;
byte b;
boolean bool;
char c;
short s;
int i;
long l;
float f;
double d;
String str;
private MainPortable() {
}
private MainPortable(boolean bool, double d, String str) {
this.bool = bool;
this.d = d;
this.str = str;
}
private MainPortable(byte b, boolean bool, char c, short s, int i, long l, float f, double d, String str) {
this.b = b;
this.bool = bool;
this.c = c;
this.s = s;
this.i = i;
this.l = l;
this.f = f;
this.d = d;
this.str = str;
}
public int getClassId() {
return CLASS_ID;
}
public void writePortable(PortableWriter writer) throws IOException {
writer.writeByte("b", b);
writer.writeBoolean("bool", bool);
writer.writeChar("c", c);
writer.writeShort("s", s);
writer.writeInt("i", i);
writer.writeLong("l", l);
writer.writeFloat("f", f);
writer.writeDouble("d", d);
writer.writeUTF("str", str);
}
public void readPortable(PortableReader reader) throws IOException {
b = reader.readByte("b");
bool = reader.readBoolean("bool");
c = reader.readChar("c");
s = reader.readShort("s");
i = reader.readInt("i");
l = reader.readLong("l");
f = reader.readFloat("f");
d = reader.readDouble("d");
str = reader.readUTF("str");
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MainPortable that = (MainPortable) o;
if (b != that.b) {
return false;
}
if (bool != that.bool) {
return false;
}
if (c != that.c) {
return false;
}
if (Double.compare(that.d, d) != 0) {
return false;
}
if (Float.compare(that.f, f) != 0) {
return false;
}
if (i != that.i) {
return false;
}
if (l != that.l) {
return false;
}
if (s != that.s) {
return false;
}
if (str != null ? !str.equals(that.str) : that.str != null) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result;
long temp;
result = (int) b;
result = 31 * result + (bool ? 1 : 0);
result = 31 * result + (int) c;
result = 31 * result + (int) s;
result = 31 * result + i;
result = 31 * result + (int) (l ^ (l >>> 32));
result = 31 * result + (f != +0.0f ? Float.floatToIntBits(f) : 0);
temp = d != +0.0d ? Double.doubleToLongBits(d) : 0L;
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + (str != null ? str.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "MainPortable{" +
"b=" + b +
", bool=" + bool +
", c=" + c +
", s=" + s +
", i=" + i +
", l=" + l +
", f=" + f +
", d=" + d +
", str='" + str + '\'' +
'}';
}
public int getFactoryId() {
return FACTORY_ID;
}
}
class QueryRecord extends QueryableEntry {
Data key;
Comparable attributeValue;
QueryRecord(Data key, Comparable attributeValue) {
this.key = key;
this.attributeValue = attributeValue;
}
public Comparable getAttributeValue(String attributeName) throws QueryException {
return attributeValue;
}
public AttributeType getAttributeType(String attributeName) {
return ReflectionHelper.getAttributeType(attributeValue.getClass());
}
@Override
public Object getTargetObject(boolean key) {
return key ? true : attributeValue;
}
public Data getKeyData() {
return key;
}
public Data getValueData() {
return null;
}
public long getCreationTime() {
return 0;
}
public long getLastAccessTime() {
return 0;
}
public Object getKey() {
return key;
}
public Object getValue() {
return attributeValue;
}
public Object setValue(Object value) {
return null;
}
public void changeAttribute(Comparable newAttributeValue) {
this.attributeValue = newAttributeValue;
}
public void writeData(ObjectDataOutput out) throws IOException {
}
public void readData(ObjectDataInput in) throws IOException {
}
public Record toRecord() {
Record<Data> record = recordFactory.newRecord(attributeValue);
((AbstractRecord) record).setKey(key);
return record;
}
}
private void testIt(boolean ordered) {
IndexImpl index = new IndexImpl(QueryConstants.THIS_ATTRIBUTE_NAME.value(), ordered, ss, Extractors.empty());
assertEquals(0, index.getRecords(0L).size());
assertEquals(0, index.getSubRecordsBetween(0L, 1000L).size());
QueryRecord record5 = newRecord(5L, 55L);
index.saveEntryIndex(record5, null);
assertEquals(Collections.<QueryableEntry>singleton(record5), index.getRecords(55L));
QueryRecord record6 = newRecord(6L, 66L);
index.saveEntryIndex(record6, null);
assertEquals(Collections.<QueryableEntry>singleton(record6), index.getRecords(66L));
QueryRecord newRecord5 = newRecord(5L, 555L);
index.saveEntryIndex(newRecord5, record5.getValue());
record5 = newRecord5;
assertEquals(0, index.getRecords(55L).size());
assertEquals(Collections.<QueryableEntry>singleton(record5), index.getRecords(555L));
assertEquals(1, index.getRecords(555L).size());
assertEquals(2, index.getSubRecordsBetween(55L, 555L).size());
assertEquals(2, index.getSubRecordsBetween(66L, 555L).size());
assertEquals(1, index.getSubRecordsBetween(555L, 555L).size());
QueryRecord record50 = newRecord(50L, 555L);
index.saveEntryIndex(record50, null);
assertEquals(new HashSet<QueryableEntry>(asList(record5, record50)), index.getRecords(555L));
Map<Data, QueryableEntry> records = getRecordMap(index, 555L);
assertNotNull(records);
assertEquals(2, records.size());
assertEquals(record5, records.get(record5.getKeyData()));
assertEquals(record50, records.get(record50.getKeyData()));
assertEquals(2, index.getRecords(555L).size());
assertEquals(3, index.getSubRecordsBetween(55L, 555L).size());
assertEquals(3, index.getSubRecordsBetween(66L, 555L).size());
assertEquals(2, index.getSubRecordsBetween(555L, 555L).size());
assertEquals(0, index.getSubRecords(ComparisonType.LESSER, 66L).size());
assertEquals(1, index.getSubRecords(ComparisonType.LESSER_EQUAL, 66L).size());
assertEquals(1, index.getSubRecords(ComparisonType.LESSER_EQUAL, 67L).size());
assertEquals(2, index.getSubRecords(ComparisonType.GREATER, 66L).size());
assertEquals(3, index.getSubRecords(ComparisonType.GREATER_EQUAL, 66L).size());
assertEquals(3, index.getSubRecords(ComparisonType.GREATER_EQUAL, 61L).size());
assertEquals(3, index.getSubRecords(ComparisonType.NOT_EQUAL, 61L).size());
assertEquals(2, index.getSubRecords(ComparisonType.NOT_EQUAL, 66L).size());
assertEquals(1, index.getSubRecords(ComparisonType.NOT_EQUAL, 555L).size());
assertEquals(3, index.getRecords(new Comparable[]{66L, 555L, 34234L}).size());
assertEquals(2, index.getRecords(new Comparable[]{555L, 34234L}).size());
Record recordToRemove = record5.toRecord();
index.removeEntryIndex(recordToRemove.getKey(), Records.getValueOrCachedValue(recordToRemove, ss));
assertEquals(Collections.<QueryableEntry>singleton(record50), index.getRecords(555L));
records = getRecordMap(index, 555L);
assertNotNull(records);
assertNull(records.get(5L));
assertEquals(record50, records.get(toData(50L)));
assertEquals(1, index.getRecords(555L).size());
assertEquals(2, index.getSubRecordsBetween(55L, 555L).size());
assertEquals(2, index.getSubRecordsBetween(66L, 555L).size());
assertEquals(1, index.getSubRecordsBetween(555L, 555L).size());
assertEquals(0, index.getSubRecords(ComparisonType.LESSER, 66L).size());
assertEquals(1, index.getSubRecords(ComparisonType.LESSER_EQUAL, 66L).size());
assertEquals(1, index.getSubRecords(ComparisonType.LESSER_EQUAL, 67L).size());
assertEquals(1, index.getSubRecords(ComparisonType.GREATER, 66L).size());
assertEquals(2, index.getSubRecords(ComparisonType.GREATER_EQUAL, 66L).size());
assertEquals(2, index.getSubRecords(ComparisonType.GREATER_EQUAL, 61L).size());
recordToRemove = record50.toRecord();
index.removeEntryIndex(recordToRemove.getKey(), Records.getValueOrCachedValue(recordToRemove, ss));
assertEquals(0, index.getRecords(555L).size());
records = getRecordMap(index, 555L);
assertNull(records);
assertEquals(0, index.getRecords(555L).size());
assertEquals(1, index.getSubRecordsBetween(55L, 555L).size());
assertEquals(1, index.getSubRecordsBetween(66L, 555L).size());
assertEquals(0, index.getSubRecordsBetween(555L, 555L).size());
recordToRemove = record6.toRecord();
index.removeEntryIndex(recordToRemove.getKey(), Records.getValueOrCachedValue(recordToRemove, ss));
assertEquals(0, index.getRecords(66L).size());
assertNull(getRecordMap(index, 66L));
assertEquals(0, index.getRecords(555L).size());
assertEquals(0, index.getSubRecordsBetween(55L, 555L).size());
assertEquals(0, index.getSubRecordsBetween(66L, 555L).size());
assertEquals(0, index.getSubRecordsBetween(555L, 555L).size());
}
private Map<Data, QueryableEntry> getRecordMap(IndexImpl index, Comparable indexValue) {
Set<QueryableEntry> records = index.getRecords(indexValue);
if (records.isEmpty()) {
return null;
}
Map<Data, QueryableEntry> recordMap = new HashMap<Data, QueryableEntry>(records.size());
for (QueryableEntry entry : records) {
recordMap.put(entry.getKeyData(), entry);
}
return recordMap;
}
}