/* * 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.index; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import javax.cache.CacheException; import org.apache.ignite.Ignite; import org.apache.ignite.IgniteCache; import org.apache.ignite.Ignition; import org.apache.ignite.cache.CacheAtomicityMode; import org.apache.ignite.cache.CacheMode; import org.apache.ignite.cache.CacheWriteSynchronizationMode; import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cache.query.SqlFieldsQuery; import org.apache.ignite.configuration.CacheConfiguration; import org.apache.ignite.configuration.IgniteConfiguration; import org.apache.ignite.configuration.NearCacheConfiguration; import org.apache.ignite.internal.binary.BinaryMarshaller; import org.apache.ignite.internal.processors.cache.query.IgniteQueryErrorCode; import org.apache.ignite.internal.processors.query.IgniteSQLException; import org.apache.ignite.internal.util.typedef.F; /** * Test that checks indexes handling on H2 side. */ public abstract class H2DynamicIndexAbstractSelfTest extends AbstractSchemaSelfTest { /** Client node index. */ private final static int CLIENT = 2; /** {@inheritDoc} */ @Override protected void beforeTestsStarted() throws Exception { super.beforeTestsStarted(); for (IgniteConfiguration cfg : configurations()) Ignition.start(cfg); } /** {@inheritDoc} */ @Override protected void afterTestsStopped() throws Exception { stopAllGrids(); super.afterTestsStopped(); } /** {@inheritDoc} */ @Override protected void beforeTest() throws Exception { super.beforeTest(); client().getOrCreateCache(cacheConfiguration()); assertNoIndex(CACHE_NAME, TBL_NAME, IDX_NAME_1); IgniteCache<KeyClass, ValueClass> cache = client().cache(CACHE_NAME); cache.put(new KeyClass(1), new ValueClass("val1")); cache.put(new KeyClass(2), new ValueClass("val2")); cache.put(new KeyClass(3), new ValueClass("val3")); } /** {@inheritDoc} */ @Override protected void afterTest() throws Exception { client().destroyCache(CACHE_NAME); super.afterTest(); } /** * Test that after index creation index is used by queries. */ public void testCreateIndex() throws Exception { IgniteCache<KeyClass, ValueClass> cache = cache(); assertSize(3); cache.query(new SqlFieldsQuery("CREATE INDEX \"" + IDX_NAME_1 + "\" ON \"" + TBL_NAME + "\"(\"" + FIELD_NAME_1 + "\" ASC)")).getAll(); // Test that local queries on all nodes use new index. for (int i = 0 ; i < 4; i++) { List<List<?>> locRes = ignite(i).cache("cache").query(new SqlFieldsQuery("explain select \"id\" from " + "\"cache\".\"ValueClass\" where \"field1\" = 'A'").setLocal(true)).getAll(); assertEquals(F.asList( Collections.singletonList("SELECT\n" + " \"id\"\n" + "FROM \"cache\".\"ValueClass\"\n" + " /* \"cache\".\"idx_1\": \"field1\" = 'A' */\n" + "WHERE \"field1\" = 'A'") ), locRes); } assertSize(3); cache.remove(new KeyClass(2)); assertSize(2); cache.put(new KeyClass(4), new ValueClass("someVal")); assertSize(3); } /** * Test that creating an index with duplicate name yields an error. */ public void testCreateIndexWithDuplicateName() { final IgniteCache<KeyClass, ValueClass> cache = cache(); cache.query(new SqlFieldsQuery("CREATE INDEX \"" + IDX_NAME_1 + "\" ON \"" + TBL_NAME + "\"(\"" + FIELD_NAME_1 + "\" ASC)")); assertSqlException(new RunnableX() { @Override public void run() throws Exception { cache.query(new SqlFieldsQuery("CREATE INDEX \"" + IDX_NAME_1 + "\" ON \"" + TBL_NAME + "\"(\"id\" ASC)")); } }, IgniteQueryErrorCode.INDEX_ALREADY_EXISTS); } /** * Test that creating an index with duplicate name does not yield an error with {@code IF NOT EXISTS}. */ public void testCreateIndexIfNotExists() { final IgniteCache<KeyClass, ValueClass> cache = cache(); cache.query(new SqlFieldsQuery("CREATE INDEX \"" + IDX_NAME_1 + "\" ON \"" + TBL_NAME + "\"(\"" + FIELD_NAME_1 + "\" ASC)")); cache.query(new SqlFieldsQuery("CREATE INDEX IF NOT EXISTS \"" + IDX_NAME_1 + "\" ON \"" + TBL_NAME + "\"(\"id\" ASC)")); } /** * Test that after index drop there are no attempts to use it, and data state remains intact. */ public void testDropIndex() { IgniteCache<KeyClass, ValueClass> cache = cache(); assertSize(3); cache.query(new SqlFieldsQuery("CREATE INDEX \"" + IDX_NAME_1 + "\" ON \"" + TBL_NAME + "\"(\"" + FIELD_NAME_1 + "\" ASC)")); assertSize(3); cache.query(new SqlFieldsQuery("DROP INDEX \"" + IDX_NAME_1 + "\"")); // Test that no local queries on all nodes use new index. for (int i = 0 ; i < 4; i++) { List<List<?>> locRes = ignite(i).cache("cache").query(new SqlFieldsQuery("explain select \"id\" from " + "\"cache\".\"ValueClass\" where \"field1\" = 'A'").setLocal(true)).getAll(); assertEquals(F.asList( Collections.singletonList("SELECT\n" + " \"id\"\n" + "FROM \"cache\".\"ValueClass\"\n" + " /* \"cache\".\"ValueClass\".__SCAN_ */\n" + "WHERE \"field1\" = 'A'") ), locRes); } assertSize(3); } /** * Test that dropping a non-existent index yields an error. */ public void testDropMissingIndex() { final IgniteCache<KeyClass, ValueClass> cache = cache(); assertSqlException(new RunnableX() { @Override public void run() throws Exception { cache.query(new SqlFieldsQuery("DROP INDEX \"" + IDX_NAME_1 + "\"")); } }, IgniteQueryErrorCode.INDEX_NOT_FOUND); } /** * Test that dropping a non-existent index does not yield an error with {@code IF EXISTS}. */ public void testDropMissingIndexIfExists() { final IgniteCache<KeyClass, ValueClass> cache = cache(); cache.query(new SqlFieldsQuery("DROP INDEX IF EXISTS \"" + IDX_NAME_1 + "\"")); } /** * Test that changes in cache affect index, and vice versa. */ public void testIndexState() { IgniteCache<KeyClass, ValueClass> cache = cache(); assertColumnValues("val1", "val2", "val3"); cache.query(new SqlFieldsQuery("CREATE INDEX \"" + IDX_NAME_1 + "\" ON \"" + TBL_NAME + "\"(\"" + FIELD_NAME_1 + "\" ASC)")); assertColumnValues("val1", "val2", "val3"); cache.remove(new KeyClass(2)); assertColumnValues("val1", "val3"); cache.put(new KeyClass(0), new ValueClass("someVal")); assertColumnValues("someVal", "val1", "val3"); cache.query(new SqlFieldsQuery("DROP INDEX \"" + IDX_NAME_1 + "\"")); assertColumnValues("someVal", "val1", "val3"); } /** * Check that values of {@code field1} match what we expect. * @param vals Expected values. */ private void assertColumnValues(String... vals) { List<List<?>> expRes = new ArrayList<>(vals.length); for (String v : vals) expRes.add(Collections.singletonList(v)); assertEquals(expRes, cache().query(new SqlFieldsQuery("SELECT \"" + FIELD_NAME_1 + "\" FROM \"" + TBL_NAME + "\" ORDER BY \"id\"")) .getAll()); } /** * Do a {@code SELECT COUNT(*)} query to check index state correctness. * @param expSize Expected number of items in table. */ private void assertSize(long expSize) { assertEquals(expSize, cache().size()); assertEquals(expSize, cache().query(new SqlFieldsQuery("SELECT COUNT(*) from \"ValueClass\"")) .getAll().get(0).get(0)); } /** * Get configurations to be used in test. * * @return Configurations. * @throws Exception If failed. */ private List<IgniteConfiguration> configurations() throws Exception { return Arrays.asList( serverConfiguration(0), serverConfiguration(1), clientConfiguration(2), serverConfiguration(3) ); } /** * @return Client node. */ private Ignite client() { return ignite(CLIENT); } /** * @return Cache. */ private IgniteCache<KeyClass, ValueClass> cache() { return client().cache(CACHE_NAME); } /** * Create server configuration. * * @param idx Index. * @return Configuration. * @throws Exception If failed. */ private IgniteConfiguration serverConfiguration(int idx) throws Exception { return commonConfiguration(idx); } /** * Create client configuration. * * @param idx Index. * @return Configuration. * @throws Exception If failed. */ private IgniteConfiguration clientConfiguration(int idx) throws Exception { return commonConfiguration(idx).setClientMode(true); } /** * Create common node configuration. * * @param idx Index. * @return Configuration. * @throws Exception If failed. */ private IgniteConfiguration commonConfiguration(int idx) throws Exception { IgniteConfiguration cfg = super.getConfiguration(getTestIgniteInstanceName(idx)); cfg.setMarshaller(new BinaryMarshaller()); return optimize(cfg); } /** * @return Default cache configuration. */ private CacheConfiguration cacheConfiguration() { CacheConfiguration<KeyClass, ValueClass> ccfg = new CacheConfiguration<KeyClass, ValueClass>() .setName(CACHE_NAME); QueryEntity entity = new QueryEntity(); entity.setKeyType(KeyClass.class.getName()); entity.setValueType(ValueClass.class.getName()); entity.addQueryField("id", Long.class.getName(), null); entity.addQueryField(FIELD_NAME_1, String.class.getName(), null); entity.addQueryField(FIELD_NAME_2, String.class.getName(), null); entity.setKeyFields(Collections.singleton("id")); entity.setAliases(Collections.singletonMap(FIELD_NAME_2, alias(FIELD_NAME_2))); ccfg.setQueryEntities(Collections.singletonList(entity)); ccfg.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC); ccfg.setSqlEscapeAll(true); ccfg.setAtomicityMode(atomicityMode()); ccfg.setCacheMode(cacheMode()); if (nearCache()) ccfg.setNearConfiguration(new NearCacheConfiguration<KeyClass, ValueClass>()); return ccfg; } /** * @return Cache mode to use. */ protected abstract CacheMode cacheMode(); /** * @return Cache atomicity mode to use. */ protected abstract CacheAtomicityMode atomicityMode(); /** * @return Whether to use near cache. */ protected abstract boolean nearCache(); /** * Ensure that SQL exception is thrown. * * @param r Runnable. * @param expCode Error code. */ private static void assertSqlException(DynamicIndexAbstractBasicSelfTest.RunnableX r, int expCode) { try { try { r.run(); } catch (CacheException e) { if (e.getCause() != null) throw (Exception)e.getCause(); else throw e; } } catch (IgniteSQLException e) { assertEquals("Unexpected error code [expected=" + expCode + ", actual=" + e.statusCode() + ']', expCode, e.statusCode()); return; } catch (Exception e) { fail("Unexpected exception: " + e); } fail(IgniteSQLException.class.getSimpleName() + " is not thrown."); } }