/*
* 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.opt;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.internal.processors.query.h2.database.H2PkHashIndex;
import org.apache.ignite.internal.processors.query.h2.database.H2RowFactory;
import org.apache.ignite.internal.util.lang.GridCursor;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
import org.h2.Driver;
import org.h2.index.Cursor;
import org.h2.index.Index;
import org.h2.result.Row;
import org.h2.result.SearchRow;
import org.h2.result.SortOrder;
import org.h2.table.IndexColumn;
import org.h2.value.ValueLong;
import org.h2.value.ValueString;
import org.h2.value.ValueTimestamp;
import org.h2.value.ValueUuid;
import org.jetbrains.annotations.Nullable;
/**
* Tests H2 Table.
*/
@SuppressWarnings({"TypeMayBeWeakened", "FieldAccessedSynchronizedAndUnsynchronized"})
public class GridH2TableSelfTest extends GridCommonAbstractTest {
/** */
private static final long MAX_X = 2000;
/** */
private static final String DB_URL = "jdbc:h2:mem:gg_table_engine;MULTI_THREADED=1;OPTIMIZE_REUSE_RESULTS=0;" +
"QUERY_CACHE_SIZE=0;RECOMPILE_ALWAYS=1";
/** */
private static final String CREATE_TABLE_SQL = "CREATE TABLE T(ID UUID, T TIMESTAMP, STR VARCHAR, X BIGINT)";
/** */
private static final String PK_NAME = "__GG_PK_";
/** Hash. */
private static final String HASH = "__GG_HASH";
/** */
private static final String STR_IDX_NAME = "__GG_IDX_";
/** */
private static final String NON_UNIQUE_IDX_NAME = "__GG_IDX_";
/** */
private static final String SCAN_IDX_NAME = GridH2PrimaryScanIndex.SCAN_INDEX_NAME_SUFFIX;
/** */
private Connection conn;
/** */
private GridH2Table tbl;
/** {@inheritDoc} */
@Override protected void beforeTest() throws Exception {
// TODO: IGNITE-4994: Restore mock.
// Driver.load();
//
// conn = DriverManager.getConnection(DB_URL);
//
// tbl = GridH2Table.Engine.createTable(conn, CREATE_TABLE_SQL, null, new GridH2Table.IndexesFactory() {
// @Override public void onTableCreated(GridH2Table tbl) {
// // No-op.
// }
//
// @Override public H2RowFactory createRowFactory(GridH2Table tbl) {
// return null;
// }
//
// @Override public ArrayList<Index> createIndexes(GridH2Table tbl) {
// ArrayList<Index> idxs = new ArrayList<>();
//
// IndexColumn id = tbl.indexColumn(0, SortOrder.ASCENDING);
// IndexColumn t = tbl.indexColumn(1, SortOrder.ASCENDING);
// IndexColumn str = tbl.indexColumn(2, SortOrder.DESCENDING);
// IndexColumn x = tbl.indexColumn(3, SortOrder.DESCENDING);
//
// idxs.add(new H2PkHashIndex(null, tbl, HASH, F.asList(id)));
// idxs.add(new GridH2TreeIndex(PK_NAME, tbl, true, F.asList(id)));
// idxs.add(new GridH2TreeIndex(NON_UNIQUE_IDX_NAME, tbl, false, F.asList(x, t, id)));
// idxs.add(new GridH2TreeIndex(STR_IDX_NAME, tbl, false, F.asList(str, id)));
//
// return idxs;
// }
// }, null);
}
/** {@inheritDoc} */
@Override protected void afterTest() throws Exception {
conn.close();
conn = null;
tbl = null;
}
/**
* @param id Id.
* @param t Timestamp.
* @param str String.
* @param x X.
* @return New row.
*/
private GridH2Row row(UUID id, long t, String str, long x) {
return GridH2RowFactory.create(
ValueUuid.get(id.getMostSignificantBits(), id.getLeastSignificantBits()),
ValueTimestamp.get(new Timestamp(t)),
ValueString.get(str),
ValueLong.get(x));
}
/**
* Simple table test.
*
* @throws Exception If failed.
*/
public void testTable() throws Exception {
// Test insert.
long x = MAX_X;
Random rnd = new Random();
while(x-- > 0) {
UUID id = UUID.randomUUID();
GridH2Row row = row(id, System.currentTimeMillis(), rnd.nextBoolean() ? id.toString() :
UUID.randomUUID().toString(), rnd.nextInt(100));
tbl.doUpdate(row, false);
}
assertEquals(MAX_X, tbl.getRowCountApproximation());
assertEquals(MAX_X, tbl.getRowCount(null));
for (GridH2IndexBase idx : tbl.indexes()) {
assertEquals(MAX_X, idx.getRowCountApproximation());
assertEquals(MAX_X, idx.getRowCount(null));
}
// Check correct rows order.
checkOrdered((GridH2TreeIndex)tbl.indexes().get(0), new Comparator<SearchRow>() {
@Override public int compare(SearchRow o1, SearchRow o2) {
UUID id1 = (UUID)o1.getValue(0).getObject();
UUID id2 = (UUID)o2.getValue(0).getObject();
return id1.compareTo(id2);
}
});
checkOrdered((GridH2TreeIndex)tbl.indexes().get(1), new Comparator<SearchRow>() {
@Override public int compare(SearchRow o1, SearchRow o2) {
Long x1 = (Long)o1.getValue(3).getObject();
Long x2 = (Long)o2.getValue(3).getObject();
int c = x2.compareTo(x1);
if (c != 0)
return c;
Timestamp t1 = (Timestamp)o1.getValue(1).getObject();
Timestamp t2 = (Timestamp)o2.getValue(1).getObject();
return t1.compareTo(t2);
}
});
checkOrdered((GridH2TreeIndex)tbl.indexes().get(2), new Comparator<SearchRow>() {
@Override public int compare(SearchRow o1, SearchRow o2) {
String s1 = (String)o1.getValue(2).getObject();
String s2 = (String)o2.getValue(2).getObject();
return s2.compareTo(s1);
}
});
// Indexes data consistency.
ArrayList<? extends Index> idxs = tbl.indexes();
checkIndexesConsistent((ArrayList<Index>)idxs, null);
// Check unique index.
UUID id = UUID.randomUUID();
UUID id2 = UUID.randomUUID();
assertTrue(tbl.doUpdate(row(id, System.currentTimeMillis(), id.toString(), rnd.nextInt(100)), false));
assertTrue(tbl.doUpdate(row(id2, System.currentTimeMillis(), id2.toString(), rnd.nextInt(100)), false));
// Check index selection.
checkQueryPlan(conn, "SELECT * FROM T", SCAN_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE ID IS NULL", PK_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE ID = RANDOM_UUID()", PK_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE ID > RANDOM_UUID()", PK_NAME);
checkQueryPlan(conn, "SELECT * FROM T ORDER BY ID", PK_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE STR IS NULL", STR_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE STR = 'aaaa'", STR_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE STR > 'aaaa'", STR_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T ORDER BY STR DESC", STR_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE X IS NULL", NON_UNIQUE_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE X = 10000", NON_UNIQUE_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T WHERE X > 10000", NON_UNIQUE_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T ORDER BY X DESC", NON_UNIQUE_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T ORDER BY X DESC, T", NON_UNIQUE_IDX_NAME);
checkQueryPlan(conn, "SELECT * FROM T ORDER BY T, X DESC", SCAN_IDX_NAME);
// Simple queries.
Statement s = conn.createStatement();
ResultSet rs = s.executeQuery("select id from t where x between 0 and 100");
int i = 0;
while (rs.next())
i++;
assertEquals(MAX_X + 2, i);
// -----
rs = s.executeQuery("select id from t where t is not null");
i = 0;
while (rs.next())
i++;
assertEquals(MAX_X + 2, i);
// ----
int cnt = 10 + rnd.nextInt(25);
long t = System.currentTimeMillis();
for (i = 0; i < cnt; i++) {
id = UUID.randomUUID();
assertTrue(tbl.doUpdate(row(id, t, id.toString(), 51), false));
}
rs = s.executeQuery("select x, id from t where x = 51 limit " + cnt);
i = 0;
while (rs.next()) {
assertEquals(51, rs.getInt(1));
i++;
}
assertEquals(cnt, i);
}
/**
* @throws Exception If failed.
*/
public void testRangeQuery() throws Exception {
int rows = 3000;
int xs = 37;
long t = System.currentTimeMillis();
Random rnd = new Random();
for (int i = 0 ; i < rows; i++) {
UUID id = UUID.randomUUID();
GridH2Row row = row(id, t++, id.toString(), rnd.nextInt(xs));
assertTrue(tbl.doUpdate(row, false));
}
PreparedStatement ps = conn.prepareStatement("select count(*) from t where x = ?");
int cnt = 0;
for (int x = 0; x < xs; x++) {
ps.setInt(1, x);
ResultSet rs = ps.executeQuery();
assertTrue(rs.next());
cnt += rs.getInt(1);
}
assertEquals(rows, cnt);
}
/**
* @throws Exception If failed.
*/
public void testDataLoss() throws Exception {
final int threads = 37;
final int iterations = 15000;
final AtomicInteger cntr = new AtomicInteger();
final UUID[] ids = new UUID[threads * iterations];
for (int i = 0; i < ids.length; i++)
ids[i] = UUID.randomUUID();
final long t = System.currentTimeMillis();
final AtomicInteger deleted = new AtomicInteger();
multithreaded(new Callable<Void>() {
@Override public Void call() throws Exception {
Random rnd = new Random();
int offset = cntr.getAndIncrement() * iterations;
synchronized (ids[offset]) {
for (int i = 0; i < iterations; i++) {
UUID id = ids[offset + i];
int x = rnd.nextInt(50);
GridH2Row row = row(id, t, id.toString(), x);
assertTrue(tbl.doUpdate(row, false));
}
}
offset = (offset + iterations) % ids.length;
synchronized (ids[offset]) {
for (int i = 0; i < iterations; i += 2) {
UUID id = ids[offset + i];
int x = rnd.nextInt(50);
GridH2Row row = row(id, t, id.toString(), x);
if (tbl.doUpdate(row, true))
deleted.incrementAndGet();
}
}
return null;
}
}, threads);
assertTrue(deleted.get() > 0);
PreparedStatement p = conn.prepareStatement("select count(*) from t where id = ?");
for (int i = 1; i < ids.length; i += 2) {
p.setObject(1, ids[i]);
ResultSet rs = p.executeQuery();
assertTrue(rs.next());
assertEquals(1, rs.getInt(1));
}
Statement s = conn.createStatement();
ResultSet rs = s.executeQuery("select count(*) from t");
assertTrue(rs.next());
assertEquals(ids.length - deleted.get(), rs.getInt(1));
}
/**
* @throws Exception If failed.
*/
public void testIndexFindFirstOrLast() throws Exception {
Index index = tbl.getIndexes().get(2);
assertTrue(index instanceof GridH2TreeIndex);
assertTrue(index.canGetFirstOrLast());
//find first on empty data
Cursor cursor = index.findFirstOrLast(null, true);
assertFalse(cursor.next());
assertNull(cursor.get());
//find last on empty data
cursor = index.findFirstOrLast(null, false);
assertFalse(cursor.next());
assertNull(cursor.get());
//fill with data
int rows = 100;
long t = System.currentTimeMillis();
Random rnd = new Random();
UUID min = null;
UUID max = null;
for (int i = 0 ; i < rows; i++) {
UUID id = UUID.randomUUID();
if (min == null || id.compareTo(min) < 0)
min = id;
if (max == null || id.compareTo(max) > 0)
max = id;
GridH2Row row = row(id, t++, id.toString(), rnd.nextInt(100));
((GridH2TreeIndex)index).put(row);
}
//find first
cursor = index.findFirstOrLast(null, true);
assertTrue(cursor.next());
assertEquals(min, cursor.get().getValue(0).getObject());
assertFalse(cursor.next());
//find last
cursor = index.findFirstOrLast(null, false);
assertTrue(cursor.next());
assertEquals(max, cursor.get().getValue(0).getObject());
assertFalse(cursor.next());
}
/**
* Check query plan to correctly select index.
*
* @param conn Connection.
* @param sql Select.
* @param search Search token in result.
* @throws SQLException If failed.
*/
private void checkQueryPlan(Connection conn, String sql, String search) throws SQLException {
try (Statement s = conn.createStatement()) {
try (ResultSet r = s.executeQuery("EXPLAIN ANALYZE " + sql)) {
assertTrue(r.next());
String plan = r.getString(1);
assertTrue("Execution plan for '" + sql + "' query should contain '" + search + "'",
plan.contains(search));
}
}
}
/**
* @param idxs Indexes.
* @param rowSet Rows.
* @return Rows.
*/
private Set<Row> checkIndexesConsistent(ArrayList<Index> idxs, @Nullable Set<Row> rowSet) throws IgniteCheckedException {
for (Index idx : idxs) {
if (!(idx instanceof GridH2TreeIndex))
continue;
Set<Row> set = new HashSet<>();
GridCursor<GridH2Row> cursor = ((GridH2TreeIndex)idx).rows();
while(cursor.next())
assertTrue(set.add(cursor.get()));
//((GridH2SnapTreeSet)((GridH2Index)idx).tree).print();
if (rowSet == null || rowSet.isEmpty())
rowSet = set;
else
assertEquals(rowSet, set);
}
return rowSet;
}
/**
* @param idxs Indexes list.
*/
private void checkOrdered(ArrayList<Index> idxs) throws IgniteCheckedException {
for (Index idx : idxs) {
if (!(idx instanceof GridH2TreeIndex))
continue;
GridH2TreeIndex h2Idx = (GridH2TreeIndex)idx;
checkOrdered(h2Idx, h2Idx);
}
}
/**
* @param idx Index.
* @param cmp Comparator.
*/
private void checkOrdered(GridH2TreeIndex idx, Comparator<? super GridH2Row> cmp) throws IgniteCheckedException {
GridCursor<GridH2Row> cursor = idx.rows();
GridH2Row min = null;
while (cursor.next()) {
GridH2Row row = cursor.get();
System.out.println(row);
assertNotNull(row);
assertFalse("Incorrect row order in index: " + idx + "\n min: " + min + "\n row: " + row,
min != null && cmp.compare(min, row) > 0);
min = row;
}
}
}