/* * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, * and the EPL 1.0 (http://h2database.com/html/license.html). * Initial Developer: H2 Group */ package org.h2.test.db; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; import org.h2.api.TableEngine; import org.h2.command.ddl.CreateTableData; import org.h2.engine.Session; import org.h2.expression.Expression; import org.h2.index.BaseIndex; import org.h2.index.Cursor; import org.h2.index.Index; import org.h2.index.IndexLookupBatch; import org.h2.index.IndexType; import org.h2.index.SingleRowCursor; import org.h2.jdbc.JdbcConnection; import org.h2.message.DbException; import org.h2.result.Row; import org.h2.result.SearchRow; import org.h2.result.SortOrder; import org.h2.table.Column; import org.h2.table.IndexColumn; import org.h2.table.SubQueryInfo; import org.h2.table.Table; import org.h2.table.TableBase; import org.h2.table.TableFilter; import org.h2.table.TableType; import org.h2.test.TestBase; import org.h2.util.DoneFuture; import org.h2.util.New; import org.h2.value.Value; import org.h2.value.ValueInt; import org.h2.value.ValueNull; import org.h2.value.ValueString; /** * The class for external table engines mechanism testing. * * @author Sergi Vladykin */ public class TestTableEngines extends TestBase { /** * Run just this test. * * @param a ignored */ public static void main(String[] a) throws Exception { TestBase.createCaller().init().test(); } @Override public void test() throws Exception { testQueryExpressionFlag(); testSubQueryInfo(); testEarlyFilter(); testEngineParams(); testSchemaEngineParams(); testSimpleQuery(); testMultiColumnTreeSetIndex(); testBatchedJoin(); } private void testEarlyFilter() throws SQLException { deleteDb("tableEngine"); Connection conn = getConnection("tableEngine;EARLY_FILTER=TRUE"); Statement stat = conn.createStatement(); stat.execute("CREATE TABLE t1(id int, name varchar) ENGINE \"" + EndlessTableEngine.class.getName() + "\""); ResultSet rs = stat.executeQuery( "SELECT name FROM t1 where id=1 and name is not null"); assertTrue(rs.next()); assertEquals("((ID = 1)\n AND (NAME IS NOT NULL))", rs.getString(1)); rs.close(); conn.close(); deleteDb("tableEngine"); } private void testEngineParams() throws SQLException { deleteDb("tableEngine"); Connection conn = getConnection("tableEngine"); Statement stat = conn.createStatement(); stat.execute("CREATE TABLE t1(id int, name varchar) ENGINE \"" + EndlessTableEngine.class.getName() + "\" WITH \"param1\", \"param2\""); assertEquals(2, EndlessTableEngine.createTableData.tableEngineParams.size()); assertEquals("param1", EndlessTableEngine.createTableData.tableEngineParams.get(0)); assertEquals("param2", EndlessTableEngine.createTableData.tableEngineParams.get(1)); stat.execute("CREATE TABLE t2(id int, name varchar) WITH \"param1\", \"param2\""); assertEquals(2, EndlessTableEngine.createTableData.tableEngineParams.size()); assertEquals("param1", EndlessTableEngine.createTableData.tableEngineParams.get(0)); assertEquals("param2", EndlessTableEngine.createTableData.tableEngineParams.get(1)); conn.close(); if (!config.memory) { // Test serialization of table parameters EndlessTableEngine.createTableData.tableEngineParams.clear(); conn = getConnection("tableEngine"); assertEquals(2, EndlessTableEngine.createTableData.tableEngineParams.size()); assertEquals("param1", EndlessTableEngine.createTableData.tableEngineParams.get(0)); assertEquals("param2", EndlessTableEngine.createTableData.tableEngineParams.get(1)); conn.close(); } deleteDb("tableEngine"); } private void testSchemaEngineParams() throws SQLException { deleteDb("tableEngine"); Connection conn = getConnection("tableEngine"); Statement stat = conn.createStatement(); stat.execute("CREATE SCHEMA s1 WITH \"param1\", \"param2\""); stat.execute("CREATE TABLE s1.t1(id int, name varchar) ENGINE \"" + EndlessTableEngine.class.getName() + '\"'); assertEquals(2, EndlessTableEngine.createTableData.tableEngineParams.size()); assertEquals("param1", EndlessTableEngine.createTableData.tableEngineParams.get(0)); assertEquals("param2", EndlessTableEngine.createTableData.tableEngineParams.get(1)); conn.close(); deleteDb("tableEngine"); } private void testSimpleQuery() throws SQLException { deleteDb("tableEngine"); Connection conn = getConnection("tableEngine"); Statement stat = conn.createStatement(); stat.execute("CREATE TABLE t1(id int, name varchar) ENGINE \"" + OneRowTableEngine.class.getName() + "\""); testStatements(stat); stat.close(); conn.close(); if (!config.memory) { conn = getConnection("tableEngine"); stat = conn.createStatement(); ResultSet rs = stat.executeQuery("SELECT name FROM t1"); assertFalse(rs.next()); rs.close(); testStatements(stat); stat.close(); conn.close(); } deleteDb("tableEngine"); } private void testStatements(Statement stat) throws SQLException { assertEquals(stat.executeUpdate("INSERT INTO t1 VALUES(2, 'abc')"), 1); assertEquals(stat.executeUpdate("UPDATE t1 SET name = 'abcdef' WHERE id=2"), 1); assertEquals(stat.executeUpdate("INSERT INTO t1 VALUES(3, 'abcdefghi')"), 1); assertEquals(stat.executeUpdate("DELETE FROM t1 WHERE id=2"), 0); assertEquals(stat.executeUpdate("DELETE FROM t1 WHERE id=3"), 1); ResultSet rs = stat.executeQuery("SELECT name FROM t1"); assertFalse(rs.next()); rs.close(); assertEquals(stat.executeUpdate("INSERT INTO t1 VALUES(2, 'abc')"), 1); assertEquals(stat.executeUpdate("UPDATE t1 SET name = 'abcdef' WHERE id=2"), 1); assertEquals(stat.executeUpdate("INSERT INTO t1 VALUES(3, 'abcdefghi')"), 1); rs = stat.executeQuery("SELECT name FROM t1"); assertTrue(rs.next()); assertEquals(rs.getString(1), "abcdefghi"); assertFalse(rs.next()); rs.close(); } private void testMultiColumnTreeSetIndex() throws SQLException { deleteDb("tableEngine"); Connection conn = getConnection("tableEngine"); Statement stat = conn.createStatement(); stat.executeUpdate("CREATE TABLE T(A INT, B VARCHAR, C BIGINT, " + "D BIGINT DEFAULT 0) ENGINE \"" + TreeSetIndexTableEngine.class.getName() + "\""); stat.executeUpdate("CREATE INDEX IDX_C_B_A ON T(C, B, A)"); stat.executeUpdate("CREATE INDEX IDX_B_A ON T(B, A)"); List<List<Object>> dataSet = New.arrayList(); dataSet.add(Arrays.<Object>asList(1, "1", 1L)); dataSet.add(Arrays.<Object>asList(1, "0", 2L)); dataSet.add(Arrays.<Object>asList(2, "0", -1L)); dataSet.add(Arrays.<Object>asList(0, "0", 1L)); dataSet.add(Arrays.<Object>asList(0, "1", null)); dataSet.add(Arrays.<Object>asList(2, null, 0L)); PreparedStatement prep = conn.prepareStatement("INSERT INTO T(A,B,C) VALUES(?,?,?)"); for (List<Object> row : dataSet) { for (int i = 0; i < row.size(); i++) { prep.setObject(i + 1, row.get(i)); } assertEquals(1, prep.executeUpdate()); } prep.close(); checkPlan(stat, "select max(c) from t", "direct lookup"); checkPlan(stat, "select min(c) from t", "direct lookup"); checkPlan(stat, "select count(*) from t", "direct lookup"); checkPlan(stat, "select * from t", "scan"); checkPlan(stat, "select * from t order by c", "IDX_C_B_A"); checkPlan(stat, "select * from t order by c, b", "IDX_C_B_A"); checkPlan(stat, "select * from t order by b", "IDX_B_A"); checkPlan(stat, "select * from t order by b, a", "IDX_B_A"); checkPlan(stat, "select * from t order by b, c", "scan"); checkPlan(stat, "select * from t order by a, b", "scan"); checkPlan(stat, "select * from t order by a, c, b", "scan"); checkPlan(stat, "select * from t where b > ''", "IDX_B_A"); checkPlan(stat, "select * from t where a > 0 and b > ''", "IDX_B_A"); checkPlan(stat, "select * from t where b < ''", "IDX_B_A"); checkPlan(stat, "select * from t where b < '' and c < 1", "IDX_C_B_A"); checkPlan(stat, "select * from t where a = 0", "scan"); checkPlan(stat, "select * from t where a > 0 order by c, b", "IDX_C_B_A"); checkPlan(stat, "select * from t where a = 0 and c > 0", "IDX_C_B_A"); checkPlan(stat, "select * from t where a = 0 and b < 0", "IDX_B_A"); assertEquals(6, ((Number) query(stat, "select count(*) from t").get(0).get(0)).intValue()); checkResultsNoOrder(stat, 6, "select * from t", "select * from t order by a"); checkResultsNoOrder(stat, 6, "select * from t", "select * from t order by b"); checkResultsNoOrder(stat, 6, "select * from t", "select * from t order by c"); checkResultsNoOrder(stat, 6, "select * from t", "select * from t order by c, a"); checkResultsNoOrder(stat, 6, "select * from t", "select * from t order by b, a"); checkResultsNoOrder(stat, 6, "select * from t", "select * from t order by c, b, a"); checkResultsNoOrder(stat, 6, "select * from t", "select * from t order by a, c, b"); checkResultsNoOrder(stat, 4, "select * from t where a > 0", "select * from t where a > 0 order by a"); checkResultsNoOrder(stat, 4, "select * from t where a > 0", "select * from t where a > 0 order by b"); checkResultsNoOrder(stat, 4, "select * from t where a > 0", "select * from t where a > 0 order by c"); checkResultsNoOrder(stat, 4, "select * from t where a > 0", "select * from t where a > 0 order by c, a"); checkResultsNoOrder(stat, 4, "select * from t where a > 0", "select * from t where a > 0 order by b, a"); checkResultsNoOrder(stat, 4, "select * from t where a > 0", "select * from t where a > 0 order by c, b, a"); checkResultsNoOrder(stat, 4, "select * from t where a > 0", "select * from t where a > 0 order by a, c, b"); checkResults(6, dataSet, stat, "select * from t order by a", null, new RowComparator(0)); checkResults(6, dataSet, stat, "select * from t order by a desc", null, new RowComparator(true, 0)); checkResults(6, dataSet, stat, "select * from t order by b, c", null, new RowComparator(1, 2)); checkResults(6, dataSet, stat, "select * from t order by c, a", null, new RowComparator(2, 0)); checkResults(6, dataSet, stat, "select * from t order by b, a", null, new RowComparator(1, 0)); checkResults(6, dataSet, stat, "select * from t order by c, b, a", null, new RowComparator(2, 1, 0)); checkResults(4, dataSet, stat, "select * from t where a > 0", new RowFilter() { @Override protected boolean accept(List<Object> row) { return getInt(row, 0) > 0; } }, null); checkResults(3, dataSet, stat, "select * from t where b = '0'", new RowFilter() { @Override protected boolean accept(List<Object> row) { return "0".equals(getString(row, 1)); } }, null); checkResults(5, dataSet, stat, "select * from t where b >= '0'", new RowFilter() { @Override protected boolean accept(List<Object> row) { String b = getString(row, 1); return b != null && b.compareTo("0") >= 0; } }, null); checkResults(2, dataSet, stat, "select * from t where b > '0'", new RowFilter() { @Override protected boolean accept(List<Object> row) { String b = getString(row, 1); return b != null && b.compareTo("0") > 0; } }, null); checkResults(1, dataSet, stat, "select * from t where b > '0' and c > 0", new RowFilter() { @Override protected boolean accept(List<Object> row) { String b = getString(row, 1); Long c = getLong(row, 2); return b != null && b.compareTo("0") > 0 && c != null && c > 0; } }, null); checkResults(1, dataSet, stat, "select * from t where b > '0' and c < 2", new RowFilter() { @Override protected boolean accept(List<Object> row) { String b = getString(row, 1); Long c = getLong(row, 2); return b != null && b.compareTo("0") > 0 && c != null && c < 2; } }, null); checkResults(2, dataSet, stat, "select * from t where b > '0' and a < 2", new RowFilter() { @Override protected boolean accept(List<Object> row) { Integer a = getInt(row, 0); String b = getString(row, 1); return b != null && b.compareTo("0") > 0 && a != null && a < 2; } }, null); checkResults(1, dataSet, stat, "select * from t where b > '0' and a > 0", new RowFilter() { @Override protected boolean accept(List<Object> row) { Integer a = getInt(row, 0); String b = getString(row, 1); return b != null && b.compareTo("0") > 0 && a != null && a > 0; } }, null); checkResults(2, dataSet, stat, "select * from t where b = '0' and a > 0", new RowFilter() { @Override protected boolean accept(List<Object> row) { Integer a = getInt(row, 0); String b = getString(row, 1); return "0".equals(b) && a != null && a > 0; } }, null); checkResults(2, dataSet, stat, "select * from t where b = '0' and a < 2", new RowFilter() { @Override protected boolean accept(List<Object> row) { Integer a = getInt(row, 0); String b = getString(row, 1); return "0".equals(b) && a != null && a < 2; } }, null); conn.close(); deleteDb("tableEngine"); } private void testQueryExpressionFlag() throws SQLException { deleteDb("testQueryExpressionFlag"); Connection conn = getConnection("testQueryExpressionFlag"); Statement stat = conn.createStatement(); stat.execute("create table QUERY_EXPR_TEST(id int) ENGINE \"" + TreeSetIndexTableEngine.class.getName() + "\""); stat.execute("create table QUERY_EXPR_TEST_NO(id int) ENGINE \"" + TreeSetIndexTableEngine.class.getName() + "\""); stat.executeQuery("select 1 + (select 1 from QUERY_EXPR_TEST)").next(); stat.executeQuery("select 1 from QUERY_EXPR_TEST_NO where id in " + "(select id from QUERY_EXPR_TEST)"); stat.executeQuery("select 1 from QUERY_EXPR_TEST_NO n " + "where exists(select 1 from QUERY_EXPR_TEST y where y.id = n.id)"); conn.close(); deleteDb("testQueryExpressionFlag"); } private void testSubQueryInfo() throws SQLException { deleteDb("testSubQueryInfo"); Connection conn = getConnection("testSubQueryInfo"); Statement stat = conn.createStatement(); stat.execute("create table SUB_QUERY_TEST(id int primary key, name varchar) ENGINE \"" + TreeSetIndexTableEngine.class.getName() + "\""); // test sub-queries stat.executeQuery("select * from " + "(select t2.id from " + "(select t3.id from sub_query_test t3 where t3.name = '') t4, " + "sub_query_test t2 " + "where t2.id = t4.id) t5").next(); // test view 1 stat.execute("create view t4 as (select t3.id from sub_query_test t3 where t3.name = '')"); stat.executeQuery("select * from " + "(select t2.id from t4, sub_query_test t2 where t2.id = t4.id) t5").next(); // test view 2 stat.execute("create view t5 as " + "(select t2.id from t4, sub_query_test t2 where t2.id = t4.id)"); stat.executeQuery("select * from t5").next(); // test select expressions stat.execute("create table EXPR_TEST(id int) ENGINE \"" + TreeSetIndexTableEngine.class.getName() + "\""); stat.executeQuery("select * from (select (select id from EXPR_TEST x limit 1) a " + "from dual where 1 = (select id from EXPR_TEST y limit 1)) z").next(); // test select expressions 2 stat.execute("create table EXPR_TEST2(id int) ENGINE \"" + TreeSetIndexTableEngine.class.getName() + "\""); stat.executeQuery("select * from (select (select 1 from " + "(select (select 2 from EXPR_TEST) from EXPR_TEST2) ZZ) from dual)").next(); // test select expression plan stat.execute("create table test_plan(id int primary key, name varchar)"); stat.execute("create index MY_NAME_INDEX on test_plan(name)"); checkPlan(stat, "select * from (select (select id from test_plan " + "where name = 'z') from dual)", "MY_NAME_INDEX"); conn.close(); deleteDb("testSubQueryInfo"); } private void setBatchingEnabled(Statement stat, boolean enabled) throws SQLException { stat.execute("SET BATCH_JOINS " + enabled); if (!config.networked) { Session s = (Session) ((JdbcConnection) stat.getConnection()).getSession(); assertEquals(enabled, s.isJoinBatchEnabled()); } } private void testBatchedJoin() throws SQLException { deleteDb("testBatchedJoin"); Connection conn = getConnection("testBatchedJoin;OPTIMIZE_REUSE_RESULTS=0;BATCH_JOINS=1"); Statement stat = conn.createStatement(); setBatchingEnabled(stat, false); setBatchingEnabled(stat, true); TreeSetIndex.exec = Executors.newFixedThreadPool(8, new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread t = new Thread(r); t.setDaemon(true); return t; } }); forceJoinOrder(stat, true); try { doTestBatchedJoinSubQueryUnion(stat); TreeSetIndex.lookupBatches.set(0); doTestBatchedJoin(stat, 1, 0, 0); doTestBatchedJoin(stat, 0, 1, 0); doTestBatchedJoin(stat, 0, 0, 1); doTestBatchedJoin(stat, 0, 2, 0); doTestBatchedJoin(stat, 0, 0, 2); doTestBatchedJoin(stat, 0, 0, 3); doTestBatchedJoin(stat, 0, 0, 4); doTestBatchedJoin(stat, 0, 0, 5); doTestBatchedJoin(stat, 0, 3, 1); doTestBatchedJoin(stat, 0, 3, 3); doTestBatchedJoin(stat, 0, 3, 7); doTestBatchedJoin(stat, 0, 4, 1); doTestBatchedJoin(stat, 0, 4, 6); doTestBatchedJoin(stat, 0, 4, 20); doTestBatchedJoin(stat, 0, 10, 0); doTestBatchedJoin(stat, 0, 0, 10); doTestBatchedJoin(stat, 0, 20, 0); doTestBatchedJoin(stat, 0, 0, 20); doTestBatchedJoin(stat, 0, 20, 20); doTestBatchedJoin(stat, 3, 7, 0); doTestBatchedJoin(stat, 0, 0, 5); doTestBatchedJoin(stat, 0, 8, 1); doTestBatchedJoin(stat, 0, 2, 1); assertTrue(TreeSetIndex.lookupBatches.get() > 0); } finally { forceJoinOrder(stat, false); TreeSetIndex.exec.shutdownNow(); } conn.close(); deleteDb("testBatchedJoin"); } private static void forceJoinOrder(Statement s, boolean force) throws SQLException { s.executeUpdate("SET FORCE_JOIN_ORDER " + force); } private void checkPlan(Statement stat, String sql) throws SQLException { ResultSet rs = stat.executeQuery("EXPLAIN " + sql); assertTrue(rs.next()); String plan = rs.getString(1); assertEquals(normalize(sql), normalize(plan)); } private static String normalize(String sql) { sql = sql.replace('\n', ' '); return sql.replaceAll("\\s+", " ").trim(); } private void doTestBatchedJoinSubQueryUnion(Statement stat) throws SQLException { String engine = '"' + TreeSetIndexTableEngine.class.getName() + '"'; stat.execute("CREATE TABLE t (a int, b int) ENGINE " + engine); TreeSetTable t = TreeSetIndexTableEngine.created; stat.execute("CREATE INDEX T_IDX_A ON t(a)"); stat.execute("CREATE INDEX T_IDX_B ON t(b)"); setBatchSize(t, 3); for (int i = 0; i < 20; i++) { stat.execute("insert into t values (" + i + "," + (i + 10) + ")"); } stat.execute("CREATE TABLE u (a int, b int) ENGINE " + engine); TreeSetTable u = TreeSetIndexTableEngine.created; stat.execute("CREATE INDEX U_IDX_A ON u(a)"); stat.execute("CREATE INDEX U_IDX_B ON u(b)"); setBatchSize(u, 0); for (int i = 10; i < 25; i++) { stat.execute("insert into u values (" + i + "," + (i - 15)+ ")"); } checkPlan(stat, "SELECT 1 FROM PUBLIC.T T1 /* PUBLIC.\"scan\" */ " + "INNER JOIN PUBLIC.T T2 /* batched:test PUBLIC.T_IDX_B: B = T1.A */ " + "ON 1=1 WHERE T1.A = T2.B"); checkPlan(stat, "SELECT 1 FROM PUBLIC.T T1 /* PUBLIC.\"scan\" */ " + "INNER JOIN PUBLIC.T T2 /* batched:test PUBLIC.T_IDX_B: B = T1.A */ " + "ON 1=1 /* WHERE T1.A = T2.B */ " + "INNER JOIN PUBLIC.T T3 /* batched:test PUBLIC.T_IDX_B: B = T2.A */ " + "ON 1=1 WHERE (T2.A = T3.B) AND (T1.A = T2.B)"); checkPlan(stat, "SELECT 1 FROM PUBLIC.T T1 /* PUBLIC.\"scan\" */ " + "INNER JOIN PUBLIC.U /* batched:fake PUBLIC.U_IDX_A: A = T1.A */ " + "ON 1=1 /* WHERE T1.A = U.A */ " + "INNER JOIN PUBLIC.T T2 /* batched:test PUBLIC.T_IDX_B: B = U.B */ " + "ON 1=1 WHERE (T1.A = U.A) AND (U.B = T2.B)"); checkPlan(stat, "SELECT 1 FROM ( SELECT A FROM PUBLIC.T ) Z " + "/* SELECT A FROM PUBLIC.T /++ PUBLIC.T_IDX_A ++/ */ " + "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_B: B = Z.A */ " + "ON 1=1 WHERE Z.A = T.B"); checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_B */ " + "INNER JOIN ( SELECT A FROM PUBLIC.T ) Z " + "/* batched:view SELECT A FROM PUBLIC.T " + "/++ batched:test PUBLIC.T_IDX_A: A IS ?1 ++/ " + "WHERE A IS ?1: A = T.B */ ON 1=1 WHERE Z.A = T.B"); checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_A */ " + "INNER JOIN ( ((SELECT A FROM PUBLIC.T) UNION ALL (SELECT B FROM PUBLIC.U)) " + "UNION ALL (SELECT B FROM PUBLIC.T) ) Z /* batched:view " + "((SELECT A FROM PUBLIC.T /++ batched:test PUBLIC.T_IDX_A: A IS ?1 ++/ " + "WHERE A IS ?1) " + "UNION ALL " + "(SELECT B FROM PUBLIC.U /++ PUBLIC.U_IDX_B: B IS ?1 ++/ WHERE B IS ?1)) " + "UNION ALL " + "(SELECT B FROM PUBLIC.T /++ batched:test PUBLIC.T_IDX_B: B IS ?1 ++/ " + "WHERE B IS ?1): A = T.A */ ON 1=1 WHERE Z.A = T.A"); checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_A */ " + "INNER JOIN ( SELECT U.A FROM PUBLIC.U INNER JOIN PUBLIC.T ON 1=1 " + "WHERE U.B = T.B ) Z " + "/* batched:view SELECT U.A FROM PUBLIC.U " + "/++ batched:fake PUBLIC.U_IDX_A: A IS ?1 ++/ " + "/++ WHERE U.A IS ?1 ++/ INNER JOIN PUBLIC.T " + "/++ batched:test PUBLIC.T_IDX_B: B = U.B ++/ " + "ON 1=1 WHERE (U.A IS ?1) AND (U.B = T.B): A = T.A */ ON 1=1 WHERE Z.A = T.A"); checkPlan(stat, "SELECT 1 FROM PUBLIC.T /* PUBLIC.T_IDX_A */ " + "INNER JOIN ( SELECT A FROM PUBLIC.U ) Z /* SELECT A FROM PUBLIC.U " + "/++ PUBLIC.U_IDX_A: A IS ?1 ++/ WHERE A IS ?1: A = T.A */ " + "ON 1=1 WHERE T.A = Z.A"); checkPlan(stat, "SELECT 1 FROM " + "( SELECT U.A FROM PUBLIC.U INNER JOIN PUBLIC.T ON 1=1 WHERE U.B = T.B ) Z " + "/* SELECT U.A FROM PUBLIC.U /++ PUBLIC.\"scan\" ++/ " + "INNER JOIN PUBLIC.T /++ batched:test PUBLIC.T_IDX_B: B = U.B ++/ " + "ON 1=1 WHERE U.B = T.B */ " + "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_A: A = Z.A */ ON 1=1 " + "WHERE T.A = Z.A"); checkPlan(stat, "SELECT 1 FROM " + "( SELECT U.A FROM PUBLIC.T INNER JOIN PUBLIC.U ON 1=1 WHERE T.B = U.B ) Z " + "/* SELECT U.A FROM PUBLIC.T /++ PUBLIC.T_IDX_B ++/ " + "INNER JOIN PUBLIC.U /++ PUBLIC.U_IDX_B: B = T.B ++/ " + "ON 1=1 WHERE T.B = U.B */ INNER JOIN PUBLIC.T " + "/* batched:test PUBLIC.T_IDX_A: A = Z.A */ " + "ON 1=1 WHERE Z.A = T.A"); checkPlan(stat, "SELECT 1 FROM ( (SELECT A FROM PUBLIC.T) UNION " + "(SELECT A FROM PUBLIC.U) ) Z " + "/* (SELECT A FROM PUBLIC.T /++ PUBLIC.T_IDX_A ++/) " + "UNION " + "(SELECT A FROM PUBLIC.U /++ PUBLIC.U_IDX_A ++/) */ " + "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_A: A = Z.A */ ON 1=1 " + "WHERE Z.A = T.A"); checkPlan(stat, "SELECT 1 FROM PUBLIC.U /* PUBLIC.U_IDX_B */ " + "INNER JOIN ( (SELECT A, B FROM PUBLIC.T) UNION (SELECT B, A FROM PUBLIC.U) ) Z " + "/* batched:view (SELECT A, B FROM PUBLIC.T " + "/++ batched:test PUBLIC.T_IDX_B: B IS ?1 ++/ " + "WHERE B IS ?1) UNION (SELECT B, A FROM PUBLIC.U " + "/++ PUBLIC.U_IDX_A: A IS ?1 ++/ " + "WHERE A IS ?1): B = U.B */ ON 1=1 /* WHERE U.B = Z.B */ " + "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_A: A = Z.A */ ON 1=1 " + "WHERE (U.B = Z.B) AND (Z.A = T.A)"); checkPlan(stat, "SELECT 1 FROM PUBLIC.U /* PUBLIC.U_IDX_A */ " + "INNER JOIN ( SELECT A, B FROM PUBLIC.U ) Z " + "/* batched:fake SELECT A, B FROM PUBLIC.U /++ PUBLIC.U_IDX_A: A IS ?1 ++/ " + "WHERE A IS ?1: A = U.A */ ON 1=1 /* WHERE U.A = Z.A */ " + "INNER JOIN PUBLIC.T /* batched:test PUBLIC.T_IDX_B: B = Z.B */ " + "ON 1=1 WHERE (U.A = Z.A) AND (Z.B = T.B)"); // t: a = [ 0..20), b = [10..30) // u: a = [10..25), b = [-5..10) checkBatchedQueryResult(stat, 10, "select t.a from t, (select t.b from u, t where u.a = t.a) z " + "where t.b = z.b"); checkBatchedQueryResult(stat, 5, "select t.a from (select t1.b from t t1, t t2 where t1.a = t2.b) z, t " + "where t.b = z.b + 5"); checkBatchedQueryResult(stat, 1, "select t.a from (select u.b from u, t t2 where u.a = t2.b) z, t " + "where t.b = z.b + 1"); checkBatchedQueryResult(stat, 15, "select t.a from (select u.b from u, t t2 where u.a = t2.b) z " + "left join t on t.b = z.b"); checkBatchedQueryResult(stat, 15, "select t.a from (select t1.b from t t1 left join t t2 on t1.a = t2.b) z, t " + "where t.b = z.b + 5"); checkBatchedQueryResult(stat, 1, "select t.a from t,(select 5 as b from t union select 10 from u) z " + "where t.b = z.b"); checkBatchedQueryResult(stat, 15, "select t.a from u,(select 5 as b, a from t " + "union select 10, a from u) z, t where t.b = z.b and z.a = u.a"); stat.execute("DROP TABLE T"); stat.execute("DROP TABLE U"); } private void checkBatchedQueryResult(Statement stat, int size, String sql) throws SQLException { setBatchingEnabled(stat, false); List<List<Object>> expected = query(stat, sql); assertEquals(size, expected.size()); setBatchingEnabled(stat, true); List<List<Object>> actual = query(stat, sql); if (!expected.equals(actual)) { fail("\n" + "expected: " + expected + "\n" + "actual: " + actual); } } private void doTestBatchedJoin(Statement stat, int... batchSizes) throws SQLException { ArrayList<TreeSetTable> tables = New.arrayList(batchSizes.length); for (int i = 0; i < batchSizes.length; i++) { stat.executeUpdate("DROP TABLE IF EXISTS T" + i); stat.executeUpdate("CREATE TABLE T" + i + "(A INT, B INT) ENGINE \"" + TreeSetIndexTableEngine.class.getName() + "\""); tables.add(TreeSetIndexTableEngine.created); stat.executeUpdate("CREATE INDEX IDX_B ON T" + i + "(B)"); stat.executeUpdate("CREATE INDEX IDX_A ON T" + i + "(A)"); PreparedStatement insert = stat.getConnection().prepareStatement( "INSERT INTO T"+ i + " VALUES (?,?)"); for (int j = i, size = i + 10; j < size; j++) { insert.setInt(1, j); insert.setInt(2, j); insert.executeUpdate(); } for (TreeSetTable table : tables) { assertEquals(10, table.getRowCount(null)); } } int[] zeroBatchSizes = new int[batchSizes.length]; int tests = 1 << (batchSizes.length * 4); for (int test = 0; test < tests; test++) { String query = generateQuery(test, batchSizes.length); // System.out.println(Arrays.toString(batchSizes) + // ": " + test + " -> " + query); setBatchSize(tables, batchSizes); List<List<Object>> res1 = query(stat, query); setBatchSize(tables, zeroBatchSizes); List<List<Object>> res2 = query(stat, query); // System.out.println(res1 + " " + res2); if (!res2.equals(res1)) { System.err.println(Arrays.toString(batchSizes) + ": " + res1 + " " + res2); System.err.println("Test " + test); System.err.println(query); for (TreeSetTable table : tables) { System.err.println(table.getName() + " = " + query(stat, "select * from " + table.getName())); } fail(); } } for (int i = 0; i < batchSizes.length; i++) { stat.executeUpdate("DROP TABLE IF EXISTS T" + i); } } /** * A static assertion method. * * @param condition the condition * @param message the error message */ static void assert0(boolean condition, String message) { if (!condition) { throw new AssertionError(message); } } private static void setBatchSize(ArrayList<TreeSetTable> tables, int... batchSizes) { for (int i = 0; i < batchSizes.length; i++) { int batchSize = batchSizes[i]; setBatchSize(tables.get(i), batchSize); } } private static void setBatchSize(TreeSetTable t, int batchSize) { if (t.getIndexes() == null) { t.scan.preferredBatchSize = batchSize; } else { for (Index idx : t.getIndexes()) { ((TreeSetIndex) idx).preferredBatchSize = batchSize; } } } private static String generateQuery(int t, int tables) { final int withLeft = 1; final int withFalse = 2; final int withWhere = 4; final int withOnIsNull = 8; StringBuilder b = new StringBuilder(); b.append("select count(*) from "); StringBuilder where = new StringBuilder(); for (int i = 0; i < tables; i++) { if (i != 0) { if ((t & withLeft) != 0) { b.append(" left "); } b.append(" join "); } b.append("\nT").append(i).append(' '); if (i != 0) { boolean even = (i & 1) == 0; if ((t & withOnIsNull) != 0) { b.append(" on T").append(i - 1).append(even ? ".B" : ".A").append(" is null"); } else if ((t & withFalse) != 0) { b.append(" on false "); } else { b.append(" on T").append(i - 1).append(even ? ".B = " : ".A = "); b.append("T").append(i).append(even ? ".B " : ".A "); } } if ((t & withWhere) != 0) { if (where.length() != 0) { where.append(" and "); } where.append(" T").append(i).append(".A > 5"); } t >>>= 4; } if (where.length() != 0) { b.append("\n" + "where ").append(where); } return b.toString(); } private void checkResultsNoOrder(Statement stat, int size, String query1, String query2) throws SQLException { List<List<Object>> res1 = query(stat, query1); List<List<Object>> res2 = query(stat, query2); if (size != res1.size() || size != res2.size()) { fail("Wrong size: \n" + res1 + "\n" + res2); } if (size == 0) { return; } int[] cols = new int[res1.get(0).size()]; for (int i = 0; i < cols.length; i++) { cols[i] = i; } Comparator<List<Object>> comp = new RowComparator(cols); Collections.sort(res1, comp); Collections.sort(res2, comp); assertTrue("Wrong data: \n" + res1 + "\n" + res2, res1.equals(res2)); } private void checkResults(int size, List<List<Object>> dataSet, Statement stat, String query, RowFilter filter, RowComparator sort) throws SQLException { List<List<Object>> res1 = query(stat, query); List<List<Object>> res2 = query(dataSet, filter, sort); assertTrue("Wrong size: " + size + " \n" + res1 + "\n" + res2, res1.size() == size && res2.size() == size); assertTrue(filter != null || sort != null); for (int i = 0; i < res1.size(); i++) { List<Object> row1 = res1.get(i); List<Object> row2 = res2.get(i); assertTrue("Filter failed on row " + i + " of \n" + res1 + "\n" + res2, filter == null || filter.accept(row1)); assertTrue("Sort failed on row " + i + " of \n" + res1 + "\n" + res2, sort == null || sort.compare(row1, row2) == 0); } } private static List<List<Object>> query(List<List<Object>> dataSet, RowFilter filter, RowComparator sort) { List<List<Object>> res = New.arrayList(); if (filter == null) { res.addAll(dataSet); } else { for (List<Object> row : dataSet) { if (filter.accept(row)) { res.add(row); } } } if (sort != null) { Collections.sort(res, sort); } return res; } private static List<List<Object>> query(Statement stat, String query) throws SQLException { ResultSet rs = stat.executeQuery(query); int cols = rs.getMetaData().getColumnCount(); List<List<Object>> list = New.arrayList(); while (rs.next()) { List<Object> row = New.arrayList(cols); for (int i = 1; i <= cols; i++) { row.add(rs.getObject(i)); } list.add(row); } rs.close(); return list; } private void checkPlan(Statement stat, String query, String index) throws SQLException { String plan = query(stat, "EXPLAIN " + query).get(0).get(0).toString(); assertTrue("Index '" + index + "' is not used in query plan: " + plan, plan.contains(index)); } /** * A test table factory. */ public static class OneRowTableEngine implements TableEngine { /** * A table implementation with one row. */ private static class OneRowTable extends TableBase { /** * A scan index for one row. */ public class Scan extends BaseIndex { Scan(Table table) { initBaseIndex(table, table.getId(), table.getName() + "_SCAN", IndexColumn.wrap(table.getColumns()), IndexType.createScan(false)); } @Override public long getRowCountApproximation() { return table.getRowCountApproximation(); } @Override public long getDiskSpaceUsed() { return table.getDiskSpaceUsed(); } @Override public long getRowCount(Session session) { return table.getRowCount(session); } @Override public void checkRename() { // do nothing } @Override public void truncate(Session session) { // do nothing } @Override public void remove(Session session) { // do nothing } @Override public void remove(Session session, Row r) { // do nothing } @Override public boolean needRebuild() { return false; } @Override public double getCost(Session session, int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder, HashSet<Column> allColumnsSet) { return 0; } @Override public Cursor findFirstOrLast(Session session, boolean first) { return new SingleRowCursor(row); } @Override public Cursor find(Session session, SearchRow first, SearchRow last) { return new SingleRowCursor(row); } @Override public void close(Session session) { // do nothing } @Override public boolean canGetFirstOrLast() { return true; } @Override public void add(Session session, Row r) { // do nothing } } protected Index scanIndex; volatile Row row; OneRowTable(CreateTableData data) { super(data); scanIndex = new Scan(this); } @Override public Index addIndex(Session session, String indexName, int indexId, IndexColumn[] cols, IndexType indexType, boolean create, String indexComment) { return null; } @Override public void addRow(Session session, Row r) { this.row = r; } @Override public boolean canDrop() { return true; } @Override public boolean canGetRowCount() { return true; } @Override public void checkSupportAlter() { // do nothing } @Override public void close(Session session) { // do nothing } @Override public ArrayList<Index> getIndexes() { return null; } @Override public long getMaxDataModificationId() { return 0; } @Override public long getRowCount(Session session) { return getRowCountApproximation(); } @Override public long getRowCountApproximation() { return row == null ? 0 : 1; } @Override public long getDiskSpaceUsed() { return 0; } @Override public Index getScanIndex(Session session) { return scanIndex; } @Override public TableType getTableType() { return TableType.EXTERNAL_TABLE_ENGINE; } @Override public Index getUniqueIndex() { return null; } @Override public boolean isDeterministic() { return false; } @Override public boolean isLockedExclusively() { return false; } @Override public boolean lock(Session session, boolean exclusive, boolean force) { // do nothing return false; } @Override public void removeRow(Session session, Row r) { this.row = null; } @Override public void truncate(Session session) { row = null; } @Override public void unlock(Session s) { // do nothing } @Override public void checkRename() { // do nothing } } /** * Create a new OneRowTable. * * @param data the meta data of the table to create * @return the new table */ @Override public OneRowTable createTable(CreateTableData data) { return new OneRowTable(data); } } /** * A test table factory. */ public static class EndlessTableEngine implements TableEngine { public static CreateTableData createTableData; /** * A table implementation with one row. */ private static class EndlessTable extends OneRowTableEngine.OneRowTable { EndlessTable(CreateTableData data) { super(data); row = data.schema.getDatabase().createRow( new Value[] { ValueInt.get(1), ValueNull.INSTANCE }, 0); scanIndex = new Auto(this); } /** * A scan index for one row. */ public class Auto extends OneRowTableEngine.OneRowTable.Scan { Auto(Table table) { super(table); } @Override public Cursor find(TableFilter filter, SearchRow first, SearchRow last) { return find(filter.getFilterCondition()); } @Override public Cursor find(Session session, SearchRow first, SearchRow last) { return find(null); } /** * Search within the table. * * @param filter the table filter (optional) * @return the cursor */ private Cursor find(Expression filter) { if (filter != null) { row.setValue(1, ValueString.get(filter.getSQL())); } return new SingleRowCursor(row); } } } /** * Create a new table. * * @param data the meta data of the table to create * @return the new table */ @Override public EndlessTable createTable(CreateTableData data) { createTableData = data; return new EndlessTable(data); } } /** * A table engine that internally uses a tree set. */ public static class TreeSetIndexTableEngine implements TableEngine { static TreeSetTable created; @Override public Table createTable(CreateTableData data) { return created = new TreeSetTable(data); } } /** * A table that internally uses a tree set. */ private static class TreeSetTable extends TableBase { int dataModificationId; ArrayList<Index> indexes; TreeSetIndex scan = new TreeSetIndex(this, "scan", IndexColumn.wrap(getColumns()), IndexType.createScan(false)) { @Override public double getCost(Session session, int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder, HashSet<Column> allColumnsSet) { doTests(session); return getCostRangeIndex(masks, getRowCount(session), filters, filter, sortOrder, true, allColumnsSet); } }; public TreeSetTable(CreateTableData data) { super(data); } @Override public void checkRename() { // No-op. } @Override public void unlock(Session s) { // No-op. } @Override public void truncate(Session session) { if (indexes != null) { for (Index index : indexes) { index.truncate(session); } } else { scan.truncate(session); } dataModificationId++; } @Override public void removeRow(Session session, Row row) { if (indexes != null) { for (Index index : indexes) { index.remove(session, row); } } else { scan.remove(session, row); } dataModificationId++; } @Override public void addRow(Session session, Row row) { if (indexes != null) { for (Index index : indexes) { index.add(session, row); } } else { scan.add(session, row); } dataModificationId++; } @Override public Index addIndex(Session session, String indexName, int indexId, IndexColumn[] cols, IndexType indexType, boolean create, String indexComment) { if (indexes == null) { indexes = New.arrayList(2); // Scan must be always at 0. indexes.add(scan); } Index index = new TreeSetIndex(this, indexName, cols, indexType); for (SearchRow row : scan.set) { index.add(session, (Row) row); } indexes.add(index); dataModificationId++; setModified(); return index; } @Override public boolean lock(Session session, boolean exclusive, boolean forceLockEvenInMvcc) { return true; } @Override public boolean isLockedExclusively() { return false; } @Override public boolean isDeterministic() { return false; } @Override public Index getUniqueIndex() { return null; } @Override public TableType getTableType() { return TableType.EXTERNAL_TABLE_ENGINE; } @Override public Index getScanIndex(Session session) { return scan; } @Override public long getRowCountApproximation() { return getScanIndex(null).getRowCountApproximation(); } @Override public long getRowCount(Session session) { return scan.getRowCount(session); } @Override public long getMaxDataModificationId() { return dataModificationId; } @Override public ArrayList<Index> getIndexes() { return indexes; } @Override public long getDiskSpaceUsed() { return 0; } @Override public void close(Session session) { // No-op. } @Override public void checkSupportAlter() { // No-op. } @Override public boolean canGetRowCount() { return true; } @Override public boolean canDrop() { return true; } } /** * An index that internally uses a tree set. */ private static class TreeSetIndex extends BaseIndex implements Comparator<SearchRow> { /** * Executor service to test batched joins. */ static ExecutorService exec; static AtomicInteger lookupBatches = new AtomicInteger(); int preferredBatchSize; final TreeSet<SearchRow> set = new TreeSet<SearchRow>(this); TreeSetIndex(Table t, String name, IndexColumn[] cols, IndexType type) { initBaseIndex(t, 0, name, cols, type); } @Override public int compare(SearchRow o1, SearchRow o2) { int res = compareRows(o1, o2); if (res == 0) { if (o1.getKey() == Long.MAX_VALUE || o2.getKey() == Long.MIN_VALUE) { res = 1; } else if (o1.getKey() == Long.MIN_VALUE || o2.getKey() == Long.MAX_VALUE) { res = -1; } } return res; } @Override public IndexLookupBatch createLookupBatch(TableFilter[] filters, int f) { final TableFilter filter = filters[f]; assert0(filter.getMasks() != null || "scan".equals(getName()), "masks"); final int preferredSize = preferredBatchSize; if (preferredSize == 0) { return null; } lookupBatches.incrementAndGet(); return new IndexLookupBatch() { List<SearchRow> searchRows = New.arrayList(); @Override public String getPlanSQL() { return "test"; } @Override public boolean isBatchFull() { return searchRows.size() >= preferredSize * 2; } @Override public List<Future<Cursor>> find() { List<Future<Cursor>> res = findBatched(filter, searchRows); searchRows.clear(); return res; } @Override public boolean addSearchRows(SearchRow first, SearchRow last) { assert !isBatchFull(); searchRows.add(first); searchRows.add(last); return true; } @Override public void reset(boolean beforeQuery) { searchRows.clear(); } }; } public List<Future<Cursor>> findBatched(final TableFilter filter, List<SearchRow> firstLastPairs) { ArrayList<Future<Cursor>> result = New.arrayList(firstLastPairs.size()); final Random rnd = new Random(); for (int i = 0; i < firstLastPairs.size(); i += 2) { final SearchRow first = firstLastPairs.get(i); final SearchRow last = firstLastPairs.get(i + 1); Future<Cursor> future; if (rnd.nextBoolean()) { IteratorCursor c = (IteratorCursor) find(filter, first, last); if (c.it.hasNext()) { future = new DoneFuture<Cursor>(c); } else { // we can return null instead of future of empty cursor future = null; } } else { future = exec.submit(new Callable<Cursor>() { @Override public Cursor call() throws Exception { if (rnd.nextInt(50) == 0) { Thread.sleep(0, 500); } return find(filter, first, last); } }); } result.add(future); } return result; } @Override public void close(Session session) { // No-op. } @Override public void add(Session session, Row row) { set.add(row); } @Override public void remove(Session session, Row row) { set.remove(row); } private static SearchRow mark(SearchRow row, boolean first) { if (row != null) { // Mark this row to be a search row. row.setKey(first ? Long.MIN_VALUE : Long.MAX_VALUE); } return row; } @Override public Cursor find(Session session, SearchRow first, SearchRow last) { Set<SearchRow> subSet; if (first != null && last != null && compareRows(last, first) < 0) { subSet = Collections.emptySet(); } else { if (first != null) { first = set.floor(mark(first, true)); } if (last != null) { last = set.ceiling(mark(last, false)); } if (first == null && last == null) { subSet = set; } else if (first != null) { if (last != null) { subSet = set.subSet(first, true, last, true); } else { subSet = set.tailSet(first, true); } } else if (last != null) { subSet = set.headSet(last, true); } else { throw new IllegalStateException(); } } return new IteratorCursor(subSet.iterator()); } private static String alias(SubQueryInfo info) { return info.getFilters()[info.getFilter()].getTableAlias(); } private void checkInfo(SubQueryInfo info) { if (info.getUpper() == null) { // check 1st level info assert0(info.getFilters().length == 1, "getFilters().length " + info.getFilters().length); String alias = alias(info); assert0("T5".equals(alias), "alias: " + alias); } else { // check 2nd level info assert0(info.getFilters().length == 2, "getFilters().length " + info.getFilters().length); String alias = alias(info); assert0("T4".equals(alias), "alias: " + alias); checkInfo(info.getUpper()); } } protected void doTests(Session session) { if (getTable().getName().equals("SUB_QUERY_TEST")) { checkInfo(session.getSubQueryInfo()); } else if (getTable().getName().equals("EXPR_TEST")) { assert0(session.getSubQueryInfo() == null, "select expression"); } else if (getTable().getName().equals("EXPR_TEST2")) { String alias = alias(session.getSubQueryInfo()); assert0(alias.equals("ZZ"), "select expression sub-query: " + alias); assert0(session.getSubQueryInfo().getUpper() == null, "upper"); } else if (getTable().getName().equals("QUERY_EXPR_TEST")) { assert0(session.isPreparingQueryExpression(), "preparing query expression"); } else if (getTable().getName().equals("QUERY_EXPR_TEST_NO")) { assert0(!session.isPreparingQueryExpression(), "not preparing query expression"); } } @Override public double getCost(Session session, int[] masks, TableFilter[] filters, int filter, SortOrder sortOrder, HashSet<Column> allColumnsSet) { doTests(session); return getCostRangeIndex(masks, set.size(), filters, filter, sortOrder, false, allColumnsSet); } @Override public void remove(Session session) { // No-op. } @Override public void truncate(Session session) { set.clear(); } @Override public boolean canGetFirstOrLast() { return true; } @Override public Cursor findFirstOrLast(Session session, boolean first) { return new SingleRowCursor((Row) (set.isEmpty() ? null : first ? set.first() : set.last())); } @Override public boolean needRebuild() { return true; } @Override public long getRowCount(Session session) { return set.size(); } @Override public long getRowCountApproximation() { return getRowCount(null); } @Override public long getDiskSpaceUsed() { return 0; } @Override public void checkRename() { // No-op. } } /** */ private static class IteratorCursor implements Cursor { Iterator<SearchRow> it; private Row current; public IteratorCursor(Iterator<SearchRow> it) { this.it = it; } @Override public boolean previous() { throw DbException.getUnsupportedException("prev"); } @Override public boolean next() { if (it.hasNext()) { current = (Row) it.next(); return true; } current = null; return false; } @Override public SearchRow getSearchRow() { return get(); } @Override public Row get() { return current; } @Override public String toString() { return "IteratorCursor->" + current; } } /** * A comparator for rows (lists of comparable objects). */ private static class RowComparator implements Comparator<List<Object>> { private int[] cols; private boolean descending; public RowComparator(int... cols) { this.descending = false; this.cols = cols; } public RowComparator(boolean descending, int... cols) { this.descending = descending; this.cols = cols; } @SuppressWarnings("unchecked") @Override public int compare(List<Object> row1, List<Object> row2) { for (int i = 0; i < cols.length; i++) { int col = cols[i]; Comparable<Object> o1 = (Comparable<Object>) row1.get(col); Comparable<Object> o2 = (Comparable<Object>) row2.get(col); if (o1 == null) { return applyDescending(o2 == null ? 0 : -1); } if (o2 == null) { return applyDescending(1); } int res = o1.compareTo(o2); if (res != 0) { return applyDescending(res); } } return 0; } private int applyDescending(int v) { if (!descending) { return v; } if (v == 0) { return v; } return -v; } } /** * A filter for rows (lists of objects). */ abstract static class RowFilter { /** * Check whether the row needs to be processed. * * @param row the row * @return true if yes */ protected abstract boolean accept(List<Object> row); /** * Get an integer from a row. * * @param row the row * @param col the column index * @return the value */ protected Integer getInt(List<Object> row, int col) { return (Integer) row.get(col); } /** * Get a long from a row. * * @param row the row * @param col the column index * @return the value */ protected Long getLong(List<Object> row, int col) { return (Long) row.get(col); } /** * Get a string from a row. * * @param row the row * @param col the column index * @return the value */ protected String getString(List<Object> row, int col) { return (String) row.get(col); } } }