/* * 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.query.h2; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.ignite.IgniteCache; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.IgniteLogger; import org.apache.ignite.binary.BinaryObject; import org.apache.ignite.binary.BinaryObjectBuilder; import org.apache.ignite.cache.QueryEntity; import org.apache.ignite.cache.QueryIndex; import org.apache.ignite.cache.QueryIndexType; import org.apache.ignite.configuration.CacheConfiguration; import org.apache.ignite.configuration.IgniteConfiguration; import org.apache.ignite.internal.IgniteEx; import org.apache.ignite.internal.binary.BinaryMarshaller; import org.apache.ignite.internal.binary.BinaryObjectImpl; import org.apache.ignite.internal.processors.cache.CacheObject; import org.apache.ignite.internal.processors.cache.CacheObjectContext; import org.apache.ignite.internal.processors.cache.KeyCacheObject; import org.apache.ignite.internal.processors.query.GridQueryFieldsResult; import org.apache.ignite.internal.processors.query.GridQueryIndexDescriptor; import org.apache.ignite.internal.processors.query.GridQueryProperty; import org.apache.ignite.internal.processors.query.GridQueryTypeDescriptor; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.U; import org.apache.ignite.lang.IgniteBiTuple; import org.apache.ignite.plugin.extensions.communication.MessageReader; import org.apache.ignite.plugin.extensions.communication.MessageWriter; import org.apache.ignite.spi.IgniteSpiCloseableIterator; import org.apache.ignite.spi.IgniteSpiException; import org.apache.ignite.testframework.GridStringLogger; import org.apache.ignite.testframework.GridTestUtils; import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; import org.h2.util.JdbcUtils; import org.jetbrains.annotations.Nullable; /** * Tests for all SQL based indexing SPI implementations. */ public abstract class GridIndexingSpiAbstractSelfTest extends GridCommonAbstractTest { /** */ private static final TextIndex textIdx = new TextIndex(F.asList("txt")); /** */ private static final LinkedHashMap<String, String> fieldsAA = new LinkedHashMap<>(); /** */ private static final LinkedHashMap<String, String> fieldsAB = new LinkedHashMap<>(); /** */ private static final LinkedHashMap<String, String> fieldsBA = new LinkedHashMap<>(); /** */ private IgniteEx ignite0; /** {@inheritDoc} */ @Override protected IgniteConfiguration getConfiguration(String gridName) throws Exception { IgniteConfiguration cfg = super.getConfiguration(gridName); cfg.setMarshaller(new BinaryMarshaller()); return cfg; } /* * Fields initialization. */ static { fieldsAA.put("id", Long.class.getName()); fieldsAA.put("name", String.class.getName()); fieldsAA.put("age", Integer.class.getName()); fieldsAB.putAll(fieldsAA); fieldsAB.put("txt", String.class.getName()); fieldsBA.putAll(fieldsAA); fieldsBA.put("sex", Boolean.class.getName()); } /** */ private static TypeDesc typeAA = new TypeDesc("A", "A", Collections.<String, Class<?>>emptyMap(), null); /** */ private static TypeDesc typeAB = new TypeDesc("A", "B", Collections.<String, Class<?>>emptyMap(), textIdx); /** */ private static TypeDesc typeBA = new TypeDesc("B", "A", Collections.<String, Class<?>>emptyMap(), null); /** {@inheritDoc} */ @Override protected void beforeTest() throws Exception { ignite0 = startGrid(0); } /** */ private CacheConfiguration cacheACfg() { CacheConfiguration<?,?> cfg = new CacheConfiguration<>(DEFAULT_CACHE_NAME); cfg.setName("A"); QueryEntity eA = new QueryEntity(Integer.class.getName(), "A"); eA.setFields(fieldsAA); QueryEntity eB = new QueryEntity(Integer.class.getName(), "B"); eB.setFields(fieldsAB); List<QueryEntity> list = new ArrayList<>(2); list.add(eA); list.add(eB); QueryIndex idx = new QueryIndex("txt"); idx.setIndexType(QueryIndexType.FULLTEXT); eB.setIndexes(Collections.singleton(idx)); cfg.setQueryEntities(list); return cfg; } /** * */ private CacheConfiguration cacheBCfg() { CacheConfiguration cfg = new CacheConfiguration(DEFAULT_CACHE_NAME); cfg.setName("B"); QueryEntity eA = new QueryEntity(Integer.class.getName(), "A"); eA.setFields(fieldsBA); cfg.setQueryEntities(Collections.singleton(eA)); return cfg; } /** {@inheritDoc} */ @Override protected void afterTest() throws Exception { stopAllGrids(); } /** * @param id Id. * @param name Name. * @param age Age. * @return AA. */ private BinaryObjectBuilder aa(String typeName, long id, String name, int age) { BinaryObjectBuilder aBuilder = ignite0.binary().builder(typeName) .setField("id", id) .setField("name", name) .setField("age", age); return aBuilder; } /** * @param id Id. * @param name Name. * @param age Age. * @param txt Text. * @return AB. */ private BinaryObjectBuilder ab(long id, String name, int age, String txt) { BinaryObjectBuilder aBuilder = aa("B", id, name, age); aBuilder.setField("txt", txt); return aBuilder; } /** * @param id Id. * @param name Name. * @param age Age. * @param sex Sex. * @return BA. */ private BinaryObjectBuilder ba(long id, String name, int age, boolean sex) { BinaryObjectBuilder builder = aa("A", id, name, age); builder.setField("sex", sex); return builder; } /** * @param row Row * @return Value. * @throws IgniteSpiException If failed. */ private BinaryObjectImpl value(IgniteBiTuple<Integer, BinaryObjectImpl> row) throws IgniteSpiException { return row.get2(); } /** * @return Indexing. */ private IgniteH2Indexing getIndexing() { return U.field(ignite0.context().query(), "idx"); } /** * @return {@code true} if OFF-HEAP mode should be tested. */ protected boolean offheap() { return false; } /** * @param key Key. * @return Cache object. */ private KeyCacheObject key(int key) { return new TestCacheObject(key); } /** * @throws Exception If failed. */ public void testSpi() throws Exception { IgniteH2Indexing spi = getIndexing(); assertEquals(-1, spi.size(typeAA.space(), typeAA.name())); assertEquals(-1, spi.size(typeAB.space(), typeAB.name())); assertEquals(-1, spi.size(typeBA.space(), typeBA.name())); IgniteCache<Integer, BinaryObject> cacheA = ignite0.createCache(cacheACfg()); assertEquals(0, spi.size(typeAA.space(), typeAA.name())); assertEquals(0, spi.size(typeAB.space(), typeAB.name())); assertEquals(-1, spi.size(typeBA.space(), typeBA.name())); IgniteCache<Integer, BinaryObject> cacheB = ignite0.createCache(cacheBCfg()); // Initially all is empty. assertEquals(0, spi.size(typeAA.space(), typeAA.name())); assertEquals(0, spi.size(typeAB.space(), typeAB.name())); assertEquals(0, spi.size(typeBA.space(), typeBA.name())); assertFalse(spi.queryLocalSql(typeAA.space(), "select * from A.A", null, Collections.emptySet(), typeAA.name(), null, null).hasNext()); assertFalse(spi.queryLocalSql(typeAB.space(), "select * from A.B", null, Collections.emptySet(), typeAB.name(), null, null).hasNext()); assertFalse(spi.queryLocalSql(typeBA.space(), "select * from B.A", null, Collections.emptySet(), typeBA.name(), null, null).hasNext()); assertFalse(spi.queryLocalSql(typeBA.space(), "select * from B.A, A.B, A.A", null, Collections.emptySet(), typeBA.name(), null, null).hasNext()); try { spi.queryLocalSql(typeBA.space(), "select aa.*, ab.*, ba.* from A.A aa, A.B ab, B.A ba", null, Collections.emptySet(), typeBA.name(), null, null).hasNext(); fail("Enumerations of aliases in select block must be prohibited"); } catch (IgniteCheckedException ignored) { // all fine } assertFalse(spi.queryLocalSql(typeAB.space(), "select ab.* from A.B ab", null, Collections.emptySet(), typeAB.name(), null, null).hasNext()); assertFalse(spi.queryLocalSql(typeBA.space(), "select ba.* from B.A as ba", null, Collections.emptySet(), typeBA.name(), null, null).hasNext()); cacheA.put(1, aa("A", 1, "Vasya", 10).build()); assertEquals(1, spi.size(typeAA.space(), typeAA.name())); assertEquals(0, spi.size(typeAB.space(), typeAB.name())); assertEquals(0, spi.size(typeBA.space(), typeBA.name())); cacheA.put(1, ab(1, "Vasya", 20, "Some text about Vasya goes here.").build()); // In one space all keys must be unique. assertEquals(0, spi.size(typeAA.space(), typeAA.name())); assertEquals(1, spi.size(typeAB.space(), typeAB.name())); assertEquals(0, spi.size(typeBA.space(), typeBA.name())); cacheB.put(1, ba(2, "Petya", 25, true).build()); // No replacement because of different space. assertEquals(0, spi.size(typeAA.space(), typeAA.name())); assertEquals(1, spi.size(typeAB.space(), typeAB.name())); assertEquals(1, spi.size(typeBA.space(), typeBA.name())); cacheB.put(1, ba(2, "Kolya", 25, true).build()); // Replacement in the same table. assertEquals(0, spi.size(typeAA.space(), typeAA.name())); assertEquals(1, spi.size(typeAB.space(), typeAB.name())); assertEquals(1, spi.size(typeBA.space(), typeBA.name())); cacheA.put(2, aa("A", 2, "Valera", 19).build()); assertEquals(1, spi.size(typeAA.space(), typeAA.name())); assertEquals(1, spi.size(typeAB.space(), typeAB.name())); assertEquals(1, spi.size(typeBA.space(), typeBA.name())); cacheA.put(3, aa("A", 3, "Borya", 18).build()); assertEquals(2, spi.size(typeAA.space(), typeAA.name())); assertEquals(1, spi.size(typeAB.space(), typeAB.name())); assertEquals(1, spi.size(typeBA.space(), typeBA.name())); cacheA.put(4, ab(4, "Vitalya", 20, "Very Good guy").build()); assertEquals(2, spi.size(typeAA.space(), typeAA.name())); assertEquals(2, spi.size(typeAB.space(), typeAB.name())); assertEquals(1, spi.size(typeBA.space(), typeBA.name())); // Query data. Iterator<IgniteBiTuple<Integer, BinaryObjectImpl>> res = spi.queryLocalSql(typeAA.space(), "from a order by age", null, Collections.emptySet(), typeAA.name(), null, null); assertTrue(res.hasNext()); assertEquals(aa("A", 3, "Borya", 18).build(), value(res.next())); assertTrue(res.hasNext()); assertEquals(aa("A", 2, "Valera", 19).build(), value(res.next())); assertFalse(res.hasNext()); res = spi.queryLocalSql(typeAA.space(), "select aa.* from a aa order by aa.age", null, Collections.emptySet(), typeAA.name(), null, null); assertTrue(res.hasNext()); assertEquals(aa("A", 3, "Borya", 18).build(), value(res.next())); assertTrue(res.hasNext()); assertEquals(aa("A", 2, "Valera", 19).build(), value(res.next())); assertFalse(res.hasNext()); res = spi.queryLocalSql(typeAB.space(), "from b order by name", null, Collections.emptySet(), typeAB.name(), null, null); assertTrue(res.hasNext()); assertEquals(ab(1, "Vasya", 20, "Some text about Vasya goes here.").build(), value(res.next())); assertTrue(res.hasNext()); assertEquals(ab(4, "Vitalya", 20, "Very Good guy").build(), value(res.next())); assertFalse(res.hasNext()); res = spi.queryLocalSql(typeAB.space(), "select bb.* from b as bb order by bb.name", null, Collections.emptySet(), typeAB.name(), null, null); assertTrue(res.hasNext()); assertEquals(ab(1, "Vasya", 20, "Some text about Vasya goes here.").build(), value(res.next())); assertTrue(res.hasNext()); assertEquals(ab(4, "Vitalya", 20, "Very Good guy").build(), value(res.next())); assertFalse(res.hasNext()); res = spi.queryLocalSql(typeBA.space(), "from a", null, Collections.emptySet(), typeBA.name(), null, null); assertTrue(res.hasNext()); assertEquals(ba(2, "Kolya", 25, true).build(), value(res.next())); assertFalse(res.hasNext()); // Text queries Iterator<IgniteBiTuple<Integer, BinaryObjectImpl>> txtRes = spi.queryLocalText(typeAB.space(), "good", typeAB.name(), null); assertTrue(txtRes.hasNext()); assertEquals(ab(4, "Vitalya", 20, "Very Good guy").build(), value(txtRes.next())); assertFalse(txtRes.hasNext()); // Fields query GridQueryFieldsResult fieldsRes = spi.queryLocalSqlFields("A", "select a.a.name n1, a.a.age a1, b.a.name n2, " + "b.a.age a2 from a.a, b.a where a.a.id = b.a.id ", Collections.emptySet(), null, false, 0, null); String[] aliases = {"N1", "A1", "N2", "A2"}; Object[] vals = { "Valera", 19, "Kolya", 25}; IgniteSpiCloseableIterator<List<?>> it = fieldsRes.iterator(); assertTrue(it.hasNext()); List<?> fields = it.next(); assertEquals(4, fields.size()); int i = 0; for (Object f : fields) { assertEquals(aliases[i], fieldsRes.metaData().get(i).fieldName()); assertEquals(vals[i++], f); } assertFalse(it.hasNext()); // Remove cacheA.remove(2); assertEquals(1, spi.size(typeAA.space(), typeAA.name())); assertEquals(2, spi.size(typeAB.space(), typeAB.name())); assertEquals(1, spi.size(typeBA.space(), typeBA.name())); cacheB.remove(1); assertEquals(1, spi.size(typeAA.space(), typeAA.name())); assertEquals(2, spi.size(typeAB.space(), typeAB.name())); assertEquals(0, spi.size(typeBA.space(), typeBA.name())); // Unregister. spi.unregisterType(typeAA.space(), typeAA.name()); assertEquals(-1, spi.size(typeAA.space(), typeAA.name())); assertEquals(2, spi.size(typeAB.space(), typeAB.name())); assertEquals(0, spi.size(typeBA.space(), typeBA.name())); spi.unregisterType(typeAB.space(), typeAB.name()); assertEquals(-1, spi.size(typeAA.space(), typeAA.name())); assertEquals(-1, spi.size(typeAB.space(), typeAB.name())); assertEquals(0, spi.size(typeBA.space(), typeBA.name())); spi.unregisterType(typeBA.space(), typeBA.name()); assertEquals(-1, spi.size(typeAA.space(), typeAA.name())); } /** * Test long queries write explain warnings into log. * * @throws Exception If failed. */ public void testLongQueries() throws Exception { IgniteH2Indexing spi = getIndexing(); ignite0.createCache(cacheACfg()); long longQryExecTime = CacheConfiguration.DFLT_LONG_QRY_WARN_TIMEOUT; GridStringLogger log = new GridStringLogger(false, this.log); IgniteLogger oldLog = GridTestUtils.getFieldValue(spi, "log"); try { GridTestUtils.setFieldValue(spi, "log", log); String sql = "select sum(x) FROM SYSTEM_RANGE(?, ?)"; long now = U.currentTimeMillis(); long time = now; long range = 1000000L; while (now - time <= longQryExecTime * 3 / 2) { time = now; range *= 3; GridQueryFieldsResult res = spi.queryLocalSqlFields("A", sql, Arrays.<Object>asList(1, range), null, false, 0, null); assert res.iterator().hasNext(); now = U.currentTimeMillis(); } String res = log.toString(); assertTrue(res.contains("/* PUBLIC.RANGE_INDEX */")); } finally { GridTestUtils.setFieldValue(spi, "log", oldLog); } } /** * Index descriptor. */ private static class TextIndex implements GridQueryIndexDescriptor { /** */ private final Collection<String> fields; /** * @param fields Fields. */ private TextIndex(Collection<String> fields) { this.fields = Collections.unmodifiableCollection(fields); } /** {@inheritDoc} */ @Override public String name() { return null; } /** {@inheritDoc} */ @Override public Collection<String> fields() { return fields; } /** {@inheritDoc} */ @Override public boolean descending(String field) { return false; } /** {@inheritDoc} */ @Override public QueryIndexType type() { return QueryIndexType.FULLTEXT; } /** {@inheritDoc} */ @Override public int inlineSize() { return 0; } } /** * Type descriptor. */ private static class TypeDesc implements GridQueryTypeDescriptor { /** */ private final String name; /** */ private final String space; /** */ private final Map<String, Class<?>> valFields; /** */ private final GridQueryIndexDescriptor textIdx; /** * @param space Space name. * @param name Type name. * @param valFields Fields. * @param textIdx Fulltext index. */ private TypeDesc(String space, String name, Map<String, Class<?>> valFields, GridQueryIndexDescriptor textIdx) { this.name = name; this.space = space; this.valFields = Collections.unmodifiableMap(valFields); this.textIdx = textIdx; } /** {@inheritDoc} */ @Override public String affinityKey() { return null; } /** {@inheritDoc} */ @Override public String name() { return name; } /** {@inheritDoc} */ @Override public String tableName() { return null; } /** * @return Space name. */ String space() { return space; } /** {@inheritDoc} */ @Override public Map<String, Class<?>> fields() { return valFields; } /** {@inheritDoc} */ @Override public GridQueryProperty property(final String name) { return new GridQueryProperty() { /** */ @Override public Object value(Object key, Object val) throws IgniteCheckedException { return TypeDesc.this.value(name, key, val); } /** */ @Override public void setValue(Object key, Object val, Object propVal) throws IgniteCheckedException { throw new UnsupportedOperationException(); } /** */ @Override public String name() { return name; } /** */ @Override public Class<?> type() { return Object.class; } /** */ @Override public boolean key() { return false; } /** */ @Override public GridQueryProperty parent() { return null; } }; } /** {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public <T> T value(String field, Object key, Object val) throws IgniteSpiException { assert !F.isEmpty(field); assert key instanceof Integer; Map<String, T> m = (Map<String, T>)val; if (m.containsKey(field)) return m.get(field); return null; } /** {@inheritDoc} */ @SuppressWarnings("unchecked") @Override public void setValue(String field, Object key, Object val, Object propVal) throws IgniteCheckedException { assert !F.isEmpty(field); assert key instanceof Integer; Map<String, Object> m = (Map<String, Object>)val; m.put(field, propVal); } /** */ @Override public Map<String, GridQueryIndexDescriptor> indexes() { return Collections.emptyMap(); } /** */ @Override public GridQueryIndexDescriptor textIndex() { return textIdx; } /** */ @Override public Class<?> valueClass() { return Object.class; } /** */ @Override public Class<?> keyClass() { return Integer.class; } /** */ @Override public String keyTypeName() { return null; } /** */ @Override public String valueTypeName() { return null; } /** */ @Override public boolean valueTextIndex() { return textIdx == null; } /** */ @Override public int typeId() { return 0; } /** {@inheritDoc} */ @Override public String keyFieldName() { return null; } /** {@inheritDoc} */ @Override public String valueFieldName() { return null; } } /** */ private static class TestCacheObject implements KeyCacheObject { /** */ private Object val; /** */ private int part; /** * @param val Value. */ private TestCacheObject(Object val) { this.val = val; } /** {@inheritDoc} */ @Override public void onAckReceived() { // No-op. } /** {@inheritDoc} */ @Nullable @Override public <T> T value(CacheObjectContext ctx, boolean cpy) { return (T)val; } /** {@inheritDoc} */ @Override public int partition() { return part; } /** {@inheritDoc} */ @Override public void partition(int part) { this.part = part; } /** {@inheritDoc} */ @Override public byte[] valueBytes(CacheObjectContext ctx) throws IgniteCheckedException { return JdbcUtils.serialize(val, null); } /** {@inheritDoc} */ @Override public boolean putValue(ByteBuffer buf) throws IgniteCheckedException { return false; } /** {@inheritDoc} */ @Override public int putValue(long addr) throws IgniteCheckedException { return 0; } /** {@inheritDoc} */ @Override public boolean putValue(final ByteBuffer buf, final int off, final int len) throws IgniteCheckedException { return false; } /** {@inheritDoc} */ @Override public int valueBytesLength(CacheObjectContext ctx) throws IgniteCheckedException { return 0; } /** {@inheritDoc} */ @Override public byte cacheObjectType() { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public boolean isPlatformType() { return true; } /** {@inheritDoc} */ @Override public KeyCacheObject copy(int part) { return this; } /** {@inheritDoc} */ @Override public CacheObject prepareForCache(CacheObjectContext ctx) { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public void finishUnmarshal(CacheObjectContext ctx, ClassLoader ldr) throws IgniteCheckedException { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public void prepareMarshal(CacheObjectContext ctx) throws IgniteCheckedException { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public boolean writeTo(ByteBuffer buf, MessageWriter writer) { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public boolean readFrom(ByteBuffer buf, MessageReader reader) { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public short directType() { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public byte fieldsCount() { throw new UnsupportedOperationException(); } /** {@inheritDoc} */ @Override public boolean internal() { return false; } } }