blob: b250d71abb9015a511911b53c4d7244a7d8254b6 [file] [log] [blame]
/*
* 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.jdbc.thin;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
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.Time;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.ignite.IgniteCache;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.cache.CacheMode;
import org.apache.ignite.cache.CacheWriteSynchronizationMode;
import org.apache.ignite.cache.QueryEntity;
import org.apache.ignite.cache.affinity.rendezvous.RendezvousAffinityFunction;
import org.apache.ignite.configuration.CacheConfiguration;
import org.apache.ignite.internal.IgniteEx;
import org.apache.ignite.internal.processors.cache.GridCacheDataTypesCoverageTest;
import org.apache.ignite.internal.util.lang.GridAbsPredicateX;
import org.apache.ignite.internal.util.lang.GridClosureException;
import org.apache.ignite.lang.IgniteBiTuple;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.apache.ignite.testframework.GridTestUtils.waitForCondition;
/**
* Data types coverage for basic cache-put-jdbc-thin-retrieve operations.
*/
public class JdbcThinCacheToJdbcDataTypesCoverageTest extends GridCacheDataTypesCoverageTest {
/** Signals that tests should start in affinity awareness mode. */
public static boolean affinityAwareness;
/**
* Please pay attention, that it's not a comprehensive java to sql data types mapping,
* but a holder of specific mappings for given test only.
*
* Based on:
* https://docs.oracle.com/javase/1.5.0/docs/guide/jdbc/getstart/mapping.html
*/
private static final Map<Class, IgniteBiTuple<Integer, Class>> javaClsToSqlTypeMap;
static {
Map<Class, IgniteBiTuple<Integer, Class>> innerMap = new HashMap<>();
innerMap.put(Boolean.class, new IgniteBiTuple<>(Types.BOOLEAN, Boolean.class));
innerMap.put(Integer.class, new IgniteBiTuple<>(Types.INTEGER, Integer.class));
innerMap.put(Byte.class, new IgniteBiTuple<>(Types.TINYINT, Byte.class));
innerMap.put(Short.class, new IgniteBiTuple<>(Types.SMALLINT, Short.class));
innerMap.put(Long.class, new IgniteBiTuple<>(Types.BIGINT, Long.class));
innerMap.put(BigDecimal.class, new IgniteBiTuple<>(Types.DECIMAL, BigDecimal.class));
innerMap.put(Double.class, new IgniteBiTuple<>(Types.DOUBLE, Double.class));
innerMap.put(Float.class, new IgniteBiTuple<>(Types.FLOAT, Float.class));
innerMap.put(java.sql.Time.class, new IgniteBiTuple<>(Types.TIME, java.sql.Time.class));
innerMap.put(java.sql.Date.class, new IgniteBiTuple<>(Types.DATE, java.sql.Date.class));
innerMap.put(java.sql.Timestamp.class, new IgniteBiTuple<>(Types.TIMESTAMP, java.sql.Timestamp.class));
innerMap.put(String.class, new IgniteBiTuple<>(Types.VARCHAR, String.class));
innerMap.put(Character.class, new IgniteBiTuple<>(Types.OTHER, Character.class));
innerMap.put(byte[].class, new IgniteBiTuple<>(Types.BINARY, byte[].class));
innerMap.put(java.util.Date.class, new IgniteBiTuple<>(Types.TIMESTAMP, java.sql.Timestamp.class));
innerMap.put(LocalDate.class, new IgniteBiTuple<>(Types.DATE, java.sql.Date.class));
innerMap.put(LocalDateTime.class, new IgniteBiTuple<>(Types.TIMESTAMP, java.sql.Timestamp.class));
innerMap.put(LocalTime.class, new IgniteBiTuple<>(Types.TIME, java.sql.Time.class));
javaClsToSqlTypeMap = Collections.unmodifiableMap(innerMap);
}
/** As stmt parameter. */
private boolean asPreparedParam;
/** URL. */
private String url = affinityAwareness ?
"jdbc:ignite:thin://127.0.0.1:10800..10802?affinityAwareness=true" :
"jdbc:ignite:thin://127.0.0.1?affinityAwareness=false";
/** Connection. */
private Connection conn;
/** Expected ex. */
@Rule
public ExpectedException expEx = ExpectedException.none();
/** @inheritDoc */
@Before
@Override public void init() throws Exception {
super.init();
asPreparedParam = false;
}
/**
* Cleanup.
*
* @throws Exception If Failed.
*/
@After
public void tearDown() throws Exception {
if (conn != null && !conn.isClosed()) {
conn.close();
assert conn.isClosed();
}
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testFloatDataType() throws Exception {
checkBasicCacheOperations(
Float.MIN_VALUE,
Float.MAX_VALUE,
new Quoted(Float.NaN),
new Quoted(Float.NEGATIVE_INFINITY),
new Quoted(Float.POSITIVE_INFINITY),
0F,
0.0F,
1F,
1.1F);
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testDoubleDataType() throws Exception {
checkBasicCacheOperations(
Double.MIN_VALUE,
Double.MAX_VALUE,
new Quoted(Double.NaN),
new Quoted(Double.NEGATIVE_INFINITY),
new Quoted(Double.POSITIVE_INFINITY),
0D,
0.0D,
1D,
1.1D);
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-13231")
@Test
@Override public void testCharacterDataType() throws Exception {
checkBasicCacheOperations(
new Quoted('a'),
new Quoted('A'));
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testStringDataType() throws Exception {
checkBasicCacheOperations(
new Quoted("aAbB"),
new Quoted(""));
}
/**
* @throws Exception If failed.
*/
@SuppressWarnings("ZeroLengthArrayAllocation")
@Test
@Override public void testByteArrayDataType() throws Exception {
checkBasicCacheOperations(
new ByteArrayed(new byte[] {}),
new ByteArrayed(new byte[] {1, 2, 3}),
new ByteArrayed(new byte[] {3, 2, 1}));
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-13232")
@Test
@Override public void testObjectArrayDataType() throws Exception {
super.testObjectArrayDataType();
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testListDataType() throws Exception {
asPreparedParam = true;
checkBasicCacheOperations(new ArrayList<>());
checkBasicCacheOperations((Serializable)Collections.singletonList("Aaa"));
checkBasicCacheOperations((Serializable)Arrays.asList("String", Boolean.TRUE, 'A', 1));
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testSetDataType() throws Exception {
asPreparedParam = true;
checkBasicCacheOperations(new HashSet<>());
checkBasicCacheOperations((Serializable)Collections.singleton("Aaa"));
checkBasicCacheOperations((Serializable)Arrays.asList("String", Boolean.TRUE, 'A', 1));
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testObjectBasedOnPrimitivesDataType() throws Exception {
asPreparedParam = true;
super.testObjectBasedOnPrimitivesDataType();
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testObjectBasedOnPrimitivesAndCollectionsDataType() throws Exception {
asPreparedParam = true;
super.testObjectBasedOnPrimitivesAndCollectionsDataType();
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testObjectBasedOnPrimitivesAndCollectionsAndNestedObjectsDataType() throws Exception {
asPreparedParam = true;
super.testObjectBasedOnPrimitivesAndCollectionsAndNestedObjectsDataType();
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-13233")
@Test
@Override public void testDateDataType() throws Exception {
checkBasicCacheOperations(
(Object d) -> new Timestamp(((java.util.Date)d).getTime()),
new Dated(new java.util.Date()),
new Dated(new java.util.Date(Long.MIN_VALUE)),
new Dated(new java.util.Date(Long.MAX_VALUE)));
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-12326")
@Test
@Override public void testSqlDateDataType() throws Exception {
super.testSqlDateDataType();
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-13238")
@Test
@Override public void testInstantDataType() throws Exception {
asPreparedParam = true;
super.testInstantDataType();
}
/** {@inheritDoc} */
@Test
@Override public void testCalendarDataType() throws Exception {
asPreparedParam = true;
super.testCalendarDataType();
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-12312, https://issues.apache.org/jira/browse/IGNITE-12326")
@Test
@Override public void testLocalDateDataType() throws Exception {
checkBasicCacheOperations(
(Object ld) -> java.sql.Date.valueOf((LocalDate)ld),
new Dated(LocalDate.of(2015, 2, 20)),
new Dated(LocalDate.now().plusDays(1)));
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-12312")
@Test
@Override public void testLocalDateTimeDataType() throws Exception {
checkBasicCacheOperations(
(Object ldt) -> Timestamp.valueOf((LocalDateTime)ldt),
new Dated(LocalDateTime.of(2015, 2, 20, 9, 4, 30)),
new Dated(LocalDateTime.now().plusDays(1)));
}
/**
* @throws Exception If failed.
*/
@Ignore("https://issues.apache.org/jira/browse/IGNITE-12312")
@Test
@Override public void testLocalTimeDataType() throws Exception {
checkBasicCacheOperations(
(Object lt) -> Time.valueOf((LocalTime)lt),
new Timed(LocalTime.of(9, 4, 40)),
new Timed(LocalTime.now()));
}
/**
* @throws Exception If failed.
*/
@Test
public void testSQLTimestampDataType() throws Exception {
checkBasicCacheOperations(
new Dated(Timestamp.valueOf(LocalDateTime.now()), "yyyy-MM-dd HH:mm:ss.SSS"),
new Dated(Timestamp.valueOf(LocalDateTime.now()), "yyyy-MM-dd HH:mm:ss.SSSS"),
new Dated(Timestamp.valueOf(LocalDateTime.now()), "yyyy-MM-dd HH:mm:ss.SSSSSS"));
}
/**
* @throws Exception If failed.
*/
@Test
@Override public void testBigIntegerDataType() throws Exception {
asPreparedParam = true;
super.testBigIntegerDataType();
}
/**
* Verification that jdbc thin SELECT and SELECT with where clause works correctly
* in context of cache to jdbc data types convertion.
*
* @param valsToCheck Array of values to check.
* @throws Exception If failed.
*/
@Override protected void checkBasicCacheOperations(Serializable... valsToCheck) throws Exception {
checkBasicCacheOperations((Object o) -> o, valsToCheck);
}
/**
* Verification that jdbc thin SELECT and SELECT with where clause works correctly
* in context of cache to jdbc data types convertion.
*
* @param converterToSqlExpVal Converter to expected value.
* @param valsToCheck Array of values to check.
* @throws Exception If failed.
*/
@SuppressWarnings("unchecked")
private void checkBasicCacheOperations(Function converterToSqlExpVal, Serializable... valsToCheck) throws Exception {
assert valsToCheck.length > 0;
Object originalValItem = valsToCheck[0] instanceof SqlStrConvertedValHolder ?
((SqlStrConvertedValHolder)valsToCheck[0]).originalVal() :
valsToCheck[0];
// In case of BigDecimal, cache internally changes bitLength of BigDecimal's intValue,
// so that EqualsBuilder.reflectionEquals returns false.
// As a result in case of BigDecimal data type Objects.equals is used.
// Same is about BigInteger.
BiFunction<Object, Object, Boolean> equalsProcessor =
originalValItem instanceof BigDecimal || originalValItem instanceof BigInteger ?
Objects::equals :
(lhs, rhs) -> EqualsBuilder.reflectionEquals(
lhs, rhs, false, lhs.getClass(), true);
String uuidPostfix = UUID.randomUUID().toString().replaceAll("-", "_");
String cacheName = "cache" + uuidPostfix;
String tblName = "table" + uuidPostfix;
Class<?> dataType = originalValItem.getClass();
IgniteEx ignite =
(cacheMode == CacheMode.LOCAL || writeSyncMode == CacheWriteSynchronizationMode.PRIMARY_SYNC) ?
grid(0) :
grid(new Random().nextInt(NODES_CNT));
IgniteCache<Object, Object> cache = ignite.createCache(
new CacheConfiguration<>()
.setName(cacheName)
.setAtomicityMode(atomicityMode)
.setCacheMode(cacheMode)
.setExpiryPolicyFactory(ttlFactory)
.setBackups(backups)
.setEvictionPolicyFactory(evictionFactory)
.setOnheapCacheEnabled(evictionFactory != null || onheapCacheEnabled)
.setWriteSynchronizationMode(writeSyncMode)
.setAffinity(new RendezvousAffinityFunction(false, PARTITIONS_CNT))
.setQueryEntities(Collections.singletonList(
new QueryEntity(dataType, dataType).setTableName(tblName))));
Map<Serializable, Serializable> keyValMap = new HashMap<>();
for (int i = 0; i < valsToCheck.length; i++)
keyValMap.put(valsToCheck[i], valsToCheck[valsToCheck.length - i - 1]);
for (Map.Entry<Serializable, Serializable> keyValEntry : keyValMap.entrySet()) {
Object originalKey;
Object sqlStrKey;
if (keyValEntry.getKey() instanceof SqlStrConvertedValHolder) {
originalKey = ((SqlStrConvertedValHolder)keyValEntry.getKey()).originalVal();
sqlStrKey = ((SqlStrConvertedValHolder)keyValEntry.getKey()).sqlStrVal();
}
else {
originalKey = keyValEntry.getKey();
sqlStrKey = keyValEntry.getKey();
}
Object originalVal = keyValEntry.getValue() instanceof SqlStrConvertedValHolder ?
((SqlStrConvertedValHolder)keyValEntry.getValue()).originalVal() :
keyValEntry.getValue();
// Populate cache with data.
cache.put(originalKey, originalVal);
// Check SELECT query.
try (PreparedStatement stmt = prepareStatement(cacheName, "SELECT * FROM " + tblName)) {
checkQuery(converterToSqlExpVal, equalsProcessor, originalVal.getClass(), originalKey, originalVal, stmt);
}
// Check SELECT query with where clause.
if (asPreparedParam) {
try (PreparedStatement stmt = prepareStatement(cacheName, "SELECT * FROM " + tblName + " WHERE _key = ?")) {
if (originalKey.getClass().isArray())
stmt.setArray(1, conn.createArrayOf("OTHER", (Object[])originalKey));
else
stmt.setObject(1, originalKey);
checkQuery(converterToSqlExpVal, equalsProcessor, originalVal.getClass(), originalKey, originalVal, stmt);
}
}
else {
try (PreparedStatement stmt = prepareStatement(cacheName, "SELECT * FROM " + tblName
+ " WHERE _key = " + sqlStrKey)) {
checkQuery(converterToSqlExpVal, equalsProcessor, originalVal.getClass(), originalKey, originalVal, stmt);
}
}
// Check DELETE.
checkDelete(tblName, cache, originalKey);
}
}
/**
* Prepare jdbc thin connection and statement.
*
* @param cacheName Cache name.
* @throws SQLException If Failed.
*/
private PreparedStatement prepareStatement(String cacheName, String qry) throws SQLException {
conn = DriverManager.getConnection(url);
conn.setSchema('"' + cacheName + '"');
return conn.prepareStatement(qry);
}
/**
* Remove record form cache via cache API, and verify that data was successfully removed with sql jdbc query.
*
* @param tblName Table name.
* @param cache Ignite cache.
* @param originalKey Original Key.
* @throws SQLException If Failed.
*/
private void checkDelete(String tblName, IgniteCache<Object, Object> cache,
Object originalKey) throws IgniteCheckedException, SQLException {
// Delete from cache.
cache.remove(originalKey);
try (PreparedStatement stmt = prepareStatement(cache.getName(), "SELECT * FROM " + tblName)) {
try {
if (writeSyncMode == CacheWriteSynchronizationMode.FULL_ASYNC &&
!waitForCondition(new GridAbsPredicateX() {
@Override public boolean applyx() throws IgniteCheckedException {
try {
return !stmt.executeQuery().next();
}
catch (SQLException e) {
throw new IgniteCheckedException(e);
}
}
},
TIMEOUT_FOR_KEY_RETRIEVAL_IN_FULL_ASYNC_MODE))
fail("Deleted data are still retrievable via SELECT.");
}
catch (GridClosureException e) {
throw (SQLException)e.getCause().getCause();
}
ResultSet rs = stmt.executeQuery();
assertNotNull(rs);
assertFalse("Unexpected rows count.", rs.next());
}
}
/**
*
* @param converterToSqlExpVal Function that converts expected key/value in conformity with a format of jdbc
* return key/value.
* @param equalsProcessor Equals processor that process equality check of expected and got keys/values.
* @param dataType Data type.
* @param originalKey Original key.
* @param originalVal Original value.
* @param stmt Prepared statement.
* @throws IgniteCheckedException If failed.
* @throws SQLException If failed.
*/
@SuppressWarnings("unchecked")
private void checkQuery(Function converterToSqlExpVal, BiFunction<Object, Object, Boolean> equalsProcessor,
Class<?> dataType, Object originalKey, Object originalVal, PreparedStatement stmt)
throws IgniteCheckedException, SQLException {
try {
if (writeSyncMode == CacheWriteSynchronizationMode.FULL_ASYNC &&
!waitForCondition(new GridAbsPredicateX() {
@Override public boolean applyx() throws IgniteCheckedException {
try {
return stmt.executeQuery().next();
}
catch (SQLException e) {
throw new IgniteCheckedException(e);
}
}
}, TIMEOUT_FOR_KEY_RETRIEVAL_IN_FULL_ASYNC_MODE))
fail("Unable to retrieve data via SELECT.");
}
catch (GridClosureException e) {
throw (SQLException)e.getCause().getCause();
}
ResultSet rs = stmt.executeQuery();
assertNotNull(rs);
ResultSetMetaData meta = rs.getMetaData();
assertNotNull(meta);
IgniteBiTuple<Integer, Class> typeClsPair = javaClsToSqlTypeMap
.getOrDefault(dataType, new IgniteBiTuple<>(Types.OTHER, originalVal.getClass()));
int metaType = typeClsPair.get1();
Object expJavaDataType = typeClsPair.get2();
assertEquals("Unexpected metadata data type name for key.", metaType, meta.getColumnType(1));
assertEquals("Unexpected metadata data type name for value.", metaType, meta.getColumnType(2));
assertEquals("Unexpected columns count.", 2, meta.getColumnCount());
int cnt = 0;
while (rs.next()) {
Object gotKey = rs.getObject(1);
Object gotVal = rs.getObject(2);
assertEquals("Unexpected key data type.", expJavaDataType, gotKey.getClass());
assertEquals("Unexpected value data type.", expJavaDataType, gotVal.getClass());
assertTrue(equalsProcessor.apply(converterToSqlExpVal.apply(originalKey), gotKey));
assertTrue(equalsProcessor.apply(converterToSqlExpVal.apply(originalVal), gotVal));
cnt++;
}
assertEquals("Unexpected rows count.", 1, cnt);
}
}