/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.ignite.internal.processors.cache;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.configuration.Factory;
import javax.cache.expiry.Duration;
import javax.cache.expiry.TouchedExpiryPolicy;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteBinary;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.IgniteSystemProperties;
import org.apache.ignite.Ignition;
import org.apache.ignite.binary.BinaryObject;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.CachePeekMode;
import org.apache.ignite.cache.QueryEntity;
import org.apache.ignite.cache.QueryIndex;
import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
import org.apache.ignite.cache.query.QueryCursor;
import org.apache.ignite.cache.query.ScanQuery;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.cache.query.SqlQuery;
import org.apache.ignite.cache.query.TextQuery;
import org.apache.ignite.cache.query.annotations.QuerySqlField;
import org.apache.ignite.cache.query.annotations.QuerySqlFunction;
import org.apache.ignite.cache.query.annotations.QueryTextField;
import org.apache.ignite.cache.store.CacheStore;
import org.apache.ignite.cache.store.CacheStoreAdapter;
import org.apache.ignite.client.Config;
import org.apache.ignite.client.IgniteClient;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.ClientConfiguration;
import org.apache.ignite.configuration.DataStorageConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.configuration.NearCacheConfiguration;
import org.apache.ignite.events.CacheQueryExecutedEvent;
import org.apache.ignite.events.CacheQueryReadEvent;
import org.apache.ignite.events.Event;
import org.apache.ignite.events.EventType;
import org.apache.ignite.events.SqlQueryExecutionEvent;
import org.apache.ignite.internal.binary.BinaryMarshaller;
import org.apache.ignite.internal.processors.cache.query.QueryCursorEx;
import org.apache.ignite.internal.processors.query.GridQueryFieldMetadata;
import org.apache.ignite.internal.util.lang.GridPlainCallable;
import org.apache.ignite.internal.util.tostring.GridToStringBuilder;
import org.apache.ignite.internal.util.tostring.GridToStringExclude;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.G;
import org.apache.ignite.internal.util.typedef.internal.S;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteBiPredicate;
import org.apache.ignite.lang.IgnitePredicate;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.testframework.GridTestUtils;
import org.apache.ignite.testframework.junits.WithSystemProperty;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.junit.Test;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.apache.ignite.cache.CacheAtomicityMode.TRANSACTIONAL;
import static org.apache.ignite.cache.CacheMode.PARTITIONED;
import static org.apache.ignite.cache.CacheMode.REPLICATED;
import static org.apache.ignite.cache.CacheRebalanceMode.SYNC;
import static org.apache.ignite.cache.CacheWriteSynchronizationMode.FULL_SYNC;
import static org.apache.ignite.events.EventType.EVT_CACHE_QUERY_EXECUTED;
import static org.apache.ignite.events.EventType.EVT_CACHE_QUERY_OBJECT_READ;
import static org.apache.ignite.events.EventType.EVT_SQL_QUERY_EXECUTION;
import static org.apache.ignite.internal.processors.cache.query.CacheQueryType.FULL_TEXT;
import static org.apache.ignite.internal.processors.cache.query.CacheQueryType.SCAN;
import static org.apache.ignite.testframework.GridTestUtils.assertThrowsWithCause;
import static org.junit.Assert.assertArrayEquals;

/**
 * Various tests for cache queries.
 */
public abstract class IgniteCacheAbstractQuerySelfTest extends GridCommonAbstractTest {
    /** Key count. */
    private static final int KEY_CNT = 5000;

    /** Cache store. */
    private static TestStore store = new TestStore();

    /**
     * @return Grid count.
     */
    protected abstract int gridCount();

    /**
     * @return Cache mode.
     */
    protected abstract CacheMode cacheMode();

    /**
     * @return Atomicity mode.
     */
    protected CacheAtomicityMode atomicityMode() {
        return TRANSACTIONAL;
    }

    /**
     * @return Distribution.
     */
    protected NearCacheConfiguration nearCacheConfiguration() {
        return new NearCacheConfiguration();
    }

    /** {@inheritDoc} */
    @SuppressWarnings("unchecked")
    @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
        IgniteConfiguration c = super.getConfiguration(igniteInstanceName);

        ((TcpDiscoverySpi)c.getDiscoverySpi()).setForceServerMode(true);

        if (igniteInstanceName.startsWith("client"))
            c.setDataStorageConfiguration(new DataStorageConfiguration());

        c.setIncludeEventTypes(EventType.EVTS_ALL);

        return c;
    }

    /**
     * @return cache configuration
     */
    protected CacheConfiguration cacheConfiguration() {
        CacheConfiguration cc = defaultCacheConfiguration();

        cc.setCacheMode(cacheMode());
        cc.setAtomicityMode(atomicityMode());
        cc.setNearConfiguration(nearCacheConfiguration());
        cc.setWriteSynchronizationMode(FULL_SYNC);
        cc.setCacheStoreFactory(new StoreFactory());
        cc.setReadThrough(true);
        cc.setWriteThrough(true);
        cc.setLoadPreviousValue(true);
        cc.setRebalanceMode(SYNC);
        cc.setSqlFunctionClasses(SqlFunctions.class);

        List<QueryEntity> entityList = new ArrayList<>();

        QueryEntity qryEntity = new QueryEntity();

        qryEntity.setKeyType(Integer.class.getName());
        qryEntity.setValueType(Type1.class.getName());
        qryEntity.addQueryField("id", Integer.class.getName(), null);
        qryEntity.addQueryField("name", String.class.getName(), null);
        qryEntity.setTableName("Type2");
        qryEntity.setIndexes(Arrays.asList(new QueryIndex("id")
            .setName("index~!@#$%^&*()_=-+;[]{}|?,.*`:nameWithNonLetterSymbols")));

        entityList.add(qryEntity);

        qryEntity = new QueryEntity();

        qryEntity.setKeyType(Integer.class.getName());
        qryEntity.setValueType(Type2.class.getName());
        qryEntity.addQueryField("id", Integer.class.getName(), null);
        qryEntity.addQueryField("name", String.class.getName(), null);
        qryEntity.setTableName("Type1");
        qryEntity.setIndexes(Arrays.asList(new QueryIndex("id")));

        entityList.add(qryEntity);

        qryEntity = new QueryEntity();

        qryEntity.setKeyType(Integer.class.getName());
        qryEntity.setValueType(ObjectValue2.class.getName());

        qryEntity.addQueryField("strVal", String.class.getName(), null);

        final QueryIndex strIdx = new QueryIndex(); // Default index type

        strIdx.setFieldNames(Collections.singletonList("strVal"), true);

        qryEntity.setIndexes(Arrays.asList(strIdx));

        entityList.add(qryEntity);

        cc.setQueryEntities(entityList);

        if (cacheMode() != CacheMode.LOCAL)
            cc.setAffinity(new RendezvousAffinityFunction());

        // Explicitly set number of backups equal to number of grids.
        if (cacheMode() == CacheMode.PARTITIONED)
            cc.setBackups(gridCount());

        //cc.setSnapshotableIndex(snapshotableIndex());

        return cc;
    }

    /**
     * @param clsK Key class.
     * @param clsV Value class.
     *
     * @return cache instance
     */
    protected <K, V> IgniteCache<K, V> jcache(Class<K> clsK, Class<V> clsV) {
        return jcache(ignite(), clsK, clsV);
    }

    /**
     * @param ig Ignite.
     * @param clsK Key class.
     * @param clsV Value class.
     *
     * @return cache instance
     */
    protected <K, V> IgniteCache<K, V> jcache(Ignite ig, Class<K> clsK, Class<V> clsV) {
        IgniteCache<K, V> cache = jcache(ig, cacheConfiguration(), clsK, clsV);

        return cache;
    }

    /**
     * @return {@code True} if index snapshot is enabled.
     */
    protected boolean snapshotableIndex() {
        return false;
    }

    /**
     * @return Ignite instance.
     */
    protected Ignite ignite() {
        return grid(0);
    }

    /** {@inheritDoc} */
    @Override protected void afterTest() throws Exception {
        super.afterTest();

        for (String cacheName : ignite().cacheNames())
            ignite().cache(cacheName).destroy();
    }

    /** {@inheritDoc} */
    @Override protected void beforeTestsStarted() throws Exception {
        super.beforeTestsStarted();

        startGridsMultiThreaded(gridCount());
    }

    /** {@inheritDoc} */
    @Override protected void afterTestsStopped() throws Exception {
        store.reset();
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testDifferentKeyTypes() throws Exception {
        final IgniteCache<Object, Object> cache = jcache(Object.class, Object.class);

        cache.put(1, "value");

        cache.put("key", "value");
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testDifferentValueTypes() throws Exception {
        IgniteCache<Integer, Object> cache = jcache(Integer.class, Object.class);

        cache.put(7, "value");

        // Put value of different type but for the same key type.
        // Operation should succeed but with warning log message.
        cache.put(7, 1);
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testStringType() throws Exception {
        IgniteCache<Integer, String> cache = jcache(Integer.class, String.class);

        cache.put(666, "test");

        QueryCursor<Cache.Entry<Integer, String>> qry =
            cache.query(new SqlQuery<Integer, String>(String.class, "_val='test'"));

        Cache.Entry<Integer, String> entry = F.first(qry.getAll());

        assert entry != null;
        assertEquals(666, entry.getKey().intValue());
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testIntegerType() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        int key = 898;

        int val = 2;

        cache.put(key, val);

        QueryCursor<Cache.Entry<Integer, Integer>> qry =
            cache.query(new SqlQuery<Integer, Integer>(Integer.class, "_key = ? and _val > 1").setArgs(key));

        Cache.Entry<Integer, Integer> entry = F.first(qry.getAll());

        assert entry != null;

        assertEquals(key, entry.getKey().intValue());
        assertEquals(val, entry.getValue().intValue());
    }

    /**
     * Test table alias in SqlQuery.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testTableAliasInSqlQuery() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        int key = 898;

        int val = 2;

        cache.put(key, val);

        SqlQuery<Integer, Integer> sqlQry = new SqlQuery<>(Integer.class, "t1._key = ? and t1._val > 1");

        QueryCursor<Cache.Entry<Integer, Integer>> qry = cache.query(sqlQry.setAlias("t1").setArgs(key));

        Cache.Entry<Integer, Integer> entry = F.first(qry.getAll());

        assert entry != null;

        assertEquals(key, entry.getKey().intValue());
        assertEquals(val, entry.getValue().intValue());

        sqlQry = new SqlQuery<>(Integer.class, "FROM Integer as t1 WHERE t1._key = ? and t1._val > 1");

        qry = cache.query(sqlQry.setAlias("t1").setArgs(key));

        entry = F.first(qry.getAll());

        assert entry != null;

        assertEquals(key, entry.getKey().intValue());
        assertEquals(val, entry.getValue().intValue());
    }

    /**
     * Tests UDFs.
     *
     * @throws IgniteCheckedException If failed.
     */
    @Test
    public void testUserDefinedFunction() throws IgniteCheckedException {
        // Without alias.
        final IgniteCache<Object, Object> cache = jcache(Object.class, Object.class);

        QueryCursor<List<?>> qry = cache.query(new SqlFieldsQuery("select square(1), square(2)"));

        Collection<List<?>> res = qry.getAll();

        assertEquals(1, res.size());

        List<?> row = res.iterator().next();

        assertEquals(1, row.get(0));
        assertEquals(4, row.get(1));

        // With alias.
        qry = cache.query(new SqlFieldsQuery("select _cube_(1), _cube_(2)"));

        res = qry.getAll();

        assertEquals(1, res.size());

        row = res.iterator().next();

        assertEquals(1, row.get(0));
        assertEquals(8, row.get(1));

        // Not registered.
        GridTestUtils.assertThrows(
            log,
            new Callable<Object>() {
                @Override public Object call() throws Exception {
                    cache.query(new SqlFieldsQuery("select no()"));

                    return null;
                }
            },
            CacheException.class,
            null
        );
    }

    /**
     * Expired entries are not included to result.
     *
     * @throws Exception If failed.
     */
    @Test
    public void testExpiration() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        cache.withExpiryPolicy(new TouchedExpiryPolicy(new Duration(MILLISECONDS, 1000))).put(7, 1);

        List<Cache.Entry<Integer, Integer>> qry =
            cache.query(new SqlQuery<Integer, Integer>(Integer.class, "1=1")).getAll();

        Cache.Entry<Integer, Integer> res = F.first(qry);

        assertEquals(1, res.getValue().intValue());

        U.sleep(300); // Less than minimal amount of time that must pass before a cache entry is considered expired.

        qry = cache.query(new SqlQuery<Integer, Integer>(Integer.class, "1=1")).getAll();

        res = F.first(qry);

        assertEquals(1, res.getValue().intValue());

        U.sleep(1800); // No expiry guarantee here. Test should be refactored in case of fails.

        qry = cache.query(new SqlQuery<Integer, Integer>(Integer.class, "1=1")).getAll();

        res = F.first(qry);

        assertNull(res);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testIllegalBounds() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        cache.put(1, 1);
        cache.put(2, 2);

        QueryCursor<Cache.Entry<Integer, Integer>> qry =
            cache.query(new SqlQuery<Integer, Integer>(Integer.class, "_key between 2 and 1"));

        assertTrue(qry.getAll().isEmpty());
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testComplexType() throws Exception {
        IgniteCache<Key, GridCacheQueryTestValue> cache = jcache(Key.class, GridCacheQueryTestValue.class);

        GridCacheQueryTestValue val1 = new GridCacheQueryTestValue();

        val1.setField1("field1");
        val1.setField2(1);
        val1.setField3(1L);

        GridCacheQueryTestValue val2 = new GridCacheQueryTestValue();

        val2.setField1("field2");
        val2.setField2(2);
        val2.setField3(2L);
        val2.setField6(null);

        cache.put(new Key(100500), val1);
        cache.put(new Key(100501), val2);

        QueryCursor<Cache.Entry<Key, GridCacheQueryTestValue>> qry = cache
            .query(new SqlQuery<Key, GridCacheQueryTestValue>(GridCacheQueryTestValue.class,
                "fieldName='field1' and field2=1 and field3=1 and id=100500 and embeddedField2=11 and x=3"));

        Cache.Entry<Key, GridCacheQueryTestValue> entry = F.first(qry.getAll());

        assertNotNull(entry);
        assertEquals(100500, entry.getKey().id);
        assertEquals(val1, entry.getValue());
    }

    /**
     * @throws Exception In case of error.
     */
    @Test
    public void testComplexTypeKeepBinary() throws Exception {
        if (ignite().configuration().getMarshaller() == null || ignite().configuration().getMarshaller() instanceof BinaryMarshaller) {
            IgniteCache<Key, GridCacheQueryTestValue> cache = jcache(Key.class, GridCacheQueryTestValue.class);

            GridCacheQueryTestValue val1 = new GridCacheQueryTestValue();

            val1.setField1("field1");
            val1.setField2(1);
            val1.setField3(1L);

            GridCacheQueryTestValue val2 = new GridCacheQueryTestValue();

            val2.setField1("field2");
            val2.setField2(2);
            val2.setField3(2L);
            val2.setField6(null);

            cache.put(new Key(100500), val1);
            cache.put(new Key(100501), val2);

            QueryCursor<Cache.Entry<BinaryObject, BinaryObject>> qry = cache.withKeepBinary()
                .query(new SqlQuery<BinaryObject, BinaryObject>(GridCacheQueryTestValue.class,
                    "fieldName='field1' and field2=1 and field3=1 and id=100500 and embeddedField2=11 and x=3"));

            Cache.Entry<BinaryObject, BinaryObject> entry = F.first(qry.getAll());

            assertNotNull(entry);
            assertEquals(Long.valueOf(100500L), entry.getKey().field("id"));
            assertEquals(val1, entry.getValue().deserialize());
        }
    }

    /**
     * Complex key type.
     */
    private static class Key {
        /** */
        @QuerySqlField
        private final long id;

        /**
         * @param id Id.
         */
        private Key(long id) {
            this.id = id;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object o) {
            if (this == o)
                return true;

            if (o == null || getClass() != o.getClass())
                return false;

            Key key = (Key)o;

            return id == key.id;

        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return (int)(id ^ (id >>> 32));
        }
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testSelectQuery() throws Exception {
        IgniteCache<Integer, String> cache = jcache(Integer.class, String.class);

        cache.put(10, "value");

        QueryCursor<Cache.Entry<Integer, String>> qry =
            cache.query(new SqlQuery<Integer, String>(String.class, "true"));

        Iterator<Cache.Entry<Integer, String>> iter = qry.iterator();

        assert iter != null;
        assert iter.next() != null;
    }

    /**
     * JUnit.
     */
    @Test
    public void testSimpleCustomTableName() {
        CacheConfiguration<Integer, Object> cacheConf = new CacheConfiguration<Integer, Object>(cacheConfiguration())
            .setName(DEFAULT_CACHE_NAME)
            .setQueryEntities(Arrays.asList(
                new QueryEntity(Integer.class, Type1.class),
                new QueryEntity(Integer.class, Type2.class)
            ));

        final IgniteCache<Integer, Object> cache = ignite().getOrCreateCache(cacheConf);

        cache.put(10, new Type1(1, "Type1 record #1"));
        cache.put(20, new Type1(2, "Type1 record #2"));

        QueryCursor<Cache.Entry<Integer, Type1>> qry1 =
            cache.query(new SqlQuery<Integer, Type1>(Type1.class, "FROM Type2"));

        List<Cache.Entry<Integer, Type1>> all = qry1.getAll();

        assertEquals(2, all.size());

        QueryCursor<List<?>> qry = cache.query(new SqlFieldsQuery("SELECT name FROM Type2"));

        assertEquals(2, qry.getAll().size());

        GridTestUtils.assertThrows(log, new GridPlainCallable<Void>() {
            @Override public Void call() throws Exception {
                QueryCursor<Cache.Entry<Integer, Type1>> qry =
                    cache.query(new SqlQuery<Integer, Type1>(Type1.class, "FROM Type1"));

                qry.getAll();

                return null;
            }
        }, CacheException.class, null);
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testMixedCustomTableName() throws Exception {
        final IgniteCache<Integer, Object> cache = jcache(Integer.class, Object.class);

        cache.put(10, new Type1(1, "Type1 record #1"));
        cache.put(20, new Type1(2, "Type1 record #2"));
        cache.put(30, new Type2(1, "Type2 record #1"));
        cache.put(40, new Type2(2, "Type2 record #2"));
        cache.put(50, new Type2(3, "Type2 record #3"));

        QueryCursor<Cache.Entry<Integer, Type1>> qry1 =
            cache.query(new SqlQuery<Integer, Type1>(Type1.class, "FROM Type2"));

        List<Cache.Entry<Integer, Type1>> all = qry1.getAll();

        assertEquals(2, all.size());

        QueryCursor<Cache.Entry<Integer, Type2>> qry2 =
            cache.query(new SqlQuery<Integer, Type2>(Type2.class, "FROM Type1"));

        assertEquals(3, qry2.getAll().size());

        QueryCursor<List<?>> qry = cache.query(new SqlFieldsQuery("SELECT name FROM Type1"));

        assertEquals(3, qry.getAll().size());

        qry = cache.query(new SqlFieldsQuery("SELECT name FROM Type2"));

        assertEquals(2, qry.getAll().size());

        GridTestUtils.assertThrows(log, new GridPlainCallable<Void>() {
            @Override public Void call() throws Exception {
                QueryCursor<Cache.Entry<Integer, Type1>> qry1 =
                    cache.query(new SqlQuery<Integer, Type1>(Type1.class, "FROM Type1"));

                qry1.getAll().size();

                return null;
            }
        }, CacheException.class, null);
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testDistributedJoinCustomTableName() throws Exception {
        IgniteCache<Integer, Object> cache = jcache(Integer.class, Object.class);

        cache.put(10, new Type1(1, "Type1 record #1"));
        cache.put(20, new Type1(2, "Type1 record #2"));
        cache.put(30, new Type2(1, "Type2 record #1"));
        cache.put(40, new Type2(2, "Type2 record #2"));
        cache.put(50, new Type2(3, "Type2 record #3"));

        QueryCursor<List<?>> query = cache.query(
            new SqlFieldsQuery("SELECT t2.name, t1.name FROM Type2 as t2 LEFT JOIN Type1 as t1 ON t1.id = t2.id")
                .setDistributedJoins(cacheMode() == PARTITIONED));

        assertEquals(2, query.getAll().size());

        query = cache.query(
            new SqlFieldsQuery("SELECT t2.name, t1.name FROM Type2 as t2 RIGHT JOIN Type1 as t1 ON t1.id = t2.id")
                .setDistributedJoins(cacheMode() == PARTITIONED));

        assertEquals(3, query.getAll().size());
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testObjectQuery() throws Exception {
        IgniteCache<Integer, ObjectValue> cache = jcache(Integer.class, ObjectValue.class);

        ObjectValue val = new ObjectValue("test", 0);

        cache.put(1, val);

        QueryCursor<Cache.Entry<Integer, ObjectValue>> qry =
            cache.query(new SqlQuery<Integer, ObjectValue>(ObjectValue.class, "_val=?").setArgs(val));

        Iterator<Cache.Entry<Integer, ObjectValue>> iter = qry.iterator();

        assert iter != null;

        int expCnt = 1;

        for (int i = 0; i < expCnt; i++)
            assert iter.next() != null;

        assert !iter.hasNext();

        qry = cache.query(new TextQuery<Integer, ObjectValue>(ObjectValue.class, "test"));

        iter = qry.iterator();

        assert iter != null;

        for (int i = 0; i < expCnt; i++)
            assert iter.next() != null;

        assert !iter.hasNext();
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testObjectWithString() throws Exception {
        IgniteCache<Integer, ObjectValue2> cache = jcache(Integer.class, ObjectValue2.class);

        cache.put(1, new ObjectValue2("value 1"));
        cache.put(2, new ObjectValue2("value 2"));
        cache.put(3, new ObjectValue2("value 3"));

        QueryCursor<Cache.Entry<Integer, ObjectValue2>> qry
            = cache.query(new SqlQuery<Integer, ObjectValue2>(ObjectValue2.class, "strVal like ?").setArgs("value%"));

        int expCnt = 3;

        List<Cache.Entry<Integer, ObjectValue2>> results = qry.getAll();

        assertEquals(expCnt, results.size());

        qry = cache.query(new SqlQuery<Integer, ObjectValue2>(ObjectValue2.class, "strVal > ?").setArgs("value 1"));

        results = qry.getAll();

        assertEquals(expCnt - 1, results.size());

        qry = cache.query(new TextQuery<Integer, ObjectValue2>(ObjectValue2.class, "value"));

        results = qry.getAll();

        assertEquals(0, results.size()); //Should fail for FULL_TEXT index, but SORTED
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testEnumObjectQuery() throws Exception {
        final IgniteCache<Long, EnumObject> cache = jcache(Long.class, EnumObject.class);

        for (long i = 0; i < 50; i++)
            cache.put(i, new EnumObject(i, i % 2 == 0 ? EnumType.TYPE_A : EnumType.TYPE_B));

        assertEnumQry("type = ?", EnumType.TYPE_A, EnumType.TYPE_A, cache, 25);
        assertEnumQry("type > ?", EnumType.TYPE_A, EnumType.TYPE_B, cache, 25);
        assertEnumQry("type < ?", EnumType.TYPE_B, EnumType.TYPE_A, cache, 25);
        assertEnumQry("type != ?", EnumType.TYPE_B, EnumType.TYPE_A, cache, 25);

        assertEmptyEnumQry("type = ?", null, cache);
        assertEmptyEnumQry("type > ?", EnumType.TYPE_B, cache);
        assertEmptyEnumQry("type < ?", EnumType.TYPE_A, cache);

        cache.put(50L, new EnumObject(50, null));

        assertNoArgEnumQry("type is null", null, cache, 1);
        assertAnyResTypeEnumQry("type is not null", cache, 50);

        // Additional tests for binary enums.
        IgniteBinary binary = ignite().binary();

        if (binary != null) {
            assertEnumQry("type = ?", binaryEnum(binary, EnumType.TYPE_A), EnumType.TYPE_A, cache, 25);
            assertEnumQry("type > ?", binaryEnum(binary, EnumType.TYPE_A), EnumType.TYPE_B, cache, 25);
            assertEnumQry("type < ?", binaryEnum(binary, EnumType.TYPE_B), EnumType.TYPE_A, cache, 25);
            assertEnumQry("type != ?", binaryEnum(binary, EnumType.TYPE_B), EnumType.TYPE_A, cache, 25);

            assertEmptyEnumQry("type > ?", binaryEnum(binary, EnumType.TYPE_B), cache);
            assertEmptyEnumQry("type < ?", binaryEnum(binary, EnumType.TYPE_A), cache);
        }
    }

    /**
     * Create binary enum.
     *
     * @param binary Binary facade.
     * @param val Enum value.
     * @return Binary enum.
     */
    private static BinaryObject binaryEnum(IgniteBinary binary, EnumType val) {
        return binary.buildEnum(EnumType.class.getName(), val.ordinal());
    }

    /**
     * Fails if result size not equals to resSize.
     *
     * @param qryStr to execute.
     * @param cache cache.
     * @param resSize size of the result.
     */
    private void assertAnyResTypeEnumQry(String qryStr, IgniteCache<Long, EnumObject> cache, int resSize) {
        final SqlQuery<Long, EnumObject> qry = new SqlQuery<>(EnumObject.class, qryStr);

        final List<Cache.Entry<Long, EnumObject>> res = cache.query(qry).getAll();

        assert resSize == res.size();
    }

    /**
     * Fails if result size not equals to resSize or
     * at least one entry has different of resType type.
     *
     * @param qryStr to execute.
     * @param resType to compare with.
     * @param cache cache.
     * @param resSize size of the result.
     */
    private void assertNoArgEnumQry(String qryStr, EnumType resType, IgniteCache<Long, EnumObject> cache, int resSize) {
        final SqlQuery<Long, EnumObject> qry = new SqlQuery<>(EnumObject.class, qryStr);

        final List<Cache.Entry<Long, EnumObject>> res = cache.query(qry).getAll();

        assert resSize == res.size();

        assertEnumType(res, resType);
    }

    /**
     * Fails if result size not equals to resSize or
     * at least one entry has different of resType type.
     *
     * @param qryStr to execute.
     * @param arg to be passed to query.
     * @param resType to compare with.
     * @param cache cache.
     * @param resSize size of the result.
     */
    private void assertEnumQry(String qryStr, Object arg, EnumType resType, IgniteCache<Long, EnumObject> cache,
        int resSize) {
        final SqlQuery<Long, EnumObject> qry = new SqlQuery<>(EnumObject.class, qryStr);

        qry.setArgs(arg);

        final List<Cache.Entry<Long, EnumObject>> res = cache.query(qry).getAll();

        assert resSize == res.size();

        assertEnumType(res, resType);
    }

    /**
     * Fails if result has entries.
     *
     * @param qryStr to execute.
     * @param arg argument that will be passed to query.
     * @param cache cache on which query will be executed.
     */
    private void assertEmptyEnumQry(String qryStr, Object arg, IgniteCache<Long, EnumObject> cache) {
        final SqlQuery<Long, EnumObject> qry = new SqlQuery<>(EnumObject.class, qryStr);

        qry.setArgs(arg);

        final List<Cache.Entry<Long, EnumObject>> res = cache.query(qry).getAll();

        assert res.isEmpty();
    }

    /**
     * Fails if at least one object in result has type field that doesn't
     * equal to passed enumType.
     *
     * @param enumObjects query execution result.
     * @param enumType compare to.
     */
    private void assertEnumType(final List<Cache.Entry<Long, EnumObject>> enumObjects, final EnumType enumType) {
        for (final Cache.Entry<Long, EnumObject> entry : enumObjects)
            assert entry.getValue().type == enumType;
    }

    /**
     * JUnit.
     */
    @Test
    public void testObjectQueryWithSwap() {
        CacheConfiguration<Integer, ObjectValue> config = new CacheConfiguration<Integer, ObjectValue>(cacheConfiguration());

        config.setOnheapCacheEnabled(true);

        IgniteCache<Integer, ObjectValue> cache = jcache(ignite(), config, Integer.class, ObjectValue.class);

        boolean partitioned = cache.getConfiguration(CacheConfiguration.class).getCacheMode() == PARTITIONED;

        int cnt = 10;

        for (int i = 0; i < cnt; i++)
            cache.put(i, new ObjectValue("test" + i, i));

        for (Ignite g : G.allGrids()) {
            IgniteCache<Integer, ObjectValue> c = g.cache(cache.getName());

            for (int i = 0; i < cnt; i++) {
                assertNotNull(c.localPeek(i, CachePeekMode.ONHEAP));

                c.localEvict(Collections.singleton(i)); // Swap.

                if (!partitioned || g.affinity(cache.getName()).mapKeyToNode(i).isLocal()) {
                    ObjectValue peekVal = c.localPeek(i, CachePeekMode.ONHEAP);

                    assertNull("Non-null value for peek [key=" + i + ", val=" + peekVal + ']', peekVal);
                }
            }
        }

        QueryCursor<Cache.Entry<Integer, ObjectValue>> qry =
            cache.query(new SqlQuery<Integer, ObjectValue>(ObjectValue.class, "intVal >= ? order by intVal").
                setArgs(0));

        Iterator<Cache.Entry<Integer, ObjectValue>> iter = qry.iterator();

        assert iter != null;

        Collection<Integer> set = new HashSet<>(cnt);

        Cache.Entry<Integer, ObjectValue> next;

        while (iter.hasNext()) {
            next = iter.next();

            ObjectValue v = next.getValue();

            assert !set.contains(v.intValue());

            set.add(v.intValue());
        }

        assert !iter.hasNext();

        assertEquals(cnt, set.size());

        for (int i = 0; i < cnt; i++)
            assert set.contains(i);

        qry = cache.query(new SqlQuery<Integer, ObjectValue>(ObjectValue.class, "MOD(intVal, 2) = ? order by intVal").
            setArgs(0));

        iter = qry.iterator();

        assert iter != null;

        set.clear();

        while (iter.hasNext()) {
            next = iter.next();

            ObjectValue v = next.getValue();

            assert !set.contains(v.intValue());

            set.add(v.intValue());
        }

        assert !iter.hasNext();

        assertEquals(cnt / 2, set.size());

        for (int i = 0; i < cnt; i++)
            if (i % 2 == 0)
                assert set.contains(i);
            else
                assert !set.contains(i);
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testFullTextSearch() throws Exception {
        IgniteCache<Integer, ObjectValue> cache = jcache(Integer.class, ObjectValue.class);

        // Try to execute on empty cache first.
        QueryCursor<Cache.Entry<Integer, ObjectValue>> qry =
            cache.query(new TextQuery<Integer, ObjectValue>(ObjectValue.class, "full"));

        assert qry.getAll().isEmpty();

        qry = cache.query(new TextQuery<Integer, ObjectValue>(ObjectValue.class, "full"));

        assert qry.getAll().isEmpty();

        // Now put indexed values into cache.
        int key1 = 1;

        ObjectValue val1 = new ObjectValue("test full text", 0);

        cache.put(key1, val1);

        int key2 = 2;

        ObjectValue val2 = new ObjectValue("test full text more", 0);

        cache.put(key2, val2);

        qry = cache.query(new TextQuery<Integer, ObjectValue>(ObjectValue.class, "full"));

        Collection<Cache.Entry<Integer, ObjectValue>> res = qry.getAll();

        assertNotNull(res);
        assertEquals(2, res.size());

        qry = cache.query(new TextQuery<Integer, ObjectValue>(ObjectValue.class, "full"));

        res = qry.getAll();

        assertNotNull(res);
        assertEquals(2, res.size());
    }

    /**
     * JUnit.
     *
     * @throws Exception In case of error.
     */
    @Test
    public void testScanQuery() throws Exception {
        IgniteCache<Integer, String> c1 = jcache(Integer.class, String.class);

        Map<Integer, String> map = new HashMap<Integer, String>() {{
            for (int i = 0; i < 5000; i++)
                put(i, "str" + i);
        }};

        for (Map.Entry<Integer, String> e : map.entrySet())
            c1.put(e.getKey(), e.getValue());

        // Scan query.
        QueryCursor<Cache.Entry<Integer, String>> qry = c1.query(new ScanQuery<Integer, String>());

        Iterator<Cache.Entry<Integer, String>> iter = qry.iterator();

        assert iter != null;

        int cnt = 0;

        while (iter.hasNext()) {
            Cache.Entry<Integer, String> e = iter.next();

            String expVal = map.get(e.getKey());

            assertNotNull(expVal);

            assertEquals(expVal, e.getValue());

            cnt++;
        }

        assertEquals(map.size(), cnt);
    }

    /**
     * @throws Exception In case of error.
     */
    @Test
    public void testScanPartitionQuery() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        GridCacheContext cctx = ((IgniteCacheProxy)cache).context();

        Map<Integer, Map<Integer, Integer>> entries = new HashMap<>();

        for (int i = 0; i < KEY_CNT; i++) {
            cache.put(i, i);

            int part = cctx.affinity().partition(i);

            Map<Integer, Integer> partEntries = entries.get(part);

            if (partEntries == null)
                entries.put(part, partEntries = new HashMap<>());

            partEntries.put(i, i);
        }

        for (int i = 0; i < cctx.affinity().partitions(); i++) {
            ScanQuery<Integer, Integer> scan = new ScanQuery<>(i);

            Collection<Cache.Entry<Integer, Integer>> actual = cache.query(scan).getAll();

            Map<Integer, Integer> exp = entries.get(i);

            int size = exp == null ? 0 : exp.size();

            assertEquals("Failed for partition: " + i, size, actual.size());

            if (exp == null)
                assertTrue(actual.isEmpty());
            else
                for (Cache.Entry<Integer, Integer> entry : actual)
                    assertTrue(entry.getValue().equals(exp.get(entry.getKey())));
        }
    }

    /**
     * JUnit.
     */
    @Test
    public void testTwoObjectsTextSearch() {
        CacheConfiguration<Object, Object> conf = new CacheConfiguration<>(cacheConfiguration());

        conf.setQueryEntities(Arrays.asList(
           new QueryEntity(Integer.class, ObjectValue.class),
           new QueryEntity(String.class, ObjectValueOther.class)
        ));

        IgniteCache<Object, Object> c = jcache(ignite(), conf, Object.class, Object.class);

        c.put(1, new ObjectValue("ObjectValue str", 1));
        c.put("key", new ObjectValueOther("ObjectValueOther str"));

        Collection<Cache.Entry<Object, Object>> res = c.query(new TextQuery<>(ObjectValue.class, "str")).getAll();

        assert res != null;
        int expCnt = 1;
        assert res.size() == expCnt;
        assert F.first(res).getValue().getClass() == ObjectValue.class;

        res = c.query(new TextQuery<>(ObjectValueOther.class, "str")).getAll();

        assert res != null;
        assert res.size() == expCnt;
        assert F.first(res).getValue().getClass() == ObjectValueOther.class;
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testPrimitiveType() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);
        cache.put(1, 1);
        cache.put(2, 2);

        QueryCursor<Cache.Entry<Integer, Integer>> q =
            cache.query(new SqlQuery<Integer, Integer>(Integer.class, "_val > 1"));

        Collection<Cache.Entry<Integer, Integer>> res = q.getAll();

        assertEquals(1, res.size());

        for (Cache.Entry<Integer, Integer> e : res) {
            assertEquals(2, (int)e.getKey());
            assertEquals(2, (int)e.getValue());
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testPaginationIteratorDefaultCache() throws Exception {
        testPaginationIterator(jcache(ignite(), cacheConfiguration(), DEFAULT_CACHE_NAME, Integer.class, Integer.class));
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testPaginationIteratorNamedCache() throws Exception {
        testPaginationIterator(jcache(ignite(), cacheConfiguration(), Integer.class, Integer.class));
    }

    /**
     * @param cache Ignite cache.
     * @throws Exception If failed.
     */
    private void testPaginationIterator(IgniteCache<Integer, Integer> cache) throws Exception {
        for (int i = 0; i < 50; i++)
            cache.put(i, i);

        SqlQuery<Integer, Integer> qry = new SqlQuery<>(Integer.class, "_key >= 0");

        qry.setPageSize(10);

        QueryCursor<Cache.Entry<Integer, Integer>> q = cache.query(qry);

        int cnt = 0;

        for (Cache.Entry<Integer, Integer> e : q) {
            assertTrue(e.getKey() >= 0 && e.getKey() < 50);
            assertTrue(e.getValue() >= 0 && e.getValue() < 50);

            cnt++;
        }

        assertEquals(50, cnt);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testPaginationGetDefaultCache() throws Exception {
        testPaginationGet(jcache(ignite(), cacheConfiguration(), DEFAULT_CACHE_NAME, Integer.class, Integer.class));
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testPaginationGetNamedCache() throws Exception {
        testPaginationGet(jcache(ignite(), cacheConfiguration(), Integer.class, Integer.class));
    }

    /**
     * @param cache Ignite cache.
     * @throws Exception If failed.
     */
    private void testPaginationGet(IgniteCache<Integer, Integer> cache) throws Exception {
        for (int i = 0; i < 50; i++)
            cache.put(i, i);

        QueryCursor<Cache.Entry<Integer, Integer>> q =
            cache.query(new SqlQuery<Integer, Integer>(Integer.class, "_key >= 0"));

        List<Cache.Entry<Integer, Integer>> list = new ArrayList<>(q.getAll());

        Collections.sort(list, new Comparator<Cache.Entry<Integer, Integer>>() {
            @Override public int compare(Cache.Entry<Integer, Integer> e1, Cache.Entry<Integer, Integer> e2) {
                return e1.getKey().compareTo(e2.getKey());
            }
        });

        for (int i = 0; i < 50; i++) {
            Cache.Entry<Integer, Integer> e = list.get(i);

            assertEquals(i, (int)e.getKey());
            assertEquals(i, (int)e.getValue());
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testScanFilters() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        for (int i = 0; i < 50; i++)
            cache.put(i, i);

        QueryCursor<Cache.Entry<Integer, Integer>> q = cache.query(new ScanQuery<>(new IgniteBiPredicate<Integer, Integer>() {
            @Override public boolean apply(Integer k, Integer v) {
                assertNotNull(k);
                assertNotNull(v);

                return k >= 20 && v < 40;
            }
        }));

        List<Cache.Entry<Integer, Integer>> list = new ArrayList<>(q.getAll());

        Collections.sort(list, new Comparator<Cache.Entry<Integer, Integer>>() {
            @Override public int compare(Cache.Entry<Integer, Integer> e1, Cache.Entry<Integer, Integer> e2) {
                return e1.getKey().compareTo(e2.getKey());
            }
        });

        assertEquals(20, list.size());

        for (int i = 20; i < 40; i++) {
            Cache.Entry<Integer, Integer> e = list.get(i - 20);

            assertEquals(i, (int)e.getKey());
            assertEquals(i, (int)e.getValue());
        }
    }

    /**
     * @throws IgniteCheckedException if failed.
     */
    @Test
    public void testBadHashObjectKey() throws IgniteCheckedException {
        IgniteCache<BadHashKeyObject, Byte> cache = jcache(BadHashKeyObject.class, Byte.class);

        cache.put(new BadHashKeyObject("test_key1"), (byte)1);
        cache.put(new BadHashKeyObject("test_key0"), (byte)10);
        cache.put(new BadHashKeyObject("test_key1"), (byte)7);

        assertEquals(10, cache.query(new SqlQuery<BadHashKeyObject, Byte>(Byte.class, "_key = ?").
            setArgs(new BadHashKeyObject("test_key0"))).getAll().get(0).getValue().intValue());
    }

    /**
     * @throws IgniteCheckedException if failed.
     */
    @Test
    public void testTextIndexedKey() throws IgniteCheckedException {
        IgniteCache<ObjectValue, Long> cache = jcache(ObjectValue.class, Long.class);

        cache.put(new ObjectValue("test_key1", 10), 19L);
        cache.put(new ObjectValue("test_key0", 11), 11005L);
        cache.put(new ObjectValue("test_key1", 12), 17L);

        assertEquals(11005L,
            cache.query(new TextQuery<ObjectValue, Long>(Long.class, "test_key0"))
                .getAll().get(0).getValue().intValue());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testOrderByOnly() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        for (int i = 0; i < 10; i++)
            cache.put(i, i);

        QueryCursor<Cache.Entry<Integer, Integer>> q =
            cache.query(new SqlQuery<Integer, Integer>(Integer.class, "_key >= 0"));

        Collection<Cache.Entry<Integer, Integer>> res = q.getAll();

        assertEquals(10, res.size());

        if (cacheMode() != PARTITIONED) {
            Iterator<Cache.Entry<Integer, Integer>> it = res.iterator();

            for (Integer i = 0; i < 10; i++) {
                assertTrue(it.hasNext());

                Cache.Entry<Integer, Integer> e = it.next();

                assertEquals(i, e.getKey());
                assertEquals(i, e.getValue());
            }
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testLimitOnly() throws Exception {
        IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);

        for (int i = 0; i < 10; i++)
            cache.put(i, i);

        QueryCursor<Cache.Entry<Integer, Integer>> q =
            cache.query(new SqlQuery<Integer, Integer>(Integer.class, "limit 5"));

        Collection<Cache.Entry<Integer, Integer>> res = q.getAll();

        assertEquals(5, res.size());

        Set<Integer> checkDuplicate = new HashSet<>();

        for (Cache.Entry<Integer, Integer> e : res) {
            assert e.getKey() < 10 && e.getKey() >= 0;
            assert e.getValue() < 10 && e.getValue() >= 0;

            checkDuplicate.add(e.getValue());
        }

        assertEquals(5, checkDuplicate.size());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testArray() throws Exception {
        IgniteCache<Integer, ArrayObject> cache = jcache(Integer.class, ArrayObject.class);

        cache.put(1, new ArrayObject(new Long[] {1L, null, 3L}));
        cache.put(2, new ArrayObject(new Long[] {4L, 5L, 6L}));

        QueryCursor<Cache.Entry<Integer, ArrayObject>> q =
            cache.query(new SqlQuery<Integer, ArrayObject>(ArrayObject.class, "array_contains(arr, cast(? as long))").
                setArgs(4));

        Collection<Cache.Entry<Integer, ArrayObject>> res = q.getAll();

        assertEquals(1, res.size());

        Cache.Entry<Integer, ArrayObject> e = F.first(res);

        assertEquals(2, (int)e.getKey());
        assertArrayEquals(new Long[]{4L, 5L, 6L}, e.getValue().arr);
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testFieldsQueryMetadata() throws Exception {
        IgniteCache<UUID, Person> cache = jcache(UUID.class, Person.class);

        for (int i = 0; i < 100; i++)
            cache.put(UUID.randomUUID(), new Person("name-" + i, (i + 1) * 100));

        QueryCursor<List<?>> cur = cache.query(new SqlFieldsQuery("select name, salary from Person where name like ?")
            .setArgs("name-"));

        assertTrue(cur instanceof QueryCursorEx);

        QueryCursorEx<List<?>> curEx = (QueryCursorEx<List<?>>)cur;

        List<GridQueryFieldMetadata> meta = curEx.fieldsMeta();

        assertNotNull(meta);
        assertEquals(2, meta.size());
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testSqlQueryEvents() throws Exception {
        final IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);
        final boolean evtsDisabled = cache.getConfiguration(CacheConfiguration.class).isEventsDisabled();
        final CountDownLatch execLatch = new CountDownLatch(evtsDisabled ? 0 :
            cacheMode() == REPLICATED ? 1 : gridCount());

        IgnitePredicate[] lsnrs = new IgnitePredicate[gridCount()];

        for (int i = 0; i < gridCount(); i++) {
            IgnitePredicate<Event> pred = new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    assert evt instanceof CacheQueryExecutedEvent;

                    System.out.println(">>> EVENT");

                    if (evtsDisabled)
                        fail("Cache events are disabled");

                    CacheQueryExecutedEvent qe = (CacheQueryExecutedEvent)evt;

                    assertEquals(cache.getName(), qe.cacheName());
                    assertNotNull(qe.clause());
                    assertNull(qe.scanQueryFilter());
                    assertNull(qe.continuousQueryFilter());
                    assertArrayEquals(new Integer[] {10}, qe.arguments());

                    execLatch.countDown();

                    return true;
                }
            };

            grid(i).events().localListen(pred, EVT_CACHE_QUERY_EXECUTED);

            lsnrs[i] = pred;
        }

        try {
            for (int i = 0; i < 20; i++)
                cache.put(i, i);

            QueryCursor<Cache.Entry<Integer, Integer>> q =
                cache.query(new SqlQuery<Integer, Integer>(Integer.class, "_key >= ?").setArgs(10));

            q.getAll();

            assert execLatch.await(1000, MILLISECONDS);
        }
        finally {
            for (int i = 0; i < gridCount(); i++)
                grid(i).events().stopLocalListen(lsnrs[i], EVT_CACHE_QUERY_EXECUTED);
        }
    }

    /** @throws Exception If failed. */
    @Test
    @WithSystemProperty(key = IgniteSystemProperties.IGNITE_TO_STRING_INCLUDE_SENSITIVE, value = "false")
    public void testClientQueryExecutedEvents() throws Exception {
        ((Map<?, ?>)GridTestUtils.getFieldValue(new GridToStringBuilder(), S.class, "classCache")).clear();

        doTestClientQueryExecutedEvents(false);
    }

    /** @throws Exception If failed. */
    @Test
    @WithSystemProperty(key = IgniteSystemProperties.IGNITE_TO_STRING_INCLUDE_SENSITIVE, value = "true")
    public void testClientQueryExecutedEventsIncludeSensitive() throws Exception {
        ((Map<?, ?>)GridTestUtils.getFieldValue(new GridToStringBuilder(), S.class, "classCache")).clear();

        doTestClientQueryExecutedEvents(true);
    }

    /** */
    public void doTestClientQueryExecutedEvents(boolean inclSens) throws Exception {
        CountDownLatch execLatch = new CountDownLatch(9);

        IgnitePredicate<SqlQueryExecutionEvent> lsnr = evt -> {
            assertNotNull(evt.text());
            if (inclSens)
                assertTrue(evt.toString().contains("args="));
            else
                assertFalse(evt.toString().contains("args="));

            execLatch.countDown();

            return true;
        };

        ignite().events().localListen(lsnr, EVT_SQL_QUERY_EXECUTION);

        ClientConfiguration cc = new ClientConfiguration().setAddresses(Config.SERVER);

        try (IgniteClient client = Ignition.startClient(cc)) {
            client.query(new SqlFieldsQuery("create table TEST_TABLE(key int primary key, val int)"))
                .getAll();

            client.query(new SqlFieldsQuery("insert into TEST_TABLE values (?, ?)").setArgs(1, 1))
                .getAll();

            client.query(new SqlFieldsQuery("update TEST_TABLE set val = ?2 where key = ?1").setArgs(1, 2))
                .getAll();

            client.query(new SqlFieldsQuery("select * from TEST_TABLE"))
                .getAll();

            client.query(new SqlFieldsQuery("create index idx_1 on TEST_TABLE(key)"))
                .getAll();

            client.query(new SqlFieldsQuery("drop index idx_1"))
                .getAll();

            client.query(new SqlFieldsQuery("alter table TEST_TABLE add column val2 int"))
                .getAll();

            client.query(new SqlFieldsQuery("alter table TEST_TABLE drop val2"))
                .getAll();

            client.query(new SqlFieldsQuery("drop table TEST_TABLE"))
                .getAll();

            assert execLatch.await(3_000, MILLISECONDS);
        }
        finally {
            ignite().events().stopLocalListen(lsnr, EVT_SQL_QUERY_EXECUTION);
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testScanQueryEvents() throws Exception {
        final Map<Integer, Integer> map = new ConcurrentHashMap<>();
        final IgniteCache<Integer, Integer> cache = jcache(Integer.class, Integer.class);
        final boolean evtsDisabled = cache.getConfiguration(CacheConfiguration.class).isEventsDisabled();
        final CountDownLatch latch = new CountDownLatch(evtsDisabled ? 0 : 10);
        final CountDownLatch execLatch = new CountDownLatch(evtsDisabled ? 0 :
            cacheMode() == REPLICATED ? 1 : gridCount());

        IgnitePredicate[] objReadLsnrs = new IgnitePredicate[gridCount()];
        IgnitePredicate[] qryExecLsnrs = new IgnitePredicate[gridCount()];

        for (int i = 0; i < gridCount(); i++) {
            IgnitePredicate<Event> pred = new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    assert evt instanceof CacheQueryReadEvent;

                    if (evtsDisabled)
                        fail("Cache events are disabled");

                    CacheQueryReadEvent<Integer, Integer> qe = (CacheQueryReadEvent<Integer, Integer>)evt;

                    assertEquals(SCAN.name(), qe.queryType());
                    assertEquals(cache.getName(), qe.cacheName());

                    assertNull(qe.className());
                    assertNull(null, qe.clause());
                    assertNotNull(qe.scanQueryFilter());
                    assertNull(qe.continuousQueryFilter());
                    assertNull(qe.arguments());

                    map.put(qe.key(), qe.value());

                    latch.countDown();

                    return true;
                }
            };

            grid(i).events().localListen(pred, EVT_CACHE_QUERY_OBJECT_READ);
            objReadLsnrs[i] = pred;

            IgnitePredicate<Event> execPred = new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    assert evt instanceof CacheQueryExecutedEvent;

                    if (evtsDisabled)
                        fail("Cache events are disabled");

                    CacheQueryExecutedEvent qe = (CacheQueryExecutedEvent)evt;

                    assertEquals(SCAN.name(), qe.queryType());
                    assertEquals(cache.getName(), qe.cacheName());

                    assertNull(qe.className());
                    assertNull(null, qe.clause());
                    assertNotNull(qe.scanQueryFilter());
                    assertNull(qe.continuousQueryFilter());
                    assertNull(qe.arguments());

                    execLatch.countDown();

                    return true;
                }
            };

            grid(i).events().localListen(execPred, EVT_CACHE_QUERY_EXECUTED);
            qryExecLsnrs[i] = execPred;
        }

        try {
            for (int i = 0; i < 20; i++)
                cache.put(i, i);

            IgniteBiPredicate<Integer, Integer> filter = new IgniteBiPredicate<Integer, Integer>() {
                @Override public boolean apply(Integer k, Integer v) {
                    return k >= 10;
                }
            };

            QueryCursor<Cache.Entry<Integer, Integer>> q = cache.query(new ScanQuery<>(filter));

            q.getAll();

            assert latch.await(1000, MILLISECONDS);
            assert execLatch.await(1000, MILLISECONDS);

            if (!evtsDisabled) {
                assertEquals(10, map.size());

                for (int i = 10; i < 20; i++)
                    assertEquals(i, map.get(i).intValue());
            }
        }
        finally {
            for (int i = 0; i < gridCount(); i++) {
                grid(i).events().stopLocalListen(objReadLsnrs[i]);
                grid(i).events().stopLocalListen(qryExecLsnrs[i]);
            }
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testTextQueryEvents() throws Exception {
        final Map<UUID, Person> map = new ConcurrentHashMap<>();
        final IgniteCache<UUID, Person> cache = jcache(UUID.class, Person.class);
        final boolean evtsDisabled = cache.getConfiguration(CacheConfiguration.class).isEventsDisabled();
        final CountDownLatch latch = new CountDownLatch(evtsDisabled ? 0 : 2);
        final CountDownLatch execLatch = new CountDownLatch(evtsDisabled ? 0 :
            cacheMode() == REPLICATED ? 1 : gridCount());

        IgnitePredicate[] objReadLsnrs = new IgnitePredicate[gridCount()];
        IgnitePredicate[] qryExecLsnrs = new IgnitePredicate[gridCount()];

        for (int i = 0; i < gridCount(); i++) {
            IgnitePredicate<Event> objReadPred = new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    assert evt instanceof CacheQueryReadEvent;

                    if (evtsDisabled)
                        fail("Cache events are disabled");

                    CacheQueryReadEvent<UUID, Person> qe = (CacheQueryReadEvent<UUID, Person>)evt;

                    assertEquals(FULL_TEXT.name(), qe.queryType());
                    assertEquals(cache.getName(), qe.cacheName());

                    assertEquals("Person", qe.className());
                    assertEquals("White", qe.clause());
                    assertNull(qe.scanQueryFilter());
                    assertNull(qe.continuousQueryFilter());
                    assertNull(qe.arguments());

                    map.put(qe.key(), qe.value());

                    latch.countDown();

                    return true;
                }
            };

            grid(i).events().localListen(objReadPred, EVT_CACHE_QUERY_OBJECT_READ);
            objReadLsnrs[i] = objReadPred;

            IgnitePredicate<Event> qryExecPred = new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    assert evt instanceof CacheQueryExecutedEvent;

                    if (evtsDisabled)
                        fail("Cache events are disabled");

                    CacheQueryExecutedEvent qe = (CacheQueryExecutedEvent)evt;

                    assertEquals(FULL_TEXT.name(), qe.queryType());
                    assertEquals(cache.getName(), qe.cacheName());

                    assertEquals("Person", qe.className());
                    assertEquals("White", qe.clause());
                    assertNull(qe.scanQueryFilter());
                    assertNull(qe.continuousQueryFilter());
                    assertNull(qe.arguments());

                    execLatch.countDown();

                    return true;
                }
            };

            grid(i).events().localListen(qryExecPred, EVT_CACHE_QUERY_EXECUTED);
            qryExecLsnrs[i] = qryExecPred;
        }

        try {
            UUID k1 = UUID.randomUUID();
            UUID k2 = UUID.randomUUID();
            UUID k3 = UUID.randomUUID();

            cache.put(k1, new Person("Bob White", 1000));
            cache.put(k2, new Person("Tom White", 1000));
            cache.put(k3, new Person("Mike Green", 1000));

            QueryCursor<Cache.Entry<UUID, Person>> q = cache.query(new TextQuery<UUID, Person>(Person.class, "White"));

            q.getAll();

            assert latch.await(1000, MILLISECONDS);
            assert execLatch.await(1000, MILLISECONDS);

            if (!evtsDisabled) {
                assertEquals(2, map.size());

                assertEquals("Bob White", map.get(k1).name());
                assertEquals("Tom White", map.get(k2).name());
            }
        }
        finally {
            for (int i = 0; i < gridCount(); i++) {
                grid(i).events().stopLocalListen(objReadLsnrs[i]);
                grid(i).events().stopLocalListen(qryExecLsnrs[i]);
            }
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testFieldsQueryEvents() throws Exception {
        final IgniteCache<UUID, Person> cache = jcache(UUID.class, Person.class);
        final boolean evtsDisabled = cache.getConfiguration(CacheConfiguration.class).isEventsDisabled();
        final CountDownLatch execLatch = new CountDownLatch(evtsDisabled ? 0 :
            cacheMode() == REPLICATED ? 1 : gridCount());

        IgnitePredicate[] qryExecLsnrs = new IgnitePredicate[gridCount()];

        for (int i = 0; i < gridCount(); i++) {
            IgnitePredicate<Event> pred = new IgnitePredicate<Event>() {
                @Override public boolean apply(Event evt) {
                    assert evt instanceof CacheQueryExecutedEvent;

                    if (evtsDisabled)
                        fail("Cache events are disabled");

                    CacheQueryExecutedEvent qe = (CacheQueryExecutedEvent)evt;

                    assertEquals(cache.getName(), qe.cacheName());
                    assertNotNull(qe.clause());
                    assertNull(qe.scanQueryFilter());
                    assertNull(qe.continuousQueryFilter());
                    assertArrayEquals(new Integer[] {10}, qe.arguments());

                    execLatch.countDown();

                    return true;
                }
            };

            grid(i).events().localListen(pred, EVT_CACHE_QUERY_EXECUTED);
            qryExecLsnrs[i] = pred;
        }

        try {
            for (int i = 1; i <= 20; i++)
                cache.put(UUID.randomUUID(), new Person("Person " + i, i));

            QueryCursor<List<?>> q = cache.query(new SqlFieldsQuery("select _key, name from Person where salary > ?").
                setArgs(10));

            q.getAll();

            assert execLatch.await(1000, MILLISECONDS);
        }
        finally {
            for (int i = 0; i < gridCount(); i++)
                grid(i).events().stopLocalListen(qryExecLsnrs[i]);
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testLocalSqlQueryFromClient() throws Exception {
        try (Ignite g = startClientGrid("client")) {
            IgniteCache<Integer, Integer> c = jcache(g, Integer.class, Integer.class);

            for (int i = 0; i < 10; i++)
                c.put(i, i);

            SqlQuery<Integer, Integer> qry = new SqlQuery<>(Integer.class, "_key >= 5 order by _key");

            qry.setLocal(true);

            assertThrowsWithCause(() -> c.query(qry), CacheException.class);
        }
    }

    /**
     * @throws Exception If failed.
     */
    @Test
    public void testLocalSqlFieldsQueryFromClient() throws Exception {
        try (Ignite g = startClientGrid("client")) {
            IgniteCache<UUID, Person> c = jcache(g, UUID.class, Person.class);

            Person p = new Person("Jon", 1500);

            c.put(p.id(), p);

            SqlFieldsQuery qry = new SqlFieldsQuery("select count(*) from Person");

            qry.setLocal(true);

            assertThrowsWithCause(() -> c.query(qry), CacheException.class);
        }
    }

    /**
     *
     */
    private static class ArrayObject implements Serializable {
        /** */
        @QuerySqlField
        private Long[] arr;

        /**
         * @param arr Array.
         */
        private ArrayObject(Long[] arr) {
            this.arr = arr;
        }
    }

    /**
     *
     */
    public static class Person implements Externalizable {
        /** */
        @GridToStringExclude
        @QuerySqlField
        private UUID id = UUID.randomUUID();

        /** */
        @QuerySqlField
        @QueryTextField
        private String name;

        /** */
        @QuerySqlField
        private int salary;

        /** */
        @QuerySqlField(index = true)
        private int fake$Field;

        /**
         * Required by {@link Externalizable}.
         */
        public Person() {
            // No-op.
        }

        /**
         * @param name Name.
         * @param salary Salary.
         */
        public Person(String name, int salary) {
            assert name != null;
            assert salary > 0;

            this.name = name;
            this.salary = salary;
        }

        /**
         * @return Id.
         */
        public UUID id() {
            return id;
        }

        /**
         * @return Name.
         */
        public String name() {
            return name;
        }

        /**
         * @return Salary.
         */
        public int salary() {
            return salary;
        }

        /** {@inheritDoc} */
        @Override public void writeExternal(ObjectOutput out) throws IOException {
            U.writeUuid(out, id);
            U.writeString(out, name);
            out.writeInt(salary);
        }

        /** {@inheritDoc} */
        @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
            id = U.readUuid(in);
            name = U.readString(in);
            salary = in.readInt();
        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return id.hashCode() + 31 * name.hashCode() + 31 * 31 * salary;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (!(obj instanceof Person))
                return false;

            Person that = (Person)obj;

            return that.id.equals(id) && that.name.equals(name) && that.salary == salary;
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(Person.class, this);
        }
    }

    /**
     *
     */
    public static class Type1 implements Serializable {
        /** */
        private int id;

        /** */
        private String name;

        /**
         * @param id ID.
         * @param name Name.
         */
        Type1(int id, String name) {
            assert name != null;
            assert id > 0;

            this.name = name;
            this.id = id;
        }

        /**
         * @return Name.
         */
        public String name() {
            return name;
        }

        /**
         * @return ID.
         */
        public int id() {
            return id;
        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return name.hashCode() + 31 * id;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (!(obj instanceof Type1))
                return false;

            Type1 that = (Type1)obj;

            return that.name.equals(name) && that.id == id;
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(Type1.class, this);
        }
    }

    /**
     *
     */
    public static class Type2 implements Serializable {
        /** */
        private int id;

        /** */
        private String name;

        /**
         * @param id ID.
         * @param name Name.
         */
        Type2(int id, String name) {
            assert name != null;
            assert id > 0;

            this.name = name;
            this.id = id;
        }

        /**
         * @return Name.
         */
        public String name() {
            return name;
        }

        /**
         * @return ID.
         */
        public int id() {
            return id;
        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return name.hashCode() + 31 * id;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object obj) {
            if (obj == this)
                return true;

            if (!(obj instanceof Type2))
                return false;

            Type2 that = (Type2)obj;

            return that.name.equals(name) && that.id == id;
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(Type2.class, this);
        }
    }

    /**
     * Test value object.
     */
    @SuppressWarnings("PublicInnerClass")
    public static class ObjectValue implements Serializable {
        /** String value. */
        @QueryTextField
        private String strVal;

        /** Integer value. */
        @QuerySqlField
        private int intVal;

        /**
         * Constructor.
         *
         * @param strVal String value.
         * @param intVal Integer value.
         */
        ObjectValue(String strVal, int intVal) {
            this.strVal = strVal;
            this.intVal = intVal;
        }

        /**
         * Gets value.
         *
         * @return Value.
         */
        public String getStringValue() {
            return strVal;
        }

        /**
         * @return Integer value.
         */
        public int intValue() {
            return intVal;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object o) {
            if (this == o)
                return true;

            if (o == null || getClass() != o.getClass())
                return false;

            ObjectValue other = (ObjectValue)o;

            return strVal == null ? other.strVal == null : strVal.equals(other.strVal);

        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return strVal != null ? strVal.hashCode() : 0;
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(ObjectValue.class, this);
        }
    }

    /**
     * Another test value object.
     */
    private static class ObjectValueOther {
        /** Value. */
        @QueryTextField
        private String val;

        /**
         * @param val String value.
         */
        ObjectValueOther(String val) {
            this.val = val;
        }

        /**
         * Gets value.
         *
         * @return Value.
         */
        public String value() {
            return val;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object o) {
            if (this == o)
                return true;

            if (o == null || getClass() != o.getClass())
                return false;

            ObjectValueOther other = (ObjectValueOther)o;

            return val == null ? other.val == null : val.equals(other.val);

        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return val != null ? val.hashCode() : 0;
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(ObjectValueOther.class, this);
        }
    }

    /**
     * Another test value object.
     */
    private static class ObjectValue2 {
        /** Value. */
        private String strVal;

        /**
         * @param strVal String value.
         */
        ObjectValue2(String strVal) {
            this.strVal = strVal;
        }

        /**
         * Gets value.
         *
         * @return Value.
         */
        public String value() {
            return strVal;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object o) {
            if (this == o)
                return true;

            if (o == null || getClass() != o.getClass())
                return false;

            ObjectValue2 other = (ObjectValue2)o;

            return strVal == null ? other.strVal == null : strVal.equals(other.strVal);

        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return strVal != null ? strVal.hashCode() : 0;
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(ObjectValue2.class, this);
        }
    }

    /**
     *
     */
    private static class BadHashKeyObject implements Serializable, Comparable<BadHashKeyObject> {
        /** */
        @QuerySqlField(index = false)
        private final String str;

        /**
         * @param str String.
         */
        private BadHashKeyObject(String str) {
            this.str = str == null ? "" : str;
        }

        /** {@inheritDoc} */
        @Override public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            BadHashKeyObject keyObj = (BadHashKeyObject) o;

            return str.equals(keyObj.str);
        }

        /** {@inheritDoc} */
        @Override public int hashCode() {
            return 10;
        }

        /** {@inheritDoc} */
        @Override public int compareTo(BadHashKeyObject o) {
            return str.compareTo(o.str);
        }

        /** {@inheritDoc} */
        @Override public String toString() {
            return S.toString(BadHashKeyObject.class, this);
        }
    }

    /**
     * Test store.
     */
    private static class TestStore extends CacheStoreAdapter<Object, Object> {
        /** */
        private Map<Object, Object> map = new ConcurrentHashMap<>();

        /** */
        void reset() {
            map.clear();
        }

        /** {@inheritDoc} */
        @Override public Object load(Object key) {
            return map.get(key);
        }

        /** {@inheritDoc} */
        @Override public void write(javax.cache.Cache.Entry<? extends Object, ? extends Object> e) {
            map.put(e.getKey(), e.getValue());
        }

        /** {@inheritDoc} */
        @Override public void delete(Object key) {
            map.remove(key);
        }
    }

    /**
     * Functions for test.
     */
    @SuppressWarnings("PublicInnerClass")
    public static class SqlFunctions {
        /**
         * @param x Argument.
         * @return Square of given value.
         */
        @QuerySqlFunction
        public static int square(int x) {
            return x * x;
        }

        /**
         * @param x Argument.
         * @return Cube of given value.
         */
        @QuerySqlFunction(alias = "_cube_")
        public static int cube(int x) {
            return x * x * x;
        }

        /**
         * Method which should not be registered.
         * @return Nothing.
         */
        public static int no() {
            throw new IllegalStateException();
        }
    }

    /**
     *
     */
    private static class StoreFactory implements Factory<CacheStore> {
        @Override public CacheStore create() {
            return store;
        }
    }

    /**
     * Test enum class.
     */
    private static class EnumObject {
        /**
         * Test id.
         */
        @QuerySqlField(index = true)
        private long id;

        /**
         * Test enum.
         */
        @QuerySqlField
        private EnumType type;

        /**
         * @param id id.
         * @param type enum.
         */
        public EnumObject(long id, EnumType type) {
            this.id = id;
            this.type = type;
        }

        /**
         * @return string representation of object.
         */
        @Override public String toString() {
            return "EnumObject{" +
                "id=" + id +
                ", type=" + type +
                '}';
        }
    }

    /**
     * Test enum.
     */
    private enum EnumType {
        /** */
        TYPE_A,
        /** */
        TYPE_B
    }
}
