/*
* 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.sql;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.cache.CacheAtomicityMode;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.CacheWriteSynchronizationMode;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.internal.marshaller.optimized.OptimizedMarshaller;
import org.apache.ignite.spi.discovery.tcp.TcpDiscoverySpi;
import org.apache.ignite.spi.discovery.tcp.ipfinder.TcpDiscoveryIpFinder;
import org.apache.ignite.spi.discovery.tcp.ipfinder.vm.TcpDiscoveryVmIpFinder;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.jetbrains.annotations.Nullable;
/**
* Abstract test framework to compare query results from h2 database instance and mixed ignite caches (replicated and
* partitioned) which have the same data models and data content.
*/
public abstract class AbstractH2CompareQueryTest extends GridCommonAbstractTest {
/** */
private static final TcpDiscoveryIpFinder IP_FINDER = new TcpDiscoveryVmIpFinder(true);
/** */
protected static Ignite ignite;
/** */
protected static final int SRVS = 4;
/** H2 db connection. */
protected static Connection conn;
/** {@inheritDoc} */
@SuppressWarnings("unchecked")
@Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception {
IgniteConfiguration c = super.getConfiguration(igniteInstanceName);
TcpDiscoverySpi disco = new TcpDiscoverySpi();
disco.setIpFinder(IP_FINDER);
c.setDiscoverySpi(disco);
c.setMarshaller(new OptimizedMarshaller(true));
return c;
}
/**
* Creates new cache configuration.
*
* @param name Cache name.
* @param mode Cache mode.
* @param clsK Key class.
* @param clsV Value class.
* @return Cache configuration.
*/
protected CacheConfiguration cacheConfiguration(String name, CacheMode mode, Class<?> clsK, Class<?> clsV) {
CacheConfiguration<?,?> cc = defaultCacheConfiguration();
cc.setName(name);
cc.setCacheMode(mode);
cc.setWriteSynchronizationMode(CacheWriteSynchronizationMode.FULL_SYNC);
cc.setAtomicityMode(CacheAtomicityMode.TRANSACTIONAL);
cc.setIndexedTypes(clsK, clsV);
return cc;
}
/**
* Creates caches instances.
*/
protected abstract void createCaches();
/** {@inheritDoc} */
@Override protected void beforeTestsStarted() throws Exception {
super.beforeTestsStarted();
ignite = startGrids(SRVS);
createCaches();
awaitPartitionMapExchange();
conn = openH2Connection(false);
initializeH2Schema();
initCacheAndDbData();
checkAllDataEquals();
}
/** {@inheritDoc} */
@Override protected void afterTestsStopped() throws Exception {
super.afterTestsStopped();
Statement st = conn.createStatement();
st.execute("DROP ALL OBJECTS");
conn.close();
stopAllGrids();
ignite = null;
}
/**
* Populate cache and h2 database with test data.
*
* @throws SQLException If failed.
*/
protected abstract void initCacheAndDbData() throws Exception;
/**
* @throws Exception If failed.
*/
protected abstract void checkAllDataEquals() throws Exception;
/**
* Initialize h2 database schema.
*
* @throws SQLException If exception.
* @return Statement.
*/
protected Statement initializeH2Schema() throws SQLException {
// All logic is moved to child classes.
return conn.createStatement();
}
/**
* Gets connection from a pool.
*
* @param autocommit {@code true} If connection should use autocommit mode.
* @return Pooled connection.
* @throws SQLException In case of error.
*/
protected Connection openH2Connection(boolean autocommit) throws SQLException {
System.setProperty("h2.serializeJavaObject", "false");
String dbName = "test";
Connection conn = DriverManager.getConnection("jdbc:h2:mem:" + dbName + ";DB_CLOSE_DELAY=-1");
conn.setAutoCommit(autocommit);
return conn;
}
/**
* Execute given sql query on h2 database and on ignite cache and compare results.
* Expected that results are not ordered.
*
* @param cache Ignite cache.
* @param sql SQL query.
* @param args SQL arguments.
* then results will compare as ordered queries.
* @return Result set after SQL query execution.
* @throws SQLException If exception.
*/
protected final List<List<?>> compareQueryRes0(IgniteCache cache, String sql, @Nullable Object... args)
throws SQLException {
return compareQueryRes0(cache, sql, args, Ordering.RANDOM);
}
/**
* Execute given sql query on h2 database and on partitioned ignite cache and compare results.
* Expected that results are ordered.
*
* @param cache Cache.
* @param sql SQL query.
* @param args SQL arguments.
* then results will compare as ordered queries.
* @return Result set after SQL query execution.
* @throws SQLException If exception.
*/
protected final List<List<?>> compareOrderedQueryRes0(IgniteCache cache, String sql, @Nullable Object... args)
throws SQLException {
return compareQueryRes0(cache, sql, args, Ordering.ORDERED);
}
/**
* Execute given sql query on h2 database and on ignite cache and compare results.
*
* @param cache Ignite cache.
* @param sql SQL query.
* @param args SQL arguments.
* @param ordering Expected ordering of SQL results. If {@link Ordering#ORDERED}
* then results will compare as ordered queries.
* @return Result set after SQL query execution.
* @throws SQLException If exception.
*/
@SuppressWarnings("unchecked")
protected static List<List<?>> compareQueryRes0(IgniteCache cache, String sql, @Nullable Object[] args,
Ordering ordering) throws SQLException {
return compareQueryRes0(cache, sql, false, args, ordering);
}
/**
* Execute given sql query on h2 database and on ignite cache and compare results.
*
* @param cache Ignite cache.
* @param sql SQL query.
* @param distrib Distributed SQL Join flag.
* @param args SQL arguments.
* @param ordering Expected ordering of SQL results. If {@link Ordering#ORDERED}
* then results will compare as ordered queries.
* @return Result set after SQL query execution.
* @throws SQLException If exception.
*/
protected static List<List<?>> compareQueryRes0(IgniteCache cache,
String sql,
boolean distrib,
@Nullable Object[] args,
Ordering ordering) throws SQLException {
return compareQueryRes0(cache, sql, distrib, false, args, ordering);
}
/**
* Execute given sql query on h2 database and on ignite cache and compare results.
*
* @param cache Ignite cache.
* @param sql SQL query.
* @param distrib Distributed SQL Join flag.
* @param enforceJoinOrder Enforce join order flag.
* @param args SQL arguments.
* @param ordering Expected ordering of SQL results. If {@link Ordering#ORDERED}
* then results will compare as ordered queries.
* @return Result set after SQL query execution.
* @throws SQLException If exception.
*/
@SuppressWarnings("unchecked")
protected static List<List<?>> compareQueryRes0(IgniteCache cache,
String sql,
boolean distrib,
boolean enforceJoinOrder,
@Nullable Object[] args,
Ordering ordering) throws SQLException {
if (args == null)
args = new Object[] {null};
List<List<?>> h2Res = executeH2Query(sql, args);
// String plan = (String)((IgniteCache<Object, Object>)cache).query(new SqlFieldsQuery("explain " + sql)
// .setArgs(args)
// .setDistributedJoins(distrib))
// .getAll().get(0).get(0);
//
// X.println("Plan : " + plan);
List<List<?>> cacheRes = cache.query(new SqlFieldsQuery(sql).
setArgs(args).
setDistributedJoins(distrib).
setEnforceJoinOrder(enforceJoinOrder)).getAll();
assertRsEquals(h2Res, cacheRes, ordering);
return h2Res;
}
/**
* Execute SQL query on h2 database.
*
* @param sql SQL query.
* @param args SQL arguments.
* @return Result of SQL query on h2 database.
* @throws SQLException If exception.
*/
private static List<List<?>> executeH2Query(String sql, Object[] args) throws SQLException {
List<List<?>> res = new ArrayList<>();
ResultSet rs = null;
try (PreparedStatement st = conn.prepareStatement(sql)) {
for (int idx = 0; idx < args.length; idx++)
st.setObject(idx + 1, args[idx]);
rs = st.executeQuery();
ResultSetMetaData meta = rs.getMetaData();
int colCnt = meta.getColumnCount();
// for (int i = 1; i <= colCnt; i++)
// X.print(meta.getColumnLabel(i) + " ");
//
// X.println();
while (rs.next()) {
List<Object> row = new ArrayList<>(colCnt);
for (int i = 1; i <= colCnt; i++)
row.add(rs.getObject(i));
res.add(row);
}
}
finally {
U.closeQuiet(rs);
}
return res;
}
/**
* Assert equals of result sets according to expected ordering.
*
* @param rs1 Expected result set.
* @param rs2 Actual result set.
* @param ordering Expected ordering of SQL results. If {@link Ordering#ORDERED}
* then results will compare as ordered queries.
*/
private static void assertRsEquals(List<List<?>> rs1, List<List<?>> rs2, Ordering ordering) {
assertEquals("Rows count has to be equal.", rs1.size(), rs2.size());
switch (ordering){
case ORDERED:
for (int rowNum = 0; rowNum < rs1.size(); rowNum++) {
List<?> row1 = rs1.get(rowNum);
List<?> row2 = rs2.get(rowNum);
assertEquals("Columns count have to be equal.", row1.size(), row2.size());
for (int colNum = 0; colNum < row1.size(); colNum++)
assertEquals("Row=" + rowNum + ", column=" + colNum, row1.get(colNum), row2.get(colNum));
}
break;
case RANDOM:
TreeMap<String, Integer> rowsWithCnt1 = extractUniqueRowsWithCounts(rs1);
TreeMap<String, Integer> rowsWithCnt2 = extractUniqueRowsWithCounts(rs2);
assertEquals("Unique rows count has to be equal.", rowsWithCnt1.size(), rowsWithCnt2.size());
// X.println("Result size: " + rowsWithCnt1.size());
Iterator<Map.Entry<String,Integer>> iter1 = rowsWithCnt1.entrySet().iterator();
Iterator<Map.Entry<String,Integer>> iter2 = rowsWithCnt2.entrySet().iterator();
int uSize = rowsWithCnt1.size();
for (int i = 0;; i++) {
if (!iter1.hasNext()) {
assertFalse(iter2.hasNext());
break;
}
assertTrue(iter2.hasNext());
Map.Entry<String, Integer> e1 = iter1.next();
Map.Entry<String, Integer> e2 = iter2.next();
assertEquals("Key " + i + " of " + uSize, e1.getKey(), e2.getKey());
assertEquals("Count " + i + " of " + uSize, e1.getValue(), e2.getValue());
}
break;
default:
throw new IllegalStateException();
}
}
/**
* @param rs Result set.
* @return Map of unique rows at the result set to number of occuriances at the result set.
*/
private static TreeMap<String, Integer> extractUniqueRowsWithCounts(Iterable<List<?>> rs) {
TreeMap<String, Integer> res = new TreeMap<>();
for (List<?> row : rs) {
String rowStr = row.toString();
Integer cnt = res.get(rowStr);
if (cnt == null)
cnt = 0;
res.put(rowStr, cnt + 1);
}
return res;
}
/**
* Ordering type.
*/
protected enum Ordering {
/** Random. */
RANDOM,
/** Ordered. */
ORDERED
}
}