/*
 * 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.drill.exec.physical.impl.limit;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.drill.PlanTestBase;
import org.apache.drill.categories.PlannerTest;
import org.apache.drill.common.expression.SchemaPath;
import org.apache.drill.common.types.TypeProtos;
import org.apache.drill.common.types.Types;
import org.apache.drill.exec.ExecConstants;
import org.apache.drill.exec.expr.fn.impl.DateUtility;
import org.apache.drill.test.BaseTestQuery;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import com.google.common.collect.Lists;

@Category(PlannerTest.class)
public class TestEarlyLimit0Optimization extends BaseTestQuery {

  private static final String viewName = "limitZeroEmployeeView";

  private static String wrapLimit0(final String query) {
    return "SELECT * FROM (" + query + ") LZT LIMIT 0";
  }

  @BeforeClass
  public static void createView() throws Exception {
    test("USE dfs.tmp");
    test("CREATE OR REPLACE VIEW %s AS SELECT " +
        "CAST(employee_id AS INT) AS employee_id, " +
        "CAST(full_name AS VARCHAR(25)) AS full_name, " +
        "CAST(position_id AS INTEGER) AS position_id, " +
        "CAST(department_id AS BIGINT) AS department_id," +
        "CAST(birth_date AS DATE) AS birth_date, " +
        "CAST(hire_date AS TIMESTAMP) AS hire_date, " +
        "CAST(salary AS DOUBLE) AS salary, " +
        "CAST(salary AS FLOAT) AS fsalary, " +
        "CAST((CASE WHEN marital_status = 'S' THEN true ELSE false END) AS BOOLEAN) AS single, " +
        "CAST(education_level AS VARCHAR(60)) AS education_level," +
        "CAST(gender AS CHAR(1)) AS gender " +
        "FROM cp.`employee.json` " +
        "ORDER BY employee_id " +
        "LIMIT 1", viewName);
  }

  @AfterClass
  public static void tearDownView() throws Exception {
    test("DROP VIEW " + viewName);
  }

  @Before
  public void setOption() throws Exception {
    test("SET `%s` = true", ExecConstants.EARLY_LIMIT0_OPT_KEY);
  }

  @After
  public void resetOption() throws Exception {
    test("RESET `%s`", ExecConstants.EARLY_LIMIT0_OPT_KEY);
  }

  // -------------------- SIMPLE QUERIES --------------------

  @Test
  public void infoSchema() throws Exception {
    testBuilder()
        .sqlQuery("DESCRIBE %s", viewName)
        .unOrdered()
        .baselineColumns("COLUMN_NAME", "DATA_TYPE", "IS_NULLABLE")
        .baselineValues("employee_id", "INTEGER", "YES")
        .baselineValues("full_name", "CHARACTER VARYING", "YES")
        .baselineValues("position_id", "INTEGER", "YES")
        .baselineValues("department_id", "BIGINT", "YES")
        .baselineValues("birth_date", "DATE", "YES")
        .baselineValues("hire_date", "TIMESTAMP", "YES")
        .baselineValues("salary", "DOUBLE", "YES")
        .baselineValues("fsalary", "FLOAT", "YES")
        .baselineValues("single", "BOOLEAN", "NO")
        .baselineValues("education_level", "CHARACTER VARYING", "YES")
        .baselineValues("gender", "CHARACTER", "YES")
        .go();
  }

  @Test
  public void simpleSelect() throws Exception {
    testBuilder()
        .sqlQuery("SELECT * FROM %s", viewName)
        .ordered()
        .baselineColumns("employee_id", "full_name", "position_id", "department_id", "birth_date", "hire_date",
            "salary", "fsalary", "single", "education_level", "gender")
        .baselineValues(1, "Sheri Nowmer", 1, 1L, LocalDate.parse("1961-08-26"),
            LocalDateTime.parse("1994-12-01 00:00:00", DateUtility.getDateTimeFormatter()), 80000.0D, 80000.0F, true, "Graduate Degree", "F")
        .go();
  }

  @Test
  public void simpleSelectLimit0() throws Exception {
    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("employee_id"), Types.optional(TypeProtos.MinorType.INT)),
        Pair.of(SchemaPath.getSimplePath("full_name"), Types.withPrecision(TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL, 25)),
        Pair.of(SchemaPath.getSimplePath("position_id"), Types.optional(TypeProtos.MinorType.INT)),
        Pair.of(SchemaPath.getSimplePath("department_id"), Types.optional(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("birth_date"), Types.optional(TypeProtos.MinorType.DATE)),
        Pair.of(SchemaPath.getSimplePath("hire_date"), Types.optional(TypeProtos.MinorType.TIMESTAMP)),
        Pair.of(SchemaPath.getSimplePath("salary"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("fsalary"), Types.optional(TypeProtos.MinorType.FLOAT4)),
        Pair.of(SchemaPath.getSimplePath("single"), Types.required(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("education_level"), Types.withPrecision(TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL, 60)),
        Pair.of(SchemaPath.getSimplePath("gender"), Types.withPrecision(TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL, 1)));

    testBuilder()
        .sqlQuery(wrapLimit0(String.format("SELECT * FROM %s", viewName)))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized("SELECT * FROM " + viewName);
  }

  private static void checkThatQueryPlanIsOptimized(final String query) throws Exception {
    PlanTestBase.testPlanMatchingPatterns(
        wrapLimit0(query),
        new String[]{
            ".*Project.*\n" +
                ".*Scan.*RelDataTypeReader.*"
        },
        new String[]{});
  }

  // -------------------- AGGREGATE FUNC. QUERIES --------------------

  private static String getAggQuery(final String functionName) {
    return "SELECT " +
        functionName + "(employee_id) AS e, " +
        functionName + "(position_id) AS p, " +
        functionName + "(department_id) AS d, " +
        functionName + "(salary) AS s, " +
        functionName + "(fsalary) AS f " +
        "FROM " + viewName;
  }

  @Test
  public void sums() throws Exception {
    final String query = getAggQuery("SUM");

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("e"), Types.optional(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("p"), Types.optional(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("d"), Types.optional(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("s"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("f"), Types.optional(TypeProtos.MinorType.FLOAT8)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("e", "p", "d", "s", "f")
        .baselineValues(1L, 1L, 1L, 80000D, 80000D)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void counts() throws Exception {
    final String query = getAggQuery("COUNT");

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("e"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("p"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("d"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("s"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("f"), Types.required(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .baselineColumns("e", "p", "d", "s", "f")
        .ordered()
        .baselineValues(1L, 1L, 1L, 1L, 1L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  private void minAndMaxTest(final String functionName) throws Exception {
    final String query = getAggQuery(functionName);

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("e"), Types.optional(TypeProtos.MinorType.INT)),
        Pair.of(SchemaPath.getSimplePath("p"), Types.optional(TypeProtos.MinorType.INT)),
        Pair.of(SchemaPath.getSimplePath("d"), Types.optional(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("s"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("f"), Types.optional(TypeProtos.MinorType.FLOAT4)));

    testBuilder()
        .sqlQuery(query)
        .baselineColumns("e", "p", "d", "s", "f")
        .ordered()
        .baselineValues(1, 1, 1L, 80_000D, 80_000F)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void mins() throws Exception {
    minAndMaxTest("MIN");
  }

  @Test
  public void maxs() throws Exception {
    minAndMaxTest("MAX");
  }

  @Test
  public void avgs() throws Exception {
    final String query = getAggQuery("AVG");

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("e"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("p"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("d"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("s"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("f"), Types.optional(TypeProtos.MinorType.FLOAT8)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("e", "p", "d", "s", "f")
        .baselineValues(1D, 1D, 1D, 80_000D, 80_000D)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void measures() throws Exception {
    final String query = "SELECT " +
        "STDDEV_SAMP(employee_id) AS s, " +
        "STDDEV_POP(position_id) AS p, " +
        "AVG(position_id) AS a, " +
        "COUNT(position_id) AS c " +
        "FROM " + viewName;

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("s"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("p"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("a"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("c"), Types.required(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("s", "p", "a", "c")
        .baselineValues(null, 0.0D, 1.0D, 1L)
         .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void nullableCount() throws Exception {
    final String query = "SELECT " +
        "COUNT(CASE WHEN position_id = 1 THEN NULL ELSE position_id END) AS c FROM " + viewName;

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("c"), Types.required(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("c")
        .baselineValues(0L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void nullableSumAndCount() throws Exception {
    final String query = String.format("SELECT " +
        "COUNT(position_id) AS c, " +
        "SUM(CAST((CASE WHEN position_id = 1 THEN NULL ELSE position_id END) AS INT)) AS p " +
        "FROM %s", viewName);

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("c"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("p"), Types.optional(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("c", "p")
        .baselineValues(1L, null)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void castSum() throws Exception {
    final String query = "SELECT CAST(SUM(position_id) AS INT) AS s FROM cp.`employee.json`";

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("s"), Types.optional(TypeProtos.MinorType.INT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("s")
        .baselineValues(18422)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void sumCast() throws Exception {
    final String query = "SELECT SUM(CAST(position_id AS INT)) AS s FROM cp.`employee.json`";

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("s"), Types.optional(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("s")
        .baselineValues(18422L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void sumsAndCounts1() throws Exception {
    final String query = String.format("SELECT " +
        "COUNT(*) as cs, " +
        "COUNT(1) as c1, " +
        "COUNT(employee_id) as cc, " +
        "SUM(1) as s1," +
        "department_id " +
        " FROM %s GROUP BY department_id", viewName);

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("cs"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("c1"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("cc"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("s1"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("department_id"), Types.optional(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("cs", "c1", "cc", "s1", "department_id")
        .baselineValues(1L, 1L, 1L, 1L, 1L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void sumsAndCounts2() throws Exception {
    final String query = "SELECT " +
        "SUM(1) as s1, " +
        "COUNT(1) as c1, " +
        "COUNT(*) as cs, " +
        "COUNT(CAST(n_regionkey AS INT)) as cc " +
        "FROM cp.`tpch/nation.parquet` " +
        "GROUP BY CAST(n_regionkey AS INT)";

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("s1"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("c1"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("cs"), Types.required(TypeProtos.MinorType.BIGINT)),
        Pair.of(SchemaPath.getSimplePath("cc"), Types.required(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("s1", "c1", "cs", "cc")
        .baselineValues(5L, 5L, 5L, 5L)
        .baselineValues(5L, 5L, 5L, 5L)
        .baselineValues(5L, 5L, 5L, 5L)
        .baselineValues(5L, 5L, 5L, 5L)
        .baselineValues(5L, 5L, 5L, 5L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);

  }

  @Test
  public void rank() throws Exception {
    final String query = "SELECT RANK() OVER(PARTITION BY employee_id ORDER BY employee_id) AS r FROM " + viewName;

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("r"), Types.required(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .ordered()
        .baselineColumns("r")
        .baselineValues(1L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  // -------------------- SCALAR FUNC. QUERIES --------------------

  @Test
  public void cast() throws Exception {
    final String query = String.format("SELECT CAST(fsalary AS DOUBLE) AS d," +
        "CAST(employee_id AS BIGINT) AS e FROM %s", viewName);

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("d"), Types.optional(TypeProtos.MinorType.FLOAT8)),
        Pair.of(SchemaPath.getSimplePath("e"), Types.optional(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .baselineColumns("d", "e")
        .ordered()
        .baselineValues(80_000D, 1L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  public void concatTest(final String query, int precision, boolean isNullable) throws Exception {
    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema =
        Lists.newArrayList(Pair.of(SchemaPath.getSimplePath("c"), Types.withPrecision(TypeProtos.MinorType.VARCHAR,
            isNullable ? TypeProtos.DataMode.OPTIONAL : TypeProtos.DataMode.REQUIRED, precision)));

    testBuilder()
        .sqlQuery(query)
        .baselineColumns("c")
        .ordered()
        .baselineValues("Sheri NowmerGraduate Degree")
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void concat() throws Exception {
    concatTest("SELECT CONCAT(full_name, education_level) AS c FROM " + viewName, 85, false);
  }

  @Test
  public void concatOp() throws Exception {
    concatTest("SELECT full_name || education_level AS c FROM " + viewName, 85, true);
  }

  @Test
  public void extract() throws Exception {
    final String query = "SELECT EXTRACT(YEAR FROM hire_date) AS e FROM " + viewName;

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("e"), Types.optional(TypeProtos.MinorType.BIGINT)));

    testBuilder()
        .sqlQuery(query)
        .baselineColumns("e")
        .ordered()
        .baselineValues(1994L)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void binary() throws Exception {
    final String query = "SELECT " +
        "single AND true AS b, " +
        "full_name || education_level AS c, " +
        "position_id / position_id AS d, " +
        "position_id = position_id AS e, " +
        "position_id > position_id AS g, " +
        "position_id >= position_id AS ge, " +
        "position_id IN (0, 1) AS i, +" +
        "position_id < position_id AS l, " +
        "position_id <= position_id AS le, " +
        "position_id - position_id AS m, " +
        "position_id * position_id AS mu, " +
        "position_id <> position_id AS n, " +
        "single OR false AS o, " +
        "position_id + position_id AS p FROM " + viewName;

    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("b"), Types.required(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("c"), Types.withPrecision(TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL, 85)),
        Pair.of(SchemaPath.getSimplePath("d"), Types.optional(TypeProtos.MinorType.INT)),
        Pair.of(SchemaPath.getSimplePath("e"), Types.optional(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("g"), Types.optional(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("ge"), Types.optional(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("i"), Types.optional(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("l"), Types.optional(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("le"), Types.optional(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("m"), Types.optional(TypeProtos.MinorType.INT)),
        Pair.of(SchemaPath.getSimplePath("mu"), Types.optional(TypeProtos.MinorType.INT)),
        Pair.of(SchemaPath.getSimplePath("n"), Types.optional(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("o"), Types.required(TypeProtos.MinorType.BIT)),
        Pair.of(SchemaPath.getSimplePath("p"), Types.optional(TypeProtos.MinorType.INT)));

    testBuilder()
        .sqlQuery(query)
        .baselineColumns("b", "c", "d", "e", "g", "ge", "i", "l", "le", "m", "mu", "n", "o", "p")
        .ordered()
        .baselineValues(true, "Sheri NowmerGraduate Degree", 1, true, false, true, true, false, true,
            0, 1, false, true, 2)
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  public void substringTest(final String query, int precision) throws Exception {
    @SuppressWarnings("unchecked")
    final List<Pair<SchemaPath, TypeProtos.MajorType>> expectedSchema = Lists.newArrayList(
        Pair.of(SchemaPath.getSimplePath("s"), Types.withPrecision(TypeProtos.MinorType.VARCHAR, TypeProtos.DataMode.OPTIONAL, precision)));

    testBuilder()
        .sqlQuery(query)
        .baselineColumns("s")
        .ordered()
        .baselineValues("Sheri")
        .go();

    testBuilder()
        .sqlQuery(wrapLimit0(query))
        .schemaBaseLine(expectedSchema)
        .go();

    checkThatQueryPlanIsOptimized(query);
  }

  @Test
  public void substring() throws Exception {
    substringTest("SELECT SUBSTRING(full_name, 1, 5) AS s FROM " + viewName, Types.MAX_VARCHAR_LENGTH);
  }

  @Test
  public void substr() throws Exception {
    substringTest("SELECT SUBSTR(full_name, 1, 5) AS s FROM " + viewName, Types.MAX_VARCHAR_LENGTH);
  }
}
