/*

Derby - Class org.apache.derbyTesting.perf.clients.GroupByClient

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.derbyTesting.perf.clients;

import java.io.PrintStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.sql.DatabaseMetaData;

/**
 * Test client which performs iterated GROUP BY statements on the
 * {@code ONEKTUP} tables generated by {@code WisconsinFiller}.
 *
 * Based on the parameters specified when run, we perform a particular
 * GROUP BY statement, and fetch and check the number of rows returned,
 * as part of a performance run controlled by perf.clients.Runner.
 *
 * For example, you could cause this benchmark's GROUP BY to be:
 * - one which returns 10 groups, with 1000 rows in each group, or
 * - one which returns 100 groups, with 100 rows in each group, or
 * - one which returns 1000 groups, with 10 rows in each group, etc.
 * With correspondingly larger numbers of groups as the scale factor grows.
 *
 * You can use more rows by passing '-load_opts numRows=100000', e.g. Note that
 * this only has an effect when you run -init. Note that changing the number
 * of rows in the table also changes the expected size of each group; we issue
 * a select count(*) query at the start to figure out the expected group size,
 * but this hack only works with table sizes that are multiples of 1000.
 *
 * If you use a substantially larger number of rows (say, 100000 or more), you
 * should specify '-rt 300' or higher so that a valid number of executions
 * can occur, as the benchmark starts to slow down dramatically with large
 * numbers of rows.
 *
 * To prepare the database for this little benchmark:
 *  java org.apache.derbyTesting.perf.clients.Runner -init -load group_by
 *       -load_opts numRows=NNNNNN (if you want more than 10,000 rows in DB)
 * (this will also run the default GROUP BY, which is GROUP BY TEN)
 *
 * On subsequent runs you can skip the '-init', and should instead specify
 * a particular GROUP BY to run, which you do by specifying:
 * - the number of GROUP_BY columns (-load_opts numGroupingCols=N), and
 * - the number of groups for each column (-load_opts numGroupsK=NNNN)
 *   (NOTE: we count from 1, not from 0, with these parameters!)
 *
 * I've tried this benchmark up to 5 grouping columns, which seemed like
 * plenty for the benchmarking I wanted to do. The code supports more, but
 * I'm not sure if it works or not.
 *
 * For example, this runs a 2-column group by:
 *
 * -load group_by -load_opts numGroupingCols=2,numGroups1=10,numGroups2=100
 *
 * The resulting SQL will be:
 *
 *  SELECT TEN, ONEPERCENT, COUNT(*) FROM TENKTUP1 GROUP BY TEN,ONEPERCENT
 *
 * Note that due to the way that the data in the TEN and ONEPERCENT columns
 * are loaded, they are not independent, so this actually produces 100 groups.
 *
 * If numGroupingCols == 1, and thus the code can predict the number of
 * rows that ought to be in each group, and the total number of groups, then
 * it checks those values in the result as well.
 */
public class GroupByClient implements Client
{
    private static final int  MAX_GROUPING_COLS = 25;

    private Connection        conn;
    private PreparedStatement ps;
    private int               numGroupingCols;
    //                        Note that we count from 1. Index 0 is unused.
    private int               numGroups[] = new int[MAX_GROUPING_COLS+1];
    private String            groupingExpr[] = new String[MAX_GROUPING_COLS+1];
    private int               totalExpectedGroups;
    private int               tableSize = 0;
    private String            sql;

    public GroupByClient()
    {
        numGroupingCols = Runner.getLoadOpt("numGroupingCols", -1);
        if (numGroupingCols > 0)
        {
            totalExpectedGroups = 1;
            for (int i = 1; i < numGroups.length; i++)
            {
                numGroups[i] = Runner.getLoadOpt("numGroups"+i, 1);
                totalExpectedGroups *= numGroups[i];
                getGroupingExpr(i);
            }
        }
        else
        {
            // default statement is 1 grouping column with 10 groups:
            numGroupingCols = 1;
            numGroups[1] = 10;
            totalExpectedGroups = 10;
            getGroupingExpr(1);
        }
        sql = buildStatement();
    }

    public void init(Connection c)
        throws SQLException
    {
        conn = c;
        //dumpTables();
        getTableSize();
        System.out.println("We'll run '"+sql+"'");
        if (numGroupingCols == 1)
           System.out.println("... which should produce " +
                totalExpectedGroups + " total groups, each containing " +
                (tableSize / totalExpectedGroups) + " rows.");

        ps = c.prepareStatement( sql );
        c.setAutoCommit(false);
    }
    private void getGroupingExpr(int i)
    {
        if (numGroups[i] == 1)
            groupingExpr[i] = null;
        else if (numGroups[i] == 10)
            groupingExpr[i] = "TEN";
        else if (numGroups[i] == 100)
            groupingExpr[i] = "ONEPERCENT";
        else
            groupingExpr[i] = "MOD(UNIQUE1,"+numGroups[i]+")";
    }
    private String buildStatement()
    {
        StringBuffer buf = new StringBuffer();
        buf.append("SELECT ");
        appendGroups(buf);
        buf.append( ", COUNT(*) FROM TENKTUP1 GROUP BY ");
        appendGroups(buf);
        return buf.toString();
    }
    private void appendGroups(StringBuffer buf)
    {
        for (int i = 1; i < numGroups.length && numGroups[i] > 1; i++)
        {
            if (i > 1)
                buf.append(",");
            buf.append(groupingExpr[i]);
        }
    }
    private void dumpTables()
        throws SQLException
    {
        ResultSet rs = conn.getMetaData().getTables(null,null,"%",null);
        while (rs.next()) {
            System.out.println("Schem=" + rs.getString("TABLE_SCHEM") +
                    " name=" + rs.getString("TABLE_NAME"));
        }
        rs.close();
        conn.commit();
    }
    private void getTableSize()
        throws SQLException
    {
        // Since the TENKTUP1 table can have more or less rows, depending
        // on the value of the -load_opts numRows=N parameter, we run a
        // count query here to figure out how many rows there actually are.

        Statement s = conn.createStatement();
        ResultSet rs = s.executeQuery("select count(*) from tenktup1");
        if (!rs.next())
            throw new RuntimeException("Unable to find size of tenktup1");
        tableSize = rs.getInt(1);
        rs.close();
        s.close();
        conn.commit();
    }

    public void doWork()
        throws SQLException
    {
        ResultSet rs = ps.executeQuery();
        int expectedCount = tableSize / totalExpectedGroups;
        int numRows = 0;
        while (rs.next()) {
            numRows++;
            int groupName = rs.getInt(1);
            int theCount = rs.getInt(2);
            if (numGroupingCols == 1 && theCount != expectedCount)
                System.out.println("group="+groupName+", count="+theCount+
                        ",expectedCount="+expectedCount);
        }
        if (numGroupingCols == 1 && numRows != totalExpectedGroups)
            System.out.println("Total rows from GROUP BY was " + numRows +
                    ", expected total rows to be " + totalExpectedGroups);
        rs.close();
        conn.commit();
    }

    public void printReport(PrintStream out) {}
    
}
