/*
 * 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.calcite.test;

import org.apache.calcite.config.CalciteConnectionConfig;
import org.apache.calcite.linq4j.tree.Expression;
import org.apache.calcite.plan.Context;
import org.apache.calcite.plan.Contexts;
import org.apache.calcite.plan.RelOptCluster;
import org.apache.calcite.plan.RelOptPlanner;
import org.apache.calcite.plan.RelOptSchema;
import org.apache.calcite.plan.RelOptSchemaWithSampling;
import org.apache.calcite.plan.RelOptTable;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.prepare.Prepare;
import org.apache.calcite.rel.RelCollation;
import org.apache.calcite.rel.RelCollations;
import org.apache.calcite.rel.RelDistribution;
import org.apache.calcite.rel.RelDistributions;
import org.apache.calcite.rel.RelFieldCollation;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelReferentialConstraint;
import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.RelShuttle;
import org.apache.calcite.rel.core.Correlate;
import org.apache.calcite.rel.core.CorrelationId;
import org.apache.calcite.rel.core.JoinRelType;
import org.apache.calcite.rel.core.RelFactories;
import org.apache.calcite.rel.logical.LogicalTableScan;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rel.type.RelDataTypeSystem;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.schema.ColumnStrategy;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperatorTable;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.test.SqlTestFactory;
import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
import org.apache.calcite.sql.validate.SqlConformance;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.sql.validate.SqlMonotonicity;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorCatalogReader;
import org.apache.calcite.sql.validate.SqlValidatorImpl;
import org.apache.calcite.sql.validate.SqlValidatorTable;
import org.apache.calcite.sql2rel.RelFieldTrimmer;
import org.apache.calcite.sql2rel.SqlToRelConverter;
import org.apache.calcite.sql2rel.StandardConvertletTable;
import org.apache.calcite.test.catalog.MockCatalogReader;
import org.apache.calcite.test.catalog.MockCatalogReaderDynamic;
import org.apache.calcite.test.catalog.MockCatalogReaderSimple;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.TestUtil;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
 * SqlToRelTestBase is an abstract base for tests which involve conversion from
 * SQL to relational algebra.
 *
 * <p>SQL statements to be translated can use the schema defined in
 * {@link MockCatalogReader}; note that this is slightly different from
 * Farrago's SALES schema. If you get a parser or validator error from your test
 * SQL, look down in the stack until you see "Caused by", which will usually
 * tell you the real error.
 */
public abstract class SqlToRelTestBase {
  //~ Static fields/initializers ---------------------------------------------

  protected static final String NL = System.getProperty("line.separator");

  //~ Instance fields --------------------------------------------------------

  protected final Tester tester = createTester();
  // Same as tester but without implicit type coercion.
  protected final Tester strictTester = tester.enableTypeCoercion(false);

  protected Tester createTester() {
    return new TesterImpl(getDiffRepos(), false, false, true, false,
        true, null, null, SqlToRelConverter.Config.DEFAULT,
        SqlConformanceEnum.DEFAULT, Contexts.empty());
  }

  protected Tester createTester(SqlConformance conformance) {
    return new TesterImpl(getDiffRepos(), false, false, true, false,
        true, null, null, SqlToRelConverter.Config.DEFAULT, conformance, Contexts.empty());
  }

  protected Tester getTesterWithDynamicTable() {
    return tester.withCatalogReaderFactory(MockCatalogReaderDynamic::new);
  }

  /**
   * Returns the default diff repository for this test, or null if there is
   * no repository.
   *
   * <p>The default implementation returns null.
   *
   * <p>Sub-classes that want to use a diff repository can override.
   * Sub-sub-classes can override again, inheriting test cases and overriding
   * selected test results.
   *
   * <p>And individual test cases can override by providing a different
   * tester object.
   *
   * @return Diff repository
   */
  protected DiffRepository getDiffRepos() {
    return null;
  }

  /**
   * Checks that every node of a relational expression is valid.
   *
   * @param rel Relational expression
   */
  public static void assertValid(RelNode rel) {
    SqlToRelConverterTest.RelValidityChecker checker =
        new SqlToRelConverterTest.RelValidityChecker();
    checker.go(rel);
    assertEquals(0, checker.invalidCount);
  }

  //~ Inner Interfaces -------------------------------------------------------

  /**
   * Helper class which contains default implementations of methods used for
   * running sql-to-rel conversion tests.
   */
  public interface Tester {
    /**
     * Converts a SQL string to a {@link RelNode} tree.
     *
     * @param sql SQL statement
     * @return Relational expression, never null
     */
    RelRoot convertSqlToRel(String sql);

    SqlNode parseQuery(String sql) throws Exception;

    /**
     * Factory method to create a {@link SqlValidator}.
     */
    SqlValidator createValidator(
        SqlValidatorCatalogReader catalogReader,
        RelDataTypeFactory typeFactory);

    /**
     * Factory method for a
     * {@link org.apache.calcite.prepare.Prepare.CatalogReader}.
     */
    Prepare.CatalogReader createCatalogReader(
        RelDataTypeFactory typeFactory);

    RelOptPlanner createPlanner();

    /**
     * Returns the {@link SqlOperatorTable} to use.
     */
    SqlOperatorTable getOperatorTable();

    /**
     * Returns the SQL dialect to test.
     */
    SqlConformance getConformance();

    /**
     * Checks that a SQL statement converts to a given plan.
     *
     * @param sql  SQL query
     * @param plan Expected plan
     */
    void assertConvertsTo(
        String sql,
        String plan);

    /**
     * Checks that a SQL statement converts to a given plan, optionally
     * trimming columns that are not needed.
     *
     * @param sql  SQL query
     * @param plan Expected plan
     * @param trim Whether to trim columns that are not needed
     */
    void assertConvertsTo(
        String sql,
        String plan,
        boolean trim);

    /**
     * Returns the diff repository.
     *
     * @return Diff repository
     */
    DiffRepository getDiffRepos();

    /**
     * Returns the validator.
     *
     * @return Validator
     */
    SqlValidator getValidator();

    /** Returns a tester that optionally decorrelates queries. */
    Tester withDecorrelation(boolean enable);

    /** Returns a tester that optionally decorrelates queries after planner
     * rules have fired. */
    Tester withLateDecorrelation(boolean enable);

    /** Returns a tester that optionally expands sub-queries.
     * If {@code expand} is false, the plan contains a
     * {@link org.apache.calcite.rex.RexSubQuery} for each sub-query.
     *
     * @see Prepare#THREAD_EXPAND */
    Tester withExpand(boolean expand);

    /** Returns a tester that optionally uses a
     * {@code SqlToRelConverter.Config}. */
    Tester withConfig(SqlToRelConverter.Config config);

    /** Returns a tester with a {@link SqlConformance}. */
    Tester withConformance(SqlConformance conformance);

    /** Returns a tester with a specified if allows type coercion. */
    Tester enableTypeCoercion(boolean typeCoercion);

    Tester withCatalogReaderFactory(
        SqlTestFactory.MockCatalogReaderFactory factory);

    /** Returns a tester that optionally trims unused fields. */
    Tester withTrim(boolean enable);

    Tester withClusterFactory(Function<RelOptCluster, RelOptCluster> function);

    boolean isLateDecorrelate();

    /** Returns a tester that uses a given context. */
    Tester withContext(Context context);
  }

  //~ Inner Classes ----------------------------------------------------------

  /**
   * Mock implementation of {@link RelOptSchema}.
   */
  protected static class MockRelOptSchema implements RelOptSchemaWithSampling {
    private final SqlValidatorCatalogReader catalogReader;
    private final RelDataTypeFactory typeFactory;

    public MockRelOptSchema(
        SqlValidatorCatalogReader catalogReader,
        RelDataTypeFactory typeFactory) {
      this.catalogReader = catalogReader;
      this.typeFactory = typeFactory;
    }

    public RelOptTable getTableForMember(List<String> names) {
      final SqlValidatorTable table =
          catalogReader.getTable(names);
      final RelDataType rowType = table.getRowType();
      final List<RelCollation> collationList = deduceMonotonicity(table);
      if (names.size() < 3) {
        String[] newNames2 = {"CATALOG", "SALES", ""};
        List<String> newNames = new ArrayList<>();
        int i = 0;
        while (newNames.size() < newNames2.length) {
          newNames.add(i, newNames2[i]);
          ++i;
        }
        names = newNames;
      }
      return createColumnSet(table, names, rowType, collationList);
    }

    private List<RelCollation> deduceMonotonicity(SqlValidatorTable table) {
      final RelDataType rowType = table.getRowType();
      final List<RelCollation> collationList = new ArrayList<>();

      // Deduce which fields the table is sorted on.
      int i = -1;
      for (RelDataTypeField field : rowType.getFieldList()) {
        ++i;
        final SqlMonotonicity monotonicity =
            table.getMonotonicity(field.getName());
        if (monotonicity != SqlMonotonicity.NOT_MONOTONIC) {
          final RelFieldCollation.Direction direction =
              monotonicity.isDecreasing()
                  ? RelFieldCollation.Direction.DESCENDING
                  : RelFieldCollation.Direction.ASCENDING;
          collationList.add(
              RelCollations.of(new RelFieldCollation(i, direction)));
        }
      }
      return collationList;
    }

    public RelOptTable getTableForMember(
        List<String> names,
        final String datasetName,
        boolean[] usedDataset) {
      final RelOptTable table = getTableForMember(names);

      // If they're asking for a sample, just for test purposes,
      // assume there's a table called "<table>:<sample>".
      RelOptTable datasetTable =
          new DelegatingRelOptTable(table) {
            public List<String> getQualifiedName() {
              final List<String> list =
                  new ArrayList<>(super.getQualifiedName());
              list.set(
                  list.size() - 1,
                  list.get(list.size() - 1) + ":" + datasetName);
              return ImmutableList.copyOf(list);
            }
          };
      if (usedDataset != null) {
        assert usedDataset.length == 1;
        usedDataset[0] = true;
      }
      return datasetTable;
    }

    protected MockColumnSet createColumnSet(
        SqlValidatorTable table,
        List<String> names,
        final RelDataType rowType,
        final List<RelCollation> collationList) {
      return new MockColumnSet(names, rowType, collationList);
    }

    public RelDataTypeFactory getTypeFactory() {
      return typeFactory;
    }

    public void registerRules(RelOptPlanner planner) throws Exception {
    }

    /** Mock column set. */
    protected class MockColumnSet implements RelOptTable {
      private final List<String> names;
      private final RelDataType rowType;
      private final List<RelCollation> collationList;

      protected MockColumnSet(
          List<String> names,
          RelDataType rowType,
          final List<RelCollation> collationList) {
        this.names = ImmutableList.copyOf(names);
        this.rowType = rowType;
        this.collationList = collationList;
      }

      public <T> T unwrap(Class<T> clazz) {
        if (clazz.isInstance(this)) {
          return clazz.cast(this);
        }
        return null;
      }

      public List<String> getQualifiedName() {
        return names;
      }

      public double getRowCount() {
        // use something other than 0 to give costing tests
        // some room, and make emps bigger than depts for
        // join asymmetry
        if (Iterables.getLast(names).equals("EMP")) {
          return 1000;
        } else {
          return 100;
        }
      }

      public RelDataType getRowType() {
        return rowType;
      }

      public RelOptSchema getRelOptSchema() {
        return MockRelOptSchema.this;
      }

      public RelNode toRel(ToRelContext context) {
        return LogicalTableScan.create(context.getCluster(), this,
            context.getTableHints());
      }

      public List<RelCollation> getCollationList() {
        return collationList;
      }

      public RelDistribution getDistribution() {
        return RelDistributions.BROADCAST_DISTRIBUTED;
      }

      public boolean isKey(ImmutableBitSet columns) {
        return false;
      }

      public List<ImmutableBitSet> getKeys() {
        return ImmutableList.of();
      }

      public List<RelReferentialConstraint> getReferentialConstraints() {
        return ImmutableList.of();
      }

      public List<ColumnStrategy> getColumnStrategies() {
        throw new UnsupportedOperationException();
      }

      public Expression getExpression(Class clazz) {
        return null;
      }

      public RelOptTable extend(List<RelDataTypeField> extendedFields) {
        final RelDataType extendedRowType =
            getRelOptSchema().getTypeFactory().builder()
                .addAll(rowType.getFieldList())
                .addAll(extendedFields)
                .build();
        return new MockColumnSet(names, extendedRowType, collationList);
      }
    }
  }

  /** Table that delegates to a given table. */
  private static class DelegatingRelOptTable implements RelOptTable {
    private final RelOptTable parent;

    DelegatingRelOptTable(RelOptTable parent) {
      this.parent = parent;
    }

    public <T> T unwrap(Class<T> clazz) {
      if (clazz.isInstance(this)) {
        return clazz.cast(this);
      }
      return parent.unwrap(clazz);
    }

    public Expression getExpression(Class clazz) {
      return parent.getExpression(clazz);
    }

    public RelOptTable extend(List<RelDataTypeField> extendedFields) {
      return parent.extend(extendedFields);
    }

    public List<String> getQualifiedName() {
      return parent.getQualifiedName();
    }

    public double getRowCount() {
      return parent.getRowCount();
    }

    public RelDataType getRowType() {
      return parent.getRowType();
    }

    public RelOptSchema getRelOptSchema() {
      return parent.getRelOptSchema();
    }

    public RelNode toRel(ToRelContext context) {
      return LogicalTableScan.create(context.getCluster(), this,
          context.getTableHints());
    }

    public List<RelCollation> getCollationList() {
      return parent.getCollationList();
    }

    public RelDistribution getDistribution() {
      return parent.getDistribution();
    }

    public boolean isKey(ImmutableBitSet columns) {
      return parent.isKey(columns);
    }

    public List<ImmutableBitSet> getKeys() {
      return parent.getKeys();
    }

    public List<RelReferentialConstraint> getReferentialConstraints() {
      return parent.getReferentialConstraints();
    }

    public List<ColumnStrategy> getColumnStrategies() {
      return parent.getColumnStrategies();
    }
  }

  /**
   * Default implementation of {@link Tester}, using mock classes
   * {@link MockRelOptSchema} and {@link MockRelOptPlanner}.
   */
  public static class TesterImpl implements Tester {
    private RelOptPlanner planner;
    private SqlOperatorTable opTab;
    private final DiffRepository diffRepos;
    private final boolean enableDecorrelate;
    private final boolean enableLateDecorrelate;
    private final boolean enableTrim;
    private final boolean enableExpand;
    private final boolean enableTypeCoercion;
    private final SqlConformance conformance;
    private final SqlTestFactory.MockCatalogReaderFactory catalogReaderFactory;
    private final Function<RelOptCluster, RelOptCluster> clusterFactory;
    private RelDataTypeFactory typeFactory;
    public final SqlToRelConverter.Config config;
    private final Context context;

    /**
     * Creates a TesterImpl.
     *
     * @param diffRepos Diff repository
     * @param enableDecorrelate Whether to decorrelate
     * @param enableTrim Whether to trim unused fields
     * @param enableExpand Whether to expand sub-queries
     * @param catalogReaderFactory Function to create catalog reader, or null
     * @param clusterFactory Called after a cluster has been created
     */
    protected TesterImpl(DiffRepository diffRepos, boolean enableDecorrelate,
        boolean enableTrim, boolean enableExpand,
        boolean enableLateDecorrelate,
        boolean enableTypeCoercion,
        SqlTestFactory.MockCatalogReaderFactory
            catalogReaderFactory,
        Function<RelOptCluster, RelOptCluster> clusterFactory) {
      this(diffRepos, enableDecorrelate, enableTrim, enableExpand,
          enableLateDecorrelate,
          enableTypeCoercion,
          catalogReaderFactory,
          clusterFactory,
          SqlToRelConverter.Config.DEFAULT,
          SqlConformanceEnum.DEFAULT,
          Contexts.empty());
    }

    protected TesterImpl(DiffRepository diffRepos, boolean enableDecorrelate,
        boolean enableTrim, boolean enableExpand, boolean enableLateDecorrelate,
        boolean enableTypeCoercion,
        SqlTestFactory.MockCatalogReaderFactory catalogReaderFactory,
        Function<RelOptCluster, RelOptCluster> clusterFactory,
        SqlToRelConverter.Config config, SqlConformance conformance,
        Context context) {
      this.diffRepos = diffRepos;
      this.enableDecorrelate = enableDecorrelate;
      this.enableTrim = enableTrim;
      this.enableExpand = enableExpand;
      this.enableLateDecorrelate = enableLateDecorrelate;
      this.enableTypeCoercion = enableTypeCoercion;
      this.catalogReaderFactory = catalogReaderFactory;
      this.clusterFactory = clusterFactory;
      this.config = config;
      this.conformance = conformance;
      this.context = context;
    }

    public RelRoot convertSqlToRel(String sql) {
      Objects.requireNonNull(sql);
      final SqlNode sqlQuery;
      try {
        sqlQuery = parseQuery(sql);
      } catch (RuntimeException | Error e) {
        throw e;
      } catch (Exception e) {
        throw TestUtil.rethrow(e);
      }
      final RelDataTypeFactory typeFactory = getTypeFactory();
      final Prepare.CatalogReader catalogReader =
          createCatalogReader(typeFactory);
      final SqlValidator validator =
          createValidator(
              catalogReader, typeFactory);
      final CalciteConnectionConfig calciteConfig = context.unwrap(CalciteConnectionConfig.class);
      if (calciteConfig != null) {
        validator.transform(config ->
            config.withDefaultNullCollation(calciteConfig.defaultNullCollation()));
      }
      final SqlToRelConverter.Config localConfig;
      if (config.equals(SqlToRelConverter.Config.DEFAULT)) {
        localConfig = SqlToRelConverter.configBuilder()
            .withTrimUnusedFields(true).withExpand(enableExpand).build();
      } else {
        localConfig = config;
      }

      final SqlToRelConverter converter =
          createSqlToRelConverter(
              validator,
              catalogReader,
              typeFactory,
              localConfig);

      final SqlNode validatedQuery = validator.validate(sqlQuery);
      RelRoot root =
          converter.convertQuery(validatedQuery, false, true);
      assert root != null;
      if (enableDecorrelate || enableTrim) {
        root = root.withRel(converter.flattenTypes(root.rel, true));
      }
      if (enableDecorrelate) {
        root = root.withRel(converter.decorrelate(sqlQuery, root.rel));
      }
      if (enableTrim) {
        root = root.withRel(converter.trimUnusedFields(true, root.rel));
      }
      return root;
    }

    protected SqlToRelConverter createSqlToRelConverter(
        final SqlValidator validator,
        final Prepare.CatalogReader catalogReader,
        final RelDataTypeFactory typeFactory,
        final SqlToRelConverter.Config config) {
      final RexBuilder rexBuilder = new RexBuilder(typeFactory);
      RelOptCluster cluster =
          RelOptCluster.create(getPlanner(), rexBuilder);
      if (clusterFactory != null) {
        cluster = clusterFactory.apply(cluster);
      }
      RelOptTable.ViewExpander viewExpander =
          new MockViewExpander(validator, catalogReader, cluster, config);
      return new SqlToRelConverter(viewExpander, validator, catalogReader, cluster,
          StandardConvertletTable.INSTANCE, config);
    }

    protected final RelDataTypeFactory getTypeFactory() {
      if (typeFactory == null) {
        typeFactory = createTypeFactory();
      }
      return typeFactory;
    }

    protected RelDataTypeFactory createTypeFactory() {
      return new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
    }

    protected final RelOptPlanner getPlanner() {
      if (planner == null) {
        planner = createPlanner();
      }
      return planner;
    }

    public SqlNode parseQuery(String sql) throws Exception {
      final SqlParser.Config config =
          SqlParser.configBuilder().setConformance(getConformance()).build();
      SqlParser parser = SqlParser.create(sql, config);
      return parser.parseQuery();
    }

    public SqlConformance getConformance() {
      return conformance;
    }

    public SqlValidator createValidator(
        SqlValidatorCatalogReader catalogReader,
        RelDataTypeFactory typeFactory) {
      return new FarragoTestValidator(
          getOperatorTable(),
          catalogReader,
          typeFactory,
          SqlValidator.Config.DEFAULT
              .withSqlConformance(conformance)
              .withTypeCoercionEnabled(enableTypeCoercion)
              .withIdentifierExpansion(true));
    }

    public final SqlOperatorTable getOperatorTable() {
      if (opTab == null) {
        opTab = createOperatorTable();
      }
      return opTab;
    }

    /**
     * Creates an operator table.
     *
     * @return New operator table
     */
    protected SqlOperatorTable createOperatorTable() {
      final MockSqlOperatorTable opTab =
          new MockSqlOperatorTable(SqlStdOperatorTable.instance());
      MockSqlOperatorTable.addRamp(opTab);
      return opTab;
    }

    public Prepare.CatalogReader createCatalogReader(
        RelDataTypeFactory typeFactory) {
      MockCatalogReader catalogReader;
      if (this.catalogReaderFactory != null) {
        catalogReader = catalogReaderFactory.create(typeFactory, true);
      } else {
        catalogReader = new MockCatalogReaderSimple(typeFactory, true);
      }
      return catalogReader.init();
    }

    public RelOptPlanner createPlanner() {
      return new MockRelOptPlanner(context);
    }

    public void assertConvertsTo(
        String sql,
        String plan) {
      assertConvertsTo(sql, plan, false);
    }

    public void assertConvertsTo(
        String sql,
        String plan,
        boolean trim) {
      String sql2 = getDiffRepos().expand("sql", sql);
      RelNode rel = convertSqlToRel(sql2).project();

      assertNotNull(rel);
      assertValid(rel);

      if (trim) {
        final RelBuilder relBuilder =
            RelFactories.LOGICAL_BUILDER.create(rel.getCluster(), null);
        final RelFieldTrimmer trimmer = createFieldTrimmer(relBuilder);
        rel = trimmer.trim(rel);
        assertNotNull(rel);
        assertValid(rel);
      }

      // NOTE jvs 28-Mar-2006:  insert leading newline so
      // that plans come out nicely stacked instead of first
      // line immediately after CDATA start
      String actual = NL + RelOptUtil.toString(rel);
      diffRepos.assertEquals("plan", plan, actual);
    }

    /**
     * Creates a RelFieldTrimmer.
     *
     * @param relBuilder Builder
     * @return Field trimmer
     */
    public RelFieldTrimmer createFieldTrimmer(RelBuilder relBuilder) {
      return new RelFieldTrimmer(getValidator(), relBuilder);
    }

    public DiffRepository getDiffRepos() {
      return diffRepos;
    }

    public SqlValidator getValidator() {
      final RelDataTypeFactory typeFactory = getTypeFactory();
      final SqlValidatorCatalogReader catalogReader =
          createCatalogReader(typeFactory);
      return createValidator(catalogReader, typeFactory);
    }

    public TesterImpl withDecorrelation(boolean enableDecorrelate) {
      return this.enableDecorrelate == enableDecorrelate
          ? this
          : new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
              enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
              clusterFactory, config, conformance, context);
    }

    public Tester withLateDecorrelation(boolean enableLateDecorrelate) {
      return this.enableLateDecorrelate == enableLateDecorrelate
          ? this
          : new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
              enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
              clusterFactory, config, conformance, context);
    }

    public TesterImpl withConfig(SqlToRelConverter.Config config) {
      return this.config == config
          ? this
          : new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
              enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
              clusterFactory, config, conformance, context);
    }

    public Tester withTrim(boolean enableTrim) {
      return this.enableTrim == enableTrim
          ? this
          : new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
              enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
              clusterFactory, config, conformance, context);
    }

    public Tester withExpand(boolean enableExpand) {
      return this.enableExpand == enableExpand
          ? this
          : new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
              enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
              clusterFactory, config, conformance, context);
    }

    public Tester withConformance(SqlConformance conformance) {
      return new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
          enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
          clusterFactory, config, conformance, context);
    }

    public Tester enableTypeCoercion(boolean enableTypeCoercion) {
      return new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
          enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
          clusterFactory, config, conformance, context);
    }

    public Tester withCatalogReaderFactory(
        SqlTestFactory.MockCatalogReaderFactory factory) {
      return new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
          enableExpand, enableLateDecorrelate, enableTypeCoercion, factory,
          clusterFactory, config, conformance, context);
    }

    public Tester withClusterFactory(
        Function<RelOptCluster, RelOptCluster> clusterFactory) {
      return new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
          enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
          clusterFactory, config, conformance, context);
    }

    public Tester withContext(Context context) {
      return new TesterImpl(diffRepos, enableDecorrelate, enableTrim,
          enableExpand, enableLateDecorrelate, enableTypeCoercion, catalogReaderFactory,
          clusterFactory, config, conformance, context);
    }

    public boolean isLateDecorrelate() {
      return enableLateDecorrelate;
    }
  }

    /** Validator for testing. */
  private static class FarragoTestValidator extends SqlValidatorImpl {
    FarragoTestValidator(
        SqlOperatorTable opTab,
        SqlValidatorCatalogReader catalogReader,
        RelDataTypeFactory typeFactory,
        Config config) {
      super(opTab, catalogReader, typeFactory, config);
    }
  }

  /**
   * {@link RelOptTable.ViewExpander} implementation for testing usage.
   */
  private static class MockViewExpander implements RelOptTable.ViewExpander {
    private final SqlValidator validator;
    private final Prepare.CatalogReader catalogReader;
    private final RelOptCluster cluster;
    private final SqlToRelConverter.Config config;

    MockViewExpander(
        SqlValidator validator,
        Prepare.CatalogReader catalogReader,
        RelOptCluster cluster,
        SqlToRelConverter.Config config) {
      this.validator = validator;
      this.catalogReader = catalogReader;
      this.cluster = cluster;
      this.config = config;
    }

    @Override public RelRoot expandView(RelDataType rowType, String queryString,
        List<String> schemaPath, List<String> viewPath) {
      try {
        SqlNode parsedNode = SqlParser.create(queryString).parseStmt();
        SqlNode validatedNode = validator.validate(parsedNode);
        SqlToRelConverter converter = new SqlToRelConverter(
            this, validator, catalogReader, cluster,
            StandardConvertletTable.INSTANCE, config);
        return converter.convertQuery(validatedNode, false, true);
      } catch (SqlParseException e) {
        throw new RuntimeException("Error happened while expanding view.", e);
      }
    }
  }

  /**
   * Custom implementation of Correlate for testing.
   */
  public static class CustomCorrelate extends Correlate {
    public CustomCorrelate(
        RelOptCluster cluster,
        RelTraitSet traits,
        RelNode left,
        RelNode right,
        CorrelationId correlationId,
        ImmutableBitSet requiredColumns,
        JoinRelType joinType) {
      super(cluster, traits, left, right, correlationId, requiredColumns, joinType);
    }

    @Override public Correlate copy(RelTraitSet traitSet,
        RelNode left, RelNode right, CorrelationId correlationId,
        ImmutableBitSet requiredColumns, JoinRelType joinType) {
      return new CustomCorrelate(getCluster(), traitSet, left, right,
          correlationId, requiredColumns, joinType);
    }

    @Override public RelNode accept(RelShuttle shuttle) {
      return shuttle.visit(this);
    }
  }
}