org.apache.calcite.test.CalciteAssert.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.calcite.test.CalciteAssert.java

Source

/*
 * 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.DataContext;
import org.apache.calcite.adapter.clone.CloneSchema;
import org.apache.calcite.adapter.java.ReflectiveSchema;
import org.apache.calcite.adapter.jdbc.JdbcSchema;
import org.apache.calcite.avatica.util.DateTimeUtils;
import org.apache.calcite.config.CalciteConnectionProperty;
import org.apache.calcite.config.Lex;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.jdbc.CalciteMetaImpl;
import org.apache.calcite.jdbc.CalciteSchema;
import org.apache.calcite.materialize.Lattice;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.runtime.FlatLists;
import org.apache.calcite.runtime.Hook;
import org.apache.calcite.schema.Schema;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.impl.AbstractSchema;
import org.apache.calcite.schema.impl.ViewTable;
import org.apache.calcite.util.Closer;
import org.apache.calcite.util.JsonBuilder;
import org.apache.calcite.util.Pair;
import org.apache.calcite.util.Util;

import org.apache.commons.lang3.StringUtils;

import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultiset;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.UncheckedExecutionException;

import net.hydromatic.foodmart.data.hsqldb.FoodmartHsqldb;
import net.hydromatic.scott.data.hsqldb.ScottHsqldb;

import org.hamcrest.Matcher;

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;
import javax.sql.DataSource;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

/**
 * Fluid DSL for testing Calcite connections and queries.
 */
public class CalciteAssert {
    private CalciteAssert() {
    }

    /** Which database to use for tests that require a JDBC data source. By
     * default the test suite runs against the embedded hsqldb database.
     *
     * <p>We recommend that casual users use hsqldb, and frequent Calcite
     * developers use MySQL. The test suite runs faster against the MySQL database
     * (mainly because of the 0.1s versus 6s startup time). You have to populate
     * MySQL manually with the foodmart data set, otherwise there will be test
     * failures.  To run against MySQL, specify '-Dcalcite.test.db=mysql' on the
     * java command line. */
    public static final DatabaseInstance DB = DatabaseInstance
            .valueOf(Util.first(System.getProperty("calcite.test.db"), "HSQLDB").toUpperCase(Locale.ROOT));

    /** Whether to enable slow tests. Default is false. */
    public static final boolean ENABLE_SLOW = Util.getBooleanProperty("calcite.test.slow");

    private static final DateFormat UTC_DATE_FORMAT;
    private static final DateFormat UTC_TIME_FORMAT;
    private static final DateFormat UTC_TIMESTAMP_FORMAT;
    static {
        final TimeZone utc = DateTimeUtils.GMT_ZONE;
        UTC_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT);
        UTC_DATE_FORMAT.setTimeZone(utc);
        UTC_TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.ROOT);
        UTC_TIME_FORMAT.setTimeZone(utc);
        UTC_TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT);
        UTC_TIMESTAMP_FORMAT.setTimeZone(utc);
    }

    public static final ConnectionFactory EMPTY_CONNECTION_FACTORY = new MapConnectionFactory(
            ImmutableMap.<String, String>of(), ImmutableList.<ConnectionPostProcessor>of());

    /** Implementation of {@link AssertThat} that does nothing. */
    private static final AssertThat DISABLED = new AssertThat(EMPTY_CONNECTION_FACTORY) {
        @Override
        public AssertThat with(Config config) {
            return this;
        }

        @Override
        public AssertThat with(ConnectionFactory connectionFactory) {
            return this;
        }

        @Override
        public AssertThat with(String property, Object value) {
            return this;
        }

        @Override
        public AssertThat withSchema(String name, Schema schema) {
            return this;
        }

        @Override
        public AssertQuery query(String sql) {
            return NopAssertQuery.of(sql);
        }

        @Override
        public AssertThat connectThrows(Function<Throwable, Void> exceptionChecker) {
            return this;
        }

        @Override
        public <T> AssertThat doWithConnection(Function<CalciteConnection, T> fn) throws Exception {
            return this;
        }

        @Override
        public AssertThat withDefaultSchema(String schema) {
            return this;
        }

        @Override
        public AssertThat with(SchemaSpec... specs) {
            return this;
        }

        @Override
        public AssertThat with(Lex lex) {
            return this;
        }

        @Override
        public AssertThat with(ConnectionPostProcessor postProcessor) {
            return this;
        }

        @Override
        public AssertThat enable(boolean enabled) {
            return this;
        }

        @Override
        public AssertThat pooled() {
            return this;
        }
    };

    /** Creates an instance of {@code CalciteAssert} with the empty
     * configuration. */
    public static AssertThat that() {
        return AssertThat.EMPTY;
    }

    /** Creates an instance of {@code CalciteAssert} with a given
     * configuration. */
    public static AssertThat that(Config config) {
        return that().with(config);
    }

    /** Short-hand for
     *  {@code CalciteAssert.that().with(Config.EMPTY).withModel(model)}. */
    public static AssertThat model(String model) {
        return that().withModel(model);
    }

    /** Short-hand for {@code CalciteAssert.that().with(Config.REGULAR)}. */
    public static AssertThat hr() {
        return that(Config.REGULAR);
    }

    static Function<RelNode, Void> checkRel(final String expected, final AtomicInteger counter) {
        return new Function<RelNode, Void>() {
            public Void apply(RelNode relNode) {
                if (counter != null) {
                    counter.incrementAndGet();
                }
                String s = Util.toLinux(RelOptUtil.toString(relNode));
                assertThat(s, containsString(expected));
                return null;
            }
        };
    }

    static Function<Throwable, Void> checkException(final String expected) {
        return new Function<Throwable, Void>() {
            public Void apply(Throwable p0) {
                assertNotNull("expected exception but none was thrown", p0);
                StringWriter stringWriter = new StringWriter();
                PrintWriter printWriter = new PrintWriter(stringWriter);
                p0.printStackTrace(printWriter);
                printWriter.flush();
                String stack = stringWriter.toString();
                assertTrue(stack, stack.contains(expected));
                return null;
            }
        };
    }

    static Function<ResultSet, Void> checkResult(final String expected) {
        return checkResult(expected, new ResultSetFormatter());
    }

    static Function<ResultSet, Void> checkResult(final String expected,
            final ResultSetFormatter resultSetFormatter) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet resultSet) {
                try {
                    resultSetFormatter.resultSet(resultSet);
                    assertEquals(expected, Util.toLinux(resultSetFormatter.string()));
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    static Function<ResultSet, Void> checkResultValue(final String expected) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet resultSet) {
                try {
                    if (!resultSet.next()) {
                        throw new AssertionError("too few rows");
                    }
                    if (resultSet.getMetaData().getColumnCount() != 1) {
                        throw new AssertionError("expected 1 column");
                    }
                    final String resultString = resultSet.getString(1);
                    assertEquals(expected, resultString == null ? null : Util.toLinux(resultString));
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    public static Function<ResultSet, Void> checkResultCount(final Matcher<Integer> expected) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet resultSet) {
                try {
                    final int count = CalciteAssert.countRows(resultSet);
                    assertThat(count, expected);
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    public static Function<Integer, Void> checkUpdateCount(final int expected) {
        return new Function<Integer, Void>() {
            public Void apply(Integer updateCount) {
                assertThat(updateCount, is(expected));
                return null;
            }
        };
    }

    /** Checks that the result of the second and subsequent executions is the same
     * as the first.
     *
     * @param ordered Whether order should be the same both times
     */
    static Function<ResultSet, Void> consistentResult(final boolean ordered) {
        return new Function<ResultSet, Void>() {
            int executeCount = 0;
            Collection expected;

            public Void apply(ResultSet resultSet) {
                ++executeCount;
                try {
                    final Collection result = CalciteAssert.toStringList(resultSet,
                            ordered ? new ArrayList<String>() : new TreeSet<String>());
                    if (executeCount == 1) {
                        expected = result;
                    } else {
                        if (!expected.equals(result)) {
                            // compare strings to get better error message
                            assertThat(newlineList(result), equalTo(newlineList(expected)));
                            fail("oops");
                        }
                    }
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    static String newlineList(Collection collection) {
        final StringBuilder buf = new StringBuilder();
        for (Object o : collection) {
            buf.append(o).append('\n');
        }
        return buf.toString();
    }

    /** @see Matchers#returnsUnordered(String...) */
    static Function<ResultSet, Void> checkResultUnordered(final String... lines) {
        return checkResult(true, false, lines);
    }

    /** @see Matchers#returnsUnordered(String...) */
    static Function<ResultSet, Void> checkResult(final boolean sort, final boolean head, final String... lines) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet resultSet) {
                try {
                    final List<String> expectedList = Lists.newArrayList(lines);
                    if (sort) {
                        Collections.sort(expectedList);
                    }
                    final List<String> actualList = Lists.newArrayList();
                    CalciteAssert.toStringList(resultSet, actualList);
                    if (sort) {
                        Collections.sort(actualList);
                    }
                    final List<String> trimmedActualList;
                    if (head && actualList.size() > expectedList.size()) {
                        trimmedActualList = actualList.subList(0, expectedList.size());
                    } else {
                        trimmedActualList = actualList;
                    }
                    if (!trimmedActualList.equals(expectedList)) {
                        assertThat(Util.lines(trimmedActualList), equalTo(Util.lines(expectedList)));
                    }
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    public static Function<ResultSet, Void> checkResultContains(final String expected) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet s) {
                try {
                    final String actual = Util.toLinux(CalciteAssert.toString(s));
                    assertThat(actual, containsString(expected));
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    public static Function<ResultSet, Void> checkResultContains(final String expected, final int count) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet s) {
                try {
                    final String actual = Util.toLinux(CalciteAssert.toString(s));
                    assertTrue(actual + " should have " + count + " occurrence of " + expected,
                            StringUtils.countMatches(actual, expected) == count);
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    public static Function<ResultSet, Void> checkMaskedResultContains(final String expected) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet s) {
                try {
                    final String actual = Util.toLinux(CalciteAssert.toString(s));
                    final String maskedActual = actual.replaceAll(", id = [0-9]+", "");
                    assertThat(maskedActual, containsString(expected));
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    public static Function<ResultSet, Void> checkResultType(final String expected) {
        return new Function<ResultSet, Void>() {
            public Void apply(ResultSet s) {
                try {
                    final String actual = typeString(s.getMetaData());
                    assertEquals(expected, actual);
                    return null;
                } catch (SQLException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }

    private static String typeString(ResultSetMetaData metaData) throws SQLException {
        final List<String> list = new ArrayList<>();
        for (int i = 0; i < metaData.getColumnCount(); i++) {
            list.add(metaData.getColumnName(i + 1) + " " + metaData.getColumnTypeName(i + 1)
                    + (metaData.isNullable(i + 1) == ResultSetMetaData.columnNoNulls ? " NOT NULL" : ""));
        }
        return list.toString();
    }

    static void assertQuery(Connection connection, String sql, int limit, boolean materializationsEnabled,
            List<Pair<Hook, Function>> hooks, Function<ResultSet, Void> resultChecker,
            Function<Integer, Void> updateChecker, Function<Throwable, Void> exceptionChecker) throws Exception {
        final String message = "With materializationsEnabled=" + materializationsEnabled + ", limit=" + limit;
        try (final Closer closer = new Closer()) {
            if (connection instanceof CalciteConnection) {
                CalciteConnection calciteConnection = (CalciteConnection) connection;
                calciteConnection.getProperties().setProperty(
                        CalciteConnectionProperty.MATERIALIZATIONS_ENABLED.camelName(),
                        Boolean.toString(materializationsEnabled));
                calciteConnection.getProperties().setProperty(
                        CalciteConnectionProperty.CREATE_MATERIALIZATIONS.camelName(),
                        Boolean.toString(materializationsEnabled));
            }
            for (Pair<Hook, Function> hook : hooks) {
                closer.add(hook.left.addThread(hook.right));
            }
            Statement statement = connection.createStatement();
            statement.setMaxRows(limit <= 0 ? limit : Math.max(limit, 1));
            ResultSet resultSet = null;
            Integer updateCount = null;
            try {
                if (updateChecker == null) {
                    resultSet = statement.executeQuery(sql);
                } else {
                    updateCount = statement.executeUpdate(sql);
                }
                if (exceptionChecker != null) {
                    exceptionChecker.apply(null);
                    return;
                }
            } catch (Exception | Error e) {
                if (exceptionChecker != null) {
                    exceptionChecker.apply(e);
                    return;
                }
                throw e;
            }
            if (resultChecker != null) {
                resultChecker.apply(resultSet);
            }
            if (updateChecker != null) {
                updateChecker.apply(updateCount);
            }
            if (resultSet != null) {
                resultSet.close();
            }
            statement.close();
            connection.close();
        } catch (Error | RuntimeException e) {
            // We ignore extended message for non-runtime exception, however
            // it does not matter much since it is better to have AssertionError
            // at the very top level of the exception stack.
            throw e;
        } catch (Throwable e) {
            throw new RuntimeException(message, e);
        }
    }

    static void assertPrepare(Connection connection, String sql, boolean materializationsEnabled,
            final Function<RelNode, Void> convertChecker, final Function<RelNode, Void> substitutionChecker)
            throws Exception {
        final String message = "With materializationsEnabled=" + materializationsEnabled;
        try (Closer closer = new Closer()) {
            if (convertChecker != null) {
                closer.add(Hook.TRIMMED.addThread(new Function<RelNode, Void>() {
                    public Void apply(RelNode rel) {
                        convertChecker.apply(rel);
                        return null;
                    }
                }));
            }
            if (substitutionChecker != null) {
                closer.add(Hook.SUB.addThread(new Function<RelNode, Void>() {
                    public Void apply(RelNode rel) {
                        substitutionChecker.apply(rel);
                        return null;
                    }
                }));
            }
            ((CalciteConnection) connection).getProperties().setProperty(
                    CalciteConnectionProperty.MATERIALIZATIONS_ENABLED.camelName(),
                    Boolean.toString(materializationsEnabled));
            ((CalciteConnection) connection).getProperties().setProperty(
                    CalciteConnectionProperty.CREATE_MATERIALIZATIONS.camelName(),
                    Boolean.toString(materializationsEnabled));
            PreparedStatement statement = connection.prepareStatement(sql);
            statement.close();
            connection.close();
        } catch (Throwable e) {
            throw new RuntimeException(message, e);
        }
    }

    /** Converts a {@link ResultSet} to a string. */
    static String toString(ResultSet resultSet) throws SQLException {
        return new ResultSetFormatter().resultSet(resultSet).string();
    }

    static int countRows(ResultSet resultSet) throws SQLException {
        int n = 0;
        while (resultSet.next()) {
            ++n;
        }
        return n;
    }

    static Collection<String> toStringList(ResultSet resultSet, Collection<String> list) throws SQLException {
        return new ResultSetFormatter().toStringList(resultSet, list);
    }

    static List<String> toList(ResultSet resultSet) throws SQLException {
        return (List<String>) toStringList(resultSet, new ArrayList<String>());
    }

    static ImmutableMultiset<String> toSet(ResultSet resultSet) throws SQLException {
        return ImmutableMultiset.copyOf(toList(resultSet));
    }

    /** Calls a non-static method via reflection. Useful for testing methods that
     * don't exist in certain versions of the JDK. */
    static Object call(Object o, String methodName, Object... args)
            throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return method(o, methodName, args).invoke(o, args);
    }

    /** Finds a non-static method based on its target, name and arguments.
     * Throws if not found. */
    static Method method(Object o, String methodName, Object[] args) {
        for (Class<?> aClass = o.getClass();;) {
            loop: for (Method method1 : aClass.getMethods()) {
                if (method1.getName().equals(methodName) && method1.getParameterTypes().length == args.length
                        && Modifier.isPublic(method1.getDeclaringClass().getModifiers())) {
                    for (Pair<Object, Class> pair : Pair.zip(args, (Class[]) method1.getParameterTypes())) {
                        if (!pair.right.isInstance(pair.left)) {
                            continue loop;
                        }
                    }
                    return method1;
                }
            }
            if (aClass.getSuperclass() != null && aClass.getSuperclass() != Object.class) {
                aClass = aClass.getSuperclass();
            } else {
                final Class<?>[] interfaces = aClass.getInterfaces();
                if (interfaces.length > 0) {
                    aClass = interfaces[0];
                } else {
                    break;
                }
            }
        }
        throw new AssertionError("method " + methodName + " not found");
    }

    public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
        SchemaPlus foodmart;
        SchemaPlus jdbcScott;
        final ConnectionSpec cs;
        final DataSource dataSource;
        switch (schema) {
        case REFLECTIVE_FOODMART:
            return rootSchema.add("foodmart", new ReflectiveSchema(new JdbcTest.FoodmartSchema()));
        case JDBC_SCOTT:
            cs = DatabaseInstance.HSQLDB.scott;
            dataSource = JdbcSchema.dataSource(cs.url, cs.driver, cs.username, cs.password);
            return rootSchema.add("JDBC_SCOTT",
                    JdbcSchema.create(rootSchema, "JDBC_SCOTT", dataSource, cs.catalog, cs.schema));
        case JDBC_FOODMART:
            cs = DB.foodmart;
            dataSource = JdbcSchema.dataSource(cs.url, cs.driver, cs.username, cs.password);
            return rootSchema.add("foodmart",
                    JdbcSchema.create(rootSchema, "foodmart", dataSource, cs.catalog, cs.schema));
        case JDBC_FOODMART_WITH_LATTICE:
            foodmart = rootSchema.getSubSchema("foodmart");
            if (foodmart == null) {
                foodmart = CalciteAssert.addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
            }
            foodmart.add("lattice", Lattice.create(foodmart.unwrap(CalciteSchema.class),
                    "select 1 from \"foodmart\".\"sales_fact_1997\" as s\n"
                            + "join \"foodmart\".\"time_by_day\" as t using (\"time_id\")\n"
                            + "join \"foodmart\".\"customer\" as c using (\"customer_id\")\n"
                            + "join \"foodmart\".\"product\" as p using (\"product_id\")\n"
                            + "join \"foodmart\".\"product_class\" as pc on p.\"product_class_id\" = pc.\"product_class_id\"",
                    true));
            return foodmart;
        case SCOTT:
            jdbcScott = rootSchema.getSubSchema("jdbc_scott");
            if (jdbcScott == null) {
                jdbcScott = CalciteAssert.addSchema(rootSchema, SchemaSpec.JDBC_SCOTT);
            }
            return rootSchema.add("scott", new CloneSchema(jdbcScott));
        case CLONE_FOODMART:
            foodmart = rootSchema.getSubSchema("foodmart");
            if (foodmart == null) {
                foodmart = CalciteAssert.addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
            }
            return rootSchema.add("foodmart2", new CloneSchema(foodmart));
        case HR:
            return rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
        case LINGUAL:
            return rootSchema.add("SALES", new ReflectiveSchema(new JdbcTest.LingualSchema()));
        case BLANK:
            return rootSchema.add("BLANK", new AbstractSchema());
        case ORINOCO:
            final SchemaPlus orinoco = rootSchema.add("ORINOCO", new AbstractSchema());
            orinoco.add("ORDERS",
                    new StreamTest.OrdersHistoryTable(StreamTest.OrdersStreamTableFactory.getRowList()));
            return orinoco;
        case POST:
            final SchemaPlus post = rootSchema.add("POST", new AbstractSchema());
            post.add("EMP", ViewTable.viewMacro(post,
                    "select * from (values\n" + "    ('Jane', 10, 'F'),\n" + "    ('Bob', 10, 'M'),\n"
                            + "    ('Eric', 20, 'M'),\n" + "    ('Susan', 30, 'F'),\n" + "    ('Alice', 30, 'F'),\n"
                            + "    ('Adam', 50, 'M'),\n" + "    ('Eve', 50, 'F'),\n" + "    ('Grace', 60, 'F'),\n"
                            + "    ('Wilma', cast(null as integer), 'F'))\n" + "  as t(ename, deptno, gender)",
                    ImmutableList.<String>of(), ImmutableList.of("POST", "EMP"), null));
            post.add("DEPT",
                    ViewTable.viewMacro(post,
                            "select * from (values\n" + "    (10, 'Sales'),\n" + "    (20, 'Marketing'),\n"
                                    + "    (30, 'Engineering'),\n" + "    (40, 'Empty')) as t(deptno, dname)",
                            ImmutableList.<String>of(), ImmutableList.of("POST", "DEPT"), null));
            post.add("EMPS", ViewTable.viewMacro(post, "select * from (values\n"
                    + "    (100, 'Fred',  10, CAST(NULL AS CHAR(1)), CAST(NULL AS VARCHAR(20)), 40,               25, TRUE,    FALSE, DATE '1996-08-03'),\n"
                    + "    (110, 'Eric',  20, 'M',                   'San Francisco',           3,                80, UNKNOWN, FALSE, DATE '2001-01-01'),\n"
                    + "    (110, 'John',  40, 'M',                   'Vancouver',               2, CAST(NULL AS INT), FALSE,   TRUE,  DATE '2002-05-03'),\n"
                    + "    (120, 'Wilma', 20, 'F',                   CAST(NULL AS VARCHAR(20)), 1,                 5, UNKNOWN, TRUE,  DATE '2005-09-07'),\n"
                    + "    (130, 'Alice', 40, 'F',                   'Vancouver',               2, CAST(NULL AS INT), FALSE,   TRUE,  DATE '2007-01-01'))\n"
                    + " as t(empno, name, deptno, gender, city, empid, age, slacker, manager, joinedat)",
                    ImmutableList.<String>of(), ImmutableList.of("POST", "EMPS"), null));
            return post;
        default:
            throw new AssertionError("unknown schema " + schema);
        }
    }

    /**
     * Asserts that two objects are equal. If they are not, an
     * {@link AssertionError} is thrown with the given message. If
     * <code>expected</code> and <code>actual</code> are <code>null</code>,
     * they are considered equal.
     *
     * <p>This method produces more user-friendly error messages than
     * {@link org.junit.Assert#assertArrayEquals(String, Object[], Object[])}
     *
     * @param message the identifying message for the {@link AssertionError} (<code>null</code>
     * okay)
     * @param expected expected value
     * @param actual actual value
     */
    public static void assertArrayEqual(String message, Object[] expected, Object[] actual) {
        Joiner joiner = Joiner.on('\n');
        String strExpected = expected == null ? null : joiner.join(expected);
        String strActual = actual == null ? null : joiner.join(actual);
        assertEquals(message, strExpected, strActual);
    }

    static <F, T> Function<F, T> constantNull() {
        //noinspection unchecked
        return (Function<F, T>) (Function) Functions.<T>constant(null);
    }

    /**
     * Result of calling {@link CalciteAssert#that}.
     */
    public static class AssertThat {
        private final ConnectionFactory connectionFactory;

        private static final AssertThat EMPTY = new AssertThat(EMPTY_CONNECTION_FACTORY);

        private AssertThat(ConnectionFactory connectionFactory) {
            this.connectionFactory = Preconditions.checkNotNull(connectionFactory);
        }

        public AssertThat with(Config config) {
            if (config == Config.SPARK) {
                return with("spark", "true");
            }

            switch (config) {
            case EMPTY:
                return EMPTY;
            case REGULAR:
                return with(SchemaSpec.HR, SchemaSpec.REFLECTIVE_FOODMART, SchemaSpec.POST);
            case REGULAR_PLUS_METADATA:
                return with(SchemaSpec.HR, SchemaSpec.REFLECTIVE_FOODMART);
            case LINGUAL:
                return with(SchemaSpec.LINGUAL);
            case JDBC_FOODMART:
                return with(CalciteAssert.SchemaSpec.JDBC_FOODMART);
            case FOODMART_CLONE:
                return with(SchemaSpec.CLONE_FOODMART);
            case JDBC_FOODMART_WITH_LATTICE:
                return with(SchemaSpec.JDBC_FOODMART_WITH_LATTICE);
            case JDBC_SCOTT:
                return with(SchemaSpec.JDBC_SCOTT);
            case SCOTT:
                return with(SchemaSpec.SCOTT);
            default:
                throw Util.unexpected(config);
            }
        }

        /** Creates a copy of this AssertThat, adding more schemas */
        public AssertThat with(SchemaSpec... specs) {
            AssertThat next = this;
            for (SchemaSpec spec : specs) {
                next = next.with(new AddSchemaSpecPostProcessor(spec));
            }
            return next;
        }

        /** Creates a copy of this AssertThat, overriding the connection factory. */
        public AssertThat with(ConnectionFactory connectionFactory) {
            return new AssertThat(connectionFactory);
        }

        public final AssertThat with(final Map<String, String> map) {
            AssertThat x = this;
            for (Map.Entry<String, String> entry : map.entrySet()) {
                x = with(entry.getKey(), entry.getValue());
            }
            return x;
        }

        public AssertThat with(String property, Object value) {
            return new AssertThat(connectionFactory.with(property, value));
        }

        /** Sets Lex property **/
        public AssertThat with(Lex lex) {
            return with(CalciteConnectionProperty.LEX.name(), lex.toString());
        }

        /** Sets the default schema to a given schema. */
        public AssertThat withSchema(String name, Schema schema) {
            return new AssertThat(connectionFactory.with(new AddSchemaPostProcessor(name, schema)));
        }

        public AssertThat with(ConnectionPostProcessor postProcessor) {
            return new AssertThat(connectionFactory.with(postProcessor));
        }

        public final AssertThat withModel(String model) {
            return with("model", "inline:" + model);
        }

        /** Adds materializations to the schema. */
        public final AssertThat withMaterializations(String model, final String... materializations) {
            return withMaterializations(model, new Function<JsonBuilder, List<Object>>() {
                public List<Object> apply(JsonBuilder builder) {
                    assert materializations.length % 2 == 0;
                    final List<Object> list = builder.list();
                    for (int i = 0; i < materializations.length; i++) {
                        String table = materializations[i++];
                        final Map<String, Object> map = builder.map();
                        map.put("table", table);
                        map.put("view", table + "v");
                        String sql = materializations[i];
                        final String sql2 = sql.replaceAll("`", "\"");
                        map.put("sql", sql2);
                        list.add(map);
                    }
                    return list;
                }
            });
        }

        /** Adds materializations to the schema. */
        public final AssertThat withMaterializations(String model,
                Function<JsonBuilder, List<Object>> materializations) {
            final JsonBuilder builder = new JsonBuilder();
            final List<Object> list = materializations.apply(builder);
            final String buf = "materializations: " + builder.toJsonString(list);
            final String model2;
            if (model.contains("defaultSchema: 'foodmart'")) {
                model2 = model.replace("]", ", { name: 'mat', " + buf + "}\n" + "]");
            } else if (model.contains("type: ")) {
                model2 = model.replace("type: ", buf + ",\n" + "type: ");
            } else {
                throw new AssertionError("do not know where to splice");
            }
            return withModel(model2);
        }

        public AssertQuery query(String sql) {
            return new AssertQuery(connectionFactory, sql);
        }

        /** Asserts that there is an exception with the given message while
         * creating a connection. */
        public AssertThat connectThrows(String message) {
            return connectThrows(checkException(message));
        }

        /** Asserts that there is an exception that matches the given predicate
         * while creating a connection. */
        public AssertThat connectThrows(Function<Throwable, Void> exceptionChecker) {
            Throwable throwable;
            try {
                Connection x = connectionFactory.createConnection();
                try {
                    x.close();
                } catch (SQLException e) {
                    // ignore
                }
                throwable = null;
            } catch (Throwable e) {
                throwable = e;
            }
            exceptionChecker.apply(throwable);
            return this;
        }

        /** Creates a {@link org.apache.calcite.jdbc.CalciteConnection}
         * and executes a callback. */
        public <T> AssertThat doWithConnection(Function<CalciteConnection, T> fn) throws Exception {
            try (Connection connection = connectionFactory.createConnection()) {
                T t = fn.apply((CalciteConnection) connection);
                Util.discard(t);
                return AssertThat.this;
            }
        }

        /** Creates a {@link DataContext} and executes a callback. */
        public <T> AssertThat doWithDataContext(Function<DataContext, T> fn) throws Exception {
            CalciteConnection connection = (CalciteConnection) connectionFactory.createConnection();
            final DataContext dataContext = CalciteMetaImpl.createDataContext(connection);
            try {
                T t = fn.apply(dataContext);
                Util.discard(t);
                return AssertThat.this;
            } finally {
                connection.close();
            }
        }

        public AssertThat withDefaultSchema(String schema) {
            return new AssertThat(connectionFactory.with(new AddSchemaPostProcessor(schema, null)));
        }

        /** Use sparingly. Does not close the connection. */
        public Connection connect() throws SQLException {
            return connectionFactory.createConnection();
        }

        public AssertThat enable(boolean enabled) {
            return enabled ? this : DISABLED;
        }

        /** Returns a version that uses a single connection, as opposed to creating
         * a new one each time a test method is invoked. */
        public AssertThat pooled() {
            if (connectionFactory instanceof PoolingConnectionFactory) {
                return this;
            } else {
                return new AssertThat(new PoolingConnectionFactory(connectionFactory));
            }
        }

        public AssertMetaData metaData(Function<Connection, ResultSet> function) {
            return new AssertMetaData(connectionFactory, function);
        }
    }

    /**
     * Abstract implementation of connection factory whose {@code with}
     * methods throw.
     *
     * <p>Avoid creating new sub-classes otherwise it would be hard to support
     * {@code .with(property, value).with(...)} kind of chains.
     *
     * <p>If you want augment the connection, use {@link ConnectionPostProcessor}.
     **/
    public abstract static class ConnectionFactory {
        public abstract Connection createConnection() throws SQLException;

        public ConnectionFactory with(String property, Object value) {
            throw new UnsupportedOperationException();
        }

        public ConnectionFactory with(ConnectionPostProcessor postProcessor) {
            throw new UnsupportedOperationException();
        }
    }

    /** Connection post processor */
    public interface ConnectionPostProcessor {
        Connection apply(Connection connection) throws SQLException;
    }

    /** Adds {@link Schema} and sets it as default. */
    public static class AddSchemaPostProcessor implements ConnectionPostProcessor {
        private final String name;
        private final Schema schema;

        public AddSchemaPostProcessor(String name, Schema schema) {
            this.name = name;
            this.schema = schema;
        }

        public Connection apply(Connection connection) throws SQLException {
            if (schema != null) {
                CalciteConnection con = connection.unwrap(CalciteConnection.class);
                SchemaPlus rootSchema = con.getRootSchema();
                rootSchema.add(name, schema);
            }
            connection.setSchema(name);
            return connection;
        }
    }

    /** Adds {@link SchemaSpec} (set of schemes) to a connection. */
    public static class AddSchemaSpecPostProcessor implements ConnectionPostProcessor {
        private final SchemaSpec schemaSpec;

        public AddSchemaSpecPostProcessor(SchemaSpec schemaSpec) {
            this.schemaSpec = schemaSpec;
        }

        public Connection apply(Connection connection) throws SQLException {
            CalciteConnection con = connection.unwrap(CalciteConnection.class);
            SchemaPlus rootSchema = con.getRootSchema();
            switch (schemaSpec) {
            case CLONE_FOODMART:
            case JDBC_FOODMART_WITH_LATTICE:
                addSchema(rootSchema, SchemaSpec.JDBC_FOODMART);
                /* fall through */
            default:
                addSchema(rootSchema, schemaSpec);
            }
            if (schemaSpec == SchemaSpec.CLONE_FOODMART) {
                con.setSchema("foodmart2");
            }
            return connection;
        }
    }

    /** Connection factory that uses the same instance of connections. */
    private static class PoolingConnectionFactory extends ConnectionFactory {

        /** Connection pool. */
        private static class Pool {
            private static final LoadingCache<ConnectionFactory, Connection> POOL = CacheBuilder.newBuilder()
                    .build(new CacheLoader<ConnectionFactory, Connection>() {
                        public Connection load(@Nonnull ConnectionFactory key) throws Exception {
                            return key.createConnection();
                        }
                    });
        }

        private final ConnectionFactory factory;

        public PoolingConnectionFactory(final ConnectionFactory factory) {
            this.factory = factory;
        }

        public Connection createConnection() throws SQLException {
            try {
                return Pool.POOL.get(factory);
            } catch (UncheckedExecutionException | ExecutionException e) {
                throw new SQLException("Unable to get pooled connection for " + factory, e.getCause());
            }
        }
    }

    /** Connection factory that uses a given map of (name, value) pairs and
     * optionally an initial schema. */
    private static class MapConnectionFactory extends ConnectionFactory {
        private final ImmutableMap<String, String> map;
        private final ImmutableList<ConnectionPostProcessor> postProcessors;

        private MapConnectionFactory(ImmutableMap<String, String> map,
                ImmutableList<ConnectionPostProcessor> postProcessors) {
            this.map = Preconditions.checkNotNull(map);
            this.postProcessors = Preconditions.checkNotNull(postProcessors);
        }

        @Override
        public boolean equals(Object obj) {
            return this == obj
                    || obj.getClass() == MapConnectionFactory.class && ((MapConnectionFactory) obj).map.equals(map)
                            && ((MapConnectionFactory) obj).postProcessors.equals(postProcessors);
        }

        @Override
        public int hashCode() {
            return Objects.hash(map, postProcessors);
        }

        public Connection createConnection() throws SQLException {
            final Properties info = new Properties();
            for (Map.Entry<String, String> entry : map.entrySet()) {
                info.setProperty(entry.getKey(), entry.getValue());
            }
            Connection connection = DriverManager.getConnection("jdbc:calcite:", info);
            for (ConnectionPostProcessor postProcessor : postProcessors) {
                connection = postProcessor.apply(connection);
            }
            return connection;
        }

        public ConnectionFactory with(String property, Object value) {
            return new MapConnectionFactory(FlatLists.append(this.map, property, value.toString()), postProcessors);
        }

        public ConnectionFactory with(ConnectionPostProcessor postProcessor) {
            ImmutableList.Builder<ConnectionPostProcessor> builder = ImmutableList.builder();
            builder.addAll(postProcessors);
            builder.add(postProcessor);
            return new MapConnectionFactory(map, builder.build());
        }
    }

    /** Fluent interface for building a query to be tested. */
    public static class AssertQuery {
        private final String sql;
        private ConnectionFactory connectionFactory;
        private String plan;
        private int limit;
        private boolean materializationsEnabled = false;
        private final List<Pair<Hook, Function>> hooks = Lists.newArrayList();

        private AssertQuery(ConnectionFactory connectionFactory, String sql) {
            this.sql = sql;
            this.connectionFactory = connectionFactory;
        }

        protected Connection createConnection() throws Exception {
            return connectionFactory.createConnection();
        }

        /** Performs an action using a connection, and closes the connection
         * afterwards. */
        public final AssertQuery withConnection(Function<Connection, Void> f) throws Exception {
            try (Connection c = createConnection()) {
                f.apply(c);
            }
            return this;
        }

        public AssertQuery enable(boolean enabled) {
            return enabled ? this : NopAssertQuery.of(sql);
        }

        public AssertQuery returns(String expected) {
            return returns(checkResult(expected));
        }

        /** Simlar to {@link #returns}, but trims a few values before comparing. */
        public AssertQuery returns2(final String expected) {
            return returns(checkResult(expected, new ResultSetFormatter() {
                @Override
                protected String adjustValue(String s) {
                    if (s != null) {
                        if (s.contains(".")) {
                            while (s.endsWith("0")) {
                                s = s.substring(0, s.length() - 1);
                            }
                            if (s.endsWith(".")) {
                                s = s.substring(0, s.length() - 1);
                            }
                        }
                        if (s.endsWith(" 00:00:00")) {
                            s = s.substring(0, s.length() - " 00:00:00".length());
                        }
                    }
                    return s;
                }
            }));
        }

        public AssertQuery returnsValue(String expected) {
            return returns(checkResultValue(expected));
        }

        public AssertQuery returnsCount(int expectedCount) {
            return returns(checkResultCount(is(expectedCount)));
        }

        public final AssertQuery returns(Function<ResultSet, Void> checker) {
            return returns(sql, checker);
        }

        public final AssertQuery updates(int count) {
            try {
                assertQuery(createConnection(), sql, limit, materializationsEnabled, hooks, null,
                        checkUpdateCount(count), null);
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while executing [" + sql + "]", e);
            }
        }

        protected AssertQuery returns(String sql, Function<ResultSet, Void> checker) {
            try {
                assertQuery(createConnection(), sql, limit, materializationsEnabled, hooks, checker, null, null);
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while executing [" + sql + "]", e);
            }
        }

        public AssertQuery returnsUnordered(String... lines) {
            return returns(checkResult(true, false, lines));
        }

        public AssertQuery returnsOrdered(String... lines) {
            return returns(checkResult(false, false, lines));
        }

        public AssertQuery returnsStartingWith(String... lines) {
            return returns(checkResult(false, true, lines));
        }

        public AssertQuery throws_(String message) {
            try {
                assertQuery(createConnection(), sql, limit, materializationsEnabled, hooks, null, null,
                        checkException(message));
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while executing [" + sql + "]", e);
            }
        }

        public AssertQuery runs() {
            try {
                assertQuery(createConnection(), sql, limit, materializationsEnabled, hooks, null, null, null);
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while executing [" + sql + "]", e);
            }
        }

        public AssertQuery typeIs(String expected) {
            try {
                assertQuery(createConnection(), sql, limit, false, hooks, checkResultType(expected), null, null);
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while executing [" + sql + "]", e);
            }
        }

        /** Checks that when the query (which was set using
         * {@link AssertThat#query(String)}) is converted to a relational algebra
         * expression matching the given string. */
        public final AssertQuery convertContains(final String expected) {
            return convertMatches(checkRel(expected, null));
        }

        public AssertQuery convertMatches(final Function<RelNode, Void> checker) {
            try {
                assertPrepare(createConnection(), sql, this.materializationsEnabled, checker, null);
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while preparing [" + sql + "]", e);
            }
        }

        public AssertQuery substitutionMatches(final Function<RelNode, Void> checker) {
            try {
                assertPrepare(createConnection(), sql, materializationsEnabled, null, checker);
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while preparing [" + sql + "]", e);
            }
        }

        public AssertQuery explainContains(String expected) {
            return explainMatches("", checkResultContains(expected));
        }

        public final AssertQuery explainMatches(String extra, Function<ResultSet, Void> checker) {
            return returns("explain plan " + extra + "for " + sql, checker);
        }

        public AssertQuery planContains(String expected) {
            ensurePlan(null);
            assertTrue("Plan [" + plan + "] contains [" + expected + "]",
                    Util.toLinux(plan).replaceAll("\\\\r\\\\n", "\\\\n").contains(expected));
            return this;
        }

        public AssertQuery planUpdateHasSql(String expected, int count) {
            ensurePlan(checkUpdateCount(count));
            expected = "getDataSource(), \""
                    + expected.replace("\\", "\\\\").replace("\"", "\\\"").replaceAll("\n", "\\\\n") + "\"";
            assertTrue("Plan [" + plan + "] contains [" + expected + "]",
                    Util.toLinux(plan).replaceAll("\\\\r\\\\n", "\\\\n").contains(expected));
            return this;
        }

        public AssertQuery planHasSql(String expected) {
            return planContains("getDataSource(), \""
                    + expected.replace("\\", "\\\\").replace("\"", "\\\"").replaceAll("\n", "\\\\n") + "\"");
        }

        private void ensurePlan(Function<Integer, Void> checkUpdate) {
            if (plan != null) {
                return;
            }
            addHook(Hook.JAVA_PLAN, new Function<String, Void>() {
                public Void apply(String a0) {
                    plan = a0;
                    return null;
                }
            });
            try {
                assertQuery(createConnection(), sql, limit, materializationsEnabled, hooks, null, checkUpdate,
                        null);
                assertNotNull(plan);
            } catch (Exception e) {
                throw new RuntimeException("exception while executing [" + sql + "]", e);
            }
        }

        /** Runs the query and applies a checker to the generated third-party
         * queries. The checker should throw to fail the test if it does not see
         * what it wants. This method can be used to check whether a particular
         * MongoDB or SQL query is generated, for instance. */
        public AssertQuery queryContains(Function<List, Void> predicate1) {
            final List<Object> list = Lists.newArrayList();
            addHook(Hook.QUERY_PLAN, new Function<Object, Void>() {
                public Void apply(Object a0) {
                    list.add(a0);
                    return null;
                }
            });
            try {
                assertQuery(createConnection(), sql, limit, materializationsEnabled, hooks, null, null, null);
                predicate1.apply(list);
                return this;
            } catch (Exception e) {
                throw new RuntimeException("exception while executing [" + sql + "]", e);
            }
        }

        /** Sets a limit on the number of rows returned. -1 means no limit. */
        public AssertQuery limit(int limit) {
            this.limit = limit;
            return this;
        }

        public void sameResultWithMaterializationsDisabled() {
            boolean save = materializationsEnabled;
            try {
                materializationsEnabled = false;
                final boolean ordered = sql.toUpperCase(Locale.ROOT).contains("ORDER BY");
                final Function<ResultSet, Void> checker = consistentResult(ordered);
                returns(checker);
                materializationsEnabled = true;
                returns(checker);
            } finally {
                materializationsEnabled = save;
            }
        }

        public AssertQuery enableMaterializations(boolean enable) {
            this.materializationsEnabled = enable;
            return this;
        }

        /** Adds a hook and a handler for that hook. Calcite will create a thread
         * hook (by calling {@link Hook#addThread(com.google.common.base.Function)})
         * just before running the query, and remove the hook afterwards. */
        public <T> AssertQuery withHook(Hook hook, Function<T, Void> handler) {
            addHook(hook, handler);
            return this;
        }

        private <T> void addHook(Hook hook, Function<T, Void> handler) {
            hooks.add(Pair.of(hook, (Function) handler));
        }

        /** Adds a property hook. */
        public <V> AssertQuery withProperty(Hook hook, V value) {
            return withHook(hook, Hook.property(value));
        }
    }

    /** Fluent interface for building a metadata query to be tested. */
    public static class AssertMetaData {
        private final ConnectionFactory connectionFactory;
        private final Function<Connection, ResultSet> function;

        AssertMetaData(ConnectionFactory connectionFactory, Function<Connection, ResultSet> function) {
            this.connectionFactory = connectionFactory;
            this.function = function;
        }

        public final AssertMetaData returns(Function<ResultSet, Void> checker) {
            try {
                Connection c = connectionFactory.createConnection();
                final ResultSet resultSet = function.apply(c);
                checker.apply(resultSet);
                resultSet.close();
                c.close();
                return this;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        public AssertMetaData returns(String expected) {
            return returns(checkResult(expected));
        }
    }

    /** Connection configuration. Basically, a set of schemas that should be
     * instantiated in the connection. */
    public enum Config {
        /** Configuration that creates an empty connection. */
        EMPTY,

        /**
         * Configuration that creates a connection with two in-memory data sets:
         * {@link org.apache.calcite.test.JdbcTest.HrSchema} and
         * {@link org.apache.calcite.test.JdbcTest.FoodmartSchema}.
         */
        REGULAR,

        /**
         * Configuration that creates a connection with an in-memory data set
         * similar to the smoke test in Cascading Lingual.
         */
        LINGUAL,

        /**
         * Configuration that creates a connection to a MySQL server. Tables
         * such as "customer" and "sales_fact_1997" are available. Queries
         * are processed by generating Java that calls linq4j operators
         * such as
         * {@link org.apache.calcite.linq4j.Enumerable#where(org.apache.calcite.linq4j.function.Predicate1)}.
         */
        JDBC_FOODMART,

        /**
         * Configuration that creates a connection to hsqldb containing the
         * Scott schema via the JDBC adapter.
         */
        JDBC_SCOTT,

        /** Configuration that contains an in-memory clone of the FoodMart
         * database. */
        FOODMART_CLONE,

        /** Configuration that contains an in-memory clone of the FoodMart
         * database, plus a lattice to enable on-the-fly materializations. */
        JDBC_FOODMART_WITH_LATTICE,

        /** Configuration that includes the metadata schema. */
        REGULAR_PLUS_METADATA,

        /** Configuration that loads the "scott/tiger" database. */
        SCOTT,

        /** Configuration that loads Spark. */
        SPARK,
    }

    /** Implementation of {@link AssertQuery} that does nothing. */
    private static class NopAssertQuery extends AssertQuery {
        private NopAssertQuery(String sql) {
            super(null, sql);
        }

        /** Returns an implementation of {@link AssertQuery} that does nothing. */
        static AssertQuery of(final String sql) {
            return new NopAssertQuery(sql);
        }

        @Override
        protected Connection createConnection() throws Exception {
            throw new AssertionError("disabled");
        }

        @Override
        public AssertQuery returns(String sql, Function<ResultSet, Void> checker) {
            return this;
        }

        @Override
        public AssertQuery throws_(String message) {
            return this;
        }

        @Override
        public AssertQuery runs() {
            return this;
        }

        @Override
        public AssertQuery convertMatches(Function<RelNode, Void> checker) {
            return this;
        }

        @Override
        public AssertQuery substitutionMatches(Function<RelNode, Void> checker) {
            return this;
        }

        @Override
        public AssertQuery planContains(String expected) {
            return this;
        }

        @Override
        public AssertQuery planHasSql(String expected) {
            return this;
        }

        @Override
        public AssertQuery planUpdateHasSql(String expected, int count) {
            return this;
        }

        @Override
        public AssertQuery queryContains(Function<List, Void> predicate1) {
            return this;
        }
    }

    /** Information necessary to create a JDBC connection. Specify one to run
     * tests against a different database. (hsqldb is the default.) */
    public enum DatabaseInstance {
        HSQLDB(new ConnectionSpec(FoodmartHsqldb.URI, "FOODMART", "FOODMART", "org.hsqldb.jdbcDriver", "foodmart"),
                new ConnectionSpec(ScottHsqldb.URI, ScottHsqldb.USER, ScottHsqldb.PASSWORD, "org.hsqldb.jdbcDriver",
                        "SCOTT")), H2(
                                new ConnectionSpec(
                                        "jdbc:h2:" + getDataSetPath()
                                                + "/h2/target/foodmart;user=foodmart;password=foodmart",
                                        "foodmart", "foodmart", "org.h2.Driver", "foodmart"),
                                null), MYSQL(
                                        new ConnectionSpec("jdbc:mysql://localhost/foodmart", "foodmart",
                                                "foodmart", "com.mysql.jdbc.Driver", "foodmart"),
                                        null), ORACLE(new ConnectionSpec("jdbc:oracle:thin:@localhost:1521:XE",
                                                "foodmart", "foodmart", "oracle.jdbc.OracleDriver", "FOODMART"),
                                                null), POSTGRESQL(new ConnectionSpec(
                                                        "jdbc:postgresql://localhost/foodmart?user=foodmart&password=foodmart&searchpath=foodmart",
                                                        "foodmart", "foodmart", "org.postgresql.Driver",
                                                        "foodmart"), null);

        public final ConnectionSpec foodmart;
        public final ConnectionSpec scott;

        private static String getDataSetPath() {
            String path = System.getProperty("calcite.test.dataset");
            if (path != null) {
                return path;
            }
            final String[] dirs = { "../calcite-test-dataset", "../../calcite-test-dataset" };
            for (String s : dirs) {
                if (new File(s).exists() && new File(s, "vm").exists()) {
                    return s;
                }
            }
            return ".";
        }

        DatabaseInstance(ConnectionSpec foodmart, ConnectionSpec scott) {
            this.foodmart = foodmart;
            this.scott = scott;
        }
    }

    /** Specification for common test schemas. */
    public enum SchemaSpec {
        REFLECTIVE_FOODMART, JDBC_FOODMART, CLONE_FOODMART, JDBC_FOODMART_WITH_LATTICE, HR, JDBC_SCOTT, SCOTT, BLANK, LINGUAL, POST, ORINOCO
    }

    /** Converts a {@link ResultSet} to string. */
    static class ResultSetFormatter {
        final StringBuilder buf = new StringBuilder();

        public ResultSetFormatter resultSet(ResultSet resultSet) throws SQLException {
            final ResultSetMetaData metaData = resultSet.getMetaData();
            while (resultSet.next()) {
                rowToString(resultSet, metaData);
                buf.append("\n");
            }
            return this;
        }

        /** Converts one row to a string. */
        ResultSetFormatter rowToString(ResultSet resultSet, ResultSetMetaData metaData) throws SQLException {
            int n = metaData.getColumnCount();
            if (n > 0) {
                for (int i = 1;; i++) {
                    buf.append(metaData.getColumnLabel(i)).append("=").append(adjustValue(resultSet.getString(i)));
                    if (i == n) {
                        break;
                    }
                    buf.append("; ");
                }
            }
            return this;
        }

        protected String adjustValue(String string) {
            return string;
        }

        public Collection<String> toStringList(ResultSet resultSet, Collection<String> list) throws SQLException {
            final ResultSetMetaData metaData = resultSet.getMetaData();
            while (resultSet.next()) {
                rowToString(resultSet, metaData);
                list.add(buf.toString());
                buf.setLength(0);
            }
            return list;
        }

        /** Flushes the buffer and returns its previous contents. */
        public String string() {
            String s = buf.toString();
            buf.setLength(0);
            return s;
        }
    }
}

// End CalciteAssert.java