[CALCITE-4821] Move utility test classes into calcite-testkit and unpublish -test.jar
diff --git a/babel/build.gradle.kts b/babel/build.gradle.kts
index 177d40d..8b689d1 100644
--- a/babel/build.gradle.kts
+++ b/babel/build.gradle.kts
@@ -32,7 +32,7 @@
     testImplementation("org.hsqldb:hsqldb")
     testImplementation("org.incava:java-diff")
     testImplementation("org.slf4j:slf4j-log4j12")
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
 }
 
 val fmppMain by tasks.registering(org.apache.calcite.buildtools.fmpp.FmppTask::class) {
diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index 4166140..f943c97 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -95,6 +95,7 @@
         apiv("org.apache.cassandra:cassandra-all")
         apiv("org.apache.commons:commons-dbcp2")
         apiv("org.apache.commons:commons-lang3")
+        apiv("org.apache.commons:commons-pool2")
         apiv("org.apache.geode:geode-core")
         apiv("org.apache.hadoop:hadoop-client", "hadoop")
         apiv("org.apache.hadoop:hadoop-common", "hadoop")
diff --git a/build.gradle.kts b/build.gradle.kts
index e9fdec1..37fa0d3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -208,6 +208,7 @@
 
 dependencies {
     sqllineClasspath(platform(project(":bom")))
+    sqllineClasspath(project(":testkit"))
     sqllineClasspath("sqlline:sqlline")
     for (p in adaptersForSqlline) {
         sqllineClasspath(project(p))
@@ -778,11 +779,6 @@
             archiveClassifier.set("tests")
         }
 
-        val testSourcesJar by tasks.registering(Jar::class) {
-            from(sourceSets["test"].allJava)
-            archiveClassifier.set("test-sources")
-        }
-
         val sourcesJar by tasks.registering(Jar::class) {
             from(sourceSets["main"].allJava)
             archiveClassifier.set("sources")
@@ -793,18 +789,11 @@
             archiveClassifier.set("javadoc")
         }
 
-        val testClasses by configurations.creating {
-            extendsFrom(configurations["testRuntime"])
-        }
-
         val archives by configurations.getting
 
         // Parenthesis needed to use Project#getArtifacts
         (artifacts) {
-            testClasses(testJar)
             archives(sourcesJar)
-            archives(testJar)
-            archives(testSourcesJar)
         }
 
         val archivesBaseName = "calcite-$name"
diff --git a/cassandra/build.gradle.kts b/cassandra/build.gradle.kts
index e7f9a63..df1dcfb 100644
--- a/cassandra/build.gradle.kts
+++ b/cassandra/build.gradle.kts
@@ -24,7 +24,7 @@
 
     implementation("org.apache.calcite.avatica:avatica-core")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("org.apache.cassandra:cassandra-all") {
         exclude("org.slf4j", "log4j-over-slf4j")
             .because("log4j is already present in the classpath")
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 6c6340a..8f9a3e0 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -71,15 +71,13 @@
     testOracle("com.oracle.ojdbc:ojdbc8")
     testPostgresql("org.postgresql:postgresql")
 
+    testImplementation(project(":testkit"))
     testImplementation("commons-lang:commons-lang")
-    testImplementation("net.hydromatic:foodmart-data-hsqldb")
     testImplementation("net.hydromatic:foodmart-queries")
     testImplementation("net.hydromatic:quidem")
-    testImplementation("net.hydromatic:scott-data-hsqldb")
     testImplementation("org.apache.calcite.avatica:avatica-server")
     testImplementation("org.apache.commons:commons-pool2")
     testImplementation("org.hsqldb:hsqldb")
-    testImplementation("org.incava:java-diff")
     testImplementation("sqlline:sqlline")
     testImplementation(kotlin("stdlib-jdk8"))
     testImplementation(kotlin("test"))
@@ -87,17 +85,6 @@
     testRuntimeOnly("org.slf4j:slf4j-log4j12")
 }
 
-// There are users that reuse/extend test code (e.g. Apache Felix)
-// So publish test jar to Nexus repository
-// TODO: remove when calcite-test-framework is extracted to a standalone artifact
-publishing {
-    publications {
-        named<MavenPublication>(project.name) {
-            artifact(tasks.testJar.get())
-        }
-    }
-}
-
 tasks.jar {
     CrLfSpec(LineEndings.LF).run {
         into("codegen") {
diff --git a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
index a618b56..97a360b 100644
--- a/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
+++ b/core/src/test/java/org/apache/calcite/jdbc/CalciteRemoteDriverTest.java
@@ -28,7 +28,7 @@
 import org.apache.calcite.config.CalciteSystemProperty;
 import org.apache.calcite.test.CalciteAssert;
 import org.apache.calcite.test.JdbcFrontLinqBackTest;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.Employee;
 import org.apache.calcite.util.TestUtil;
 import org.apache.calcite.util.Util;
 
@@ -533,9 +533,9 @@
   }
 
   public static Connection makeConnection() throws Exception {
-    List<JdbcTest.Employee> employees = new ArrayList<JdbcTest.Employee>();
+    List<Employee> employees = new ArrayList<Employee>();
     for (int i = 1; i <= 101; i++) {
-      employees.add(new JdbcTest.Employee(i, 0, "first", 0f, null));
+      employees.add(new Employee(i, 0, "first", 0f, null));
     }
     Connection conn = JdbcFrontLinqBackTest.makeConnection(employees);
     return conn;
diff --git a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
index 13330d6..bdc9ca3 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
@@ -54,9 +54,9 @@
 import org.apache.calcite.sql.fun.SqlTrimFunction;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.type.SqlTypeName;
-import org.apache.calcite.test.JdbcTest;
 import org.apache.calcite.test.MockSqlOperatorTable;
 import org.apache.calcite.test.RelBuilderTest;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 import org.apache.calcite.tools.FrameworkConfig;
 import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.tools.RelBuilder;
@@ -432,7 +432,7 @@
     String s =
         Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
           rootSchema.add("hr",
-              new ReflectiveSchema(new JdbcTest.HrSchema()));
+              new ReflectiveSchema(new HrSchema()));
           LogicalTableScan scan =
               LogicalTableScan.create(cluster,
                   relOptSchema.getTableForMember(
@@ -477,7 +477,7 @@
     String s =
         Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
           rootSchema.add("hr",
-              new ReflectiveSchema(new JdbcTest.HrSchema()));
+              new ReflectiveSchema(new HrSchema()));
           LogicalTableScan scan =
               LogicalTableScan.create(cluster,
                   relOptSchema.getTableForMember(
@@ -528,7 +528,7 @@
         Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
           SchemaPlus schema =
               rootSchema.add("hr",
-                  new ReflectiveSchema(new JdbcTest.HrSchema()));
+                  new ReflectiveSchema(new HrSchema()));
           final RelJsonReader reader =
               new RelJsonReader(cluster, relOptSchema, schema);
           RelNode node;
@@ -555,7 +555,7 @@
         Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
           SchemaPlus schema =
               rootSchema.add("hr",
-                  new ReflectiveSchema(new JdbcTest.HrSchema()));
+                  new ReflectiveSchema(new HrSchema()));
           final RelJsonReader reader =
               new RelJsonReader(cluster, relOptSchema, schema);
           RelNode node;
@@ -585,7 +585,7 @@
         Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
           SchemaPlus schema =
               rootSchema.add("hr",
-                  new ReflectiveSchema(new JdbcTest.HrSchema()));
+                  new ReflectiveSchema(new HrSchema()));
           final RelJsonReader reader =
               new RelJsonReader(cluster, relOptSchema, schema);
           RelNode node;
diff --git a/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java b/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java
index fba6126..a2dee17 100644
--- a/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/rel/rules/DateRangeRulesTest.java
@@ -19,7 +19,7 @@
 import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.calcite.test.RexImplicationCheckerTest.Fixture;
+import org.apache.calcite.test.RexImplicationCheckerFixtures.Fixture;
 import org.apache.calcite.util.DateString;
 import org.apache.calcite.util.TimestampString;
 import org.apache.calcite.util.Util;
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/CoreSqlParserTest.java b/core/src/test/java/org/apache/calcite/sql/parser/CoreSqlParserTest.java
new file mode 100644
index 0000000..716bfc6
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/sql/parser/CoreSqlParserTest.java
@@ -0,0 +1,23 @@
+/*
+ * 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.sql.parser;
+
+/**
+ * Tests SQL Parser.
+ */
+public class CoreSqlParserTest extends SqlParserTest {
+}
diff --git a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
index 1719b8b..f4410b7 100644
--- a/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
+++ b/core/src/test/java/org/apache/calcite/test/CoreQuidemTest.java
@@ -16,9 +16,13 @@
  */
 package org.apache.calcite.test;
 
+import org.apache.calcite.config.CalciteConnectionProperty;
 import org.apache.calcite.prepare.Prepare;
 import org.apache.calcite.util.TryThreadLocal;
 
+import net.hydromatic.quidem.Quidem;
+
+import java.sql.Connection;
 import java.util.Collection;
 
 /**
@@ -46,6 +50,23 @@
     return data(first);
   }
 
+  @Override protected Quidem.ConnectionFactory createConnectionFactory() {
+    return new QuidemConnectionFactory() {
+      @Override public Connection connect(String name, boolean reference) throws Exception {
+        switch (name) {
+        case "blank":
+          return CalciteAssert.that()
+              .with(CalciteConnectionProperty.PARSER_FACTORY,
+                  ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY")
+              .with(CalciteAssert.SchemaSpec.BLANK)
+              .connect();
+        default:
+        }
+        return super.connect(name, reference);
+      }
+    };
+  }
+
   /** Override settings for "sql/misc.iq". */
   public void testSqlMisc(String path) throws Exception {
     switch (CalciteAssert.DB) {
diff --git a/core/src/test/java/org/apache/calcite/test/ExtensionDdlExecutor.java b/core/src/test/java/org/apache/calcite/test/ExtensionDdlExecutor.java
index 22a7707..9ff12c0 100644
--- a/core/src/test/java/org/apache/calcite/test/ExtensionDdlExecutor.java
+++ b/core/src/test/java/org/apache/calcite/test/ExtensionDdlExecutor.java
@@ -177,7 +177,7 @@
 
   /** Table backed by a Java list. */
   private static class MutableArrayTable
-      extends JdbcTest.AbstractModifiableTable {
+      extends AbstractModifiableTable {
     final List list = new ArrayList();
     private final RelProtoDataType protoRowType;
 
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
index 4607667..5299b60 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcAdapterTest.java
@@ -20,6 +20,7 @@
 import org.apache.calcite.config.Lex;
 import org.apache.calcite.test.CalciteAssert.AssertThat;
 import org.apache.calcite.test.CalciteAssert.DatabaseInstance;
+import org.apache.calcite.test.schemata.foodmart.FoodmartSchema;
 import org.apache.calcite.util.Smalls;
 import org.apache.calcite.util.TestUtil;
 
@@ -59,7 +60,7 @@
         + "  EnumerableValues(tuples=[[{ 1 }, { 2 }]])";
     final String jdbcSql = "SELECT *\n"
         + "FROM \"foodmart\".\"days\"";
-    CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query(sql)
         .explainContains(explain)
         .runs()
@@ -69,7 +70,7 @@
   }
 
   @Test void testUnionPlan() {
-    CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select * from \"sales_fact_1997\"\n"
             + "union all\n"
             + "select * from \"sales_fact_1998\"")
@@ -115,7 +116,7 @@
   }
 
   @Test void testFilterUnionPlan() {
-    CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select * from (\n"
             + "  select * from \"sales_fact_1997\"\n"
             + "  union all\n"
@@ -133,7 +134,7 @@
   }
 
   @Test void testInPlan() {
-    CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select \"store_id\", \"store_name\" from \"store\"\n"
             + "where \"store_name\" in ('Store 1', 'Store 10', 'Store 11', 'Store 15', 'Store 16', 'Store 24', 'Store 3', 'Store 7')")
         .runs()
@@ -412,7 +413,7 @@
         + "WHERE T2.\"product_department\" = 'Frozen Foods'\n"
         + " OR T2.\"product_department\" = 'Baking Goods'\n"
         + " AND T1.\"brand_name\" <> 'King'";
-    CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query(sql).runs()
         .returnsCount(275);
   }
@@ -533,7 +534,7 @@
         + "  JdbcToEnumerableConverter\n"
         + "    JdbcTableScan(table=[[foodmart, expense_fact]])\n";
     CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == DatabaseInstance.HSQLDB)
         .query(sql)
         .explainContains(explain)
@@ -544,7 +545,7 @@
 
   @Test void testTablesNoCatalogSchema() {
     final String model =
-        JdbcTest.FOODMART_MODEL
+        FoodmartSchema.FOODMART_MODEL
             .replace("jdbcCatalog: 'foodmart'", "jdbcCatalog: null")
             .replace("jdbcSchema: 'foodmart'", "jdbcSchema: null");
     // Since Calcite uses PostgreSQL JDBC driver version >= 4.1,
@@ -578,7 +579,7 @@
    * support OVER. */
   @Test void testOverDefault() {
     CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.POSTGRESQL)
         .query("select \"store_id\", \"account_id\", \"exp_date\","
             + " \"time_id\", \"category_id\", \"currency_id\", \"amount\","
@@ -604,7 +605,7 @@
    * not have TINYINT and DOUBLE types</a>. */
   @Test void testCast() {
     CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.POSTGRESQL)
         .query("select cast(\"store_id\" as TINYINT),"
             + "cast(\"store_id\" as DOUBLE)"
@@ -617,7 +618,7 @@
 
   @Test void testOverRowsBetweenBoundFollowingAndFollowing() {
     CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.POSTGRESQL)
         .query("select \"store_id\", \"account_id\", \"exp_date\","
             + " \"time_id\", \"category_id\", \"currency_id\", \"amount\","
@@ -641,7 +642,7 @@
 
   @Test void testOverRowsBetweenBoundPrecedingAndCurrent() {
     CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.POSTGRESQL)
         .query("select \"store_id\", \"account_id\", \"exp_date\","
             + " \"time_id\", \"category_id\", \"currency_id\", \"amount\","
@@ -665,7 +666,7 @@
 
   @Test void testOverDisallowPartial() {
     CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.POSTGRESQL)
         .query("select \"store_id\", \"account_id\", \"exp_date\","
             + " \"time_id\", \"category_id\", \"currency_id\", \"amount\","
@@ -695,7 +696,7 @@
 
   @Test void testLastValueOver() {
     CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == CalciteAssert.DatabaseInstance.POSTGRESQL)
         .query("select \"store_id\", \"account_id\", \"exp_date\","
             + " \"time_id\", \"category_id\", \"currency_id\", \"amount\","
@@ -731,7 +732,7 @@
     default:
       expected = "more than one value in agg SINGLE_VALUE";
     }
-    CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("SELECT \"full_name\" FROM \"employee\" WHERE "
                 + "\"employee_id\" = (SELECT \"employee_id\" FROM \"salary\")")
         .explainContains("SINGLE_VALUE")
@@ -746,7 +747,7 @@
   @Test void testMetadataTables() throws Exception {
     // The troublesome tables occur in PostgreSQL's system schema.
     final String model =
-        JdbcTest.FOODMART_MODEL.replace("jdbcSchema: 'foodmart'",
+        FoodmartSchema.FOODMART_MODEL.replace("jdbcSchema: 'foodmart'",
             "jdbcSchema: null");
     CalciteAssert.model(
         model)
@@ -885,7 +886,7 @@
         + "VALUES (666, 666, TIMESTAMP '1997-01-01 00:00:00', 666, '666', "
         + "666, 666)";
     final AssertThat that =
-        CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+        CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
             .enable(CalciteAssert.DB == DatabaseInstance.HSQLDB
                 || CalciteAssert.DB == DatabaseInstance.POSTGRESQL);
     that.doWithConnection(connection -> {
@@ -920,7 +921,7 @@
         + "(666, 666, TIMESTAMP '1997-01-01 00:00:00', 666, '666', 666, 666),\n"
         + "(666, 777, TIMESTAMP '1997-01-01 00:00:00', 666, '666', 666, 666)";
     final AssertThat that =
-        CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+        CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
             .enable(CalciteAssert.DB == DatabaseInstance.HSQLDB
                 || CalciteAssert.DB == DatabaseInstance.POSTGRESQL);
     that.doWithConnection(connection -> {
@@ -936,7 +937,7 @@
 
   @Test void testTableModifyInsertWithSubQuery() throws Exception {
     final AssertThat that = CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == DatabaseInstance.HSQLDB);
 
     that.doWithConnection(connection -> {
@@ -973,7 +974,7 @@
 
   @Test void testTableModifyUpdate() throws Exception {
     final AssertThat that = CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == DatabaseInstance.HSQLDB);
 
     that.doWithConnection(connection -> {
@@ -1001,7 +1002,7 @@
 
   @Test void testTableModifyDelete() throws Exception {
     final AssertThat that = CalciteAssert
-        .model(JdbcTest.FOODMART_MODEL)
+        .model(FoodmartSchema.FOODMART_MODEL)
         .enable(CalciteAssert.DB == DatabaseInstance.HSQLDB);
 
     that.doWithConnection(connection -> {
@@ -1029,7 +1030,7 @@
   @Test void testColumnNullability() {
     final String sql = "select \"employee_id\", \"position_id\"\n"
         + "from \"foodmart\".\"employee\" limit 10";
-    CalciteAssert.model(JdbcTest.FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query(sql)
         .runs()
         .returnsCount(10)
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcFrontLinqBackTest.java b/core/src/test/java/org/apache/calcite/test/JdbcFrontLinqBackTest.java
index 90d1352..5554c71 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcFrontLinqBackTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcFrontLinqBackTest.java
@@ -29,6 +29,7 @@
 import org.apache.calcite.schema.Schemas;
 import org.apache.calcite.schema.impl.AbstractSchema;
 import org.apache.calcite.schema.impl.AbstractTableQueryable;
+import org.apache.calcite.test.schemata.hr.Employee;
 import org.apache.calcite.util.TestUtil;
 
 import org.junit.jupiter.api.Disabled;
@@ -218,7 +219,7 @@
   }
 
   @Test void testInsert() {
-    final List<JdbcTest.Employee> employees = new ArrayList<>();
+    final List<Employee> employees = new ArrayList<>();
     CalciteAssert.AssertThat with = mutable(employees);
     with.query("select * from \"foo\".\"bar\"")
         .returns(
@@ -241,7 +242,7 @@
   }
 
   @Test void testInsertBind() throws Exception {
-    final List<JdbcTest.Employee> employees = new ArrayList<>();
+    final List<Employee> employees = new ArrayList<>();
     CalciteAssert.AssertThat with = mutable(employees);
     with.query("select count(*) as c from \"foo\".\"bar\"")
         .returns("C=1\n");
@@ -267,7 +268,7 @@
   }
 
   @Test void testDelete() {
-    final List<JdbcTest.Employee> employees = new ArrayList<>();
+    final List<Employee> employees = new ArrayList<>();
     CalciteAssert.AssertThat with = mutable(employees);
     with.query("select * from \"foo\".\"bar\"")
         .returnsUnordered(
@@ -299,14 +300,14 @@
    * @return a connection post-processor
    */
   private static CalciteAssert.ConnectionPostProcessor makePostProcessor(
-      final List<JdbcTest.Employee> initialData) {
+      final List<Employee> initialData) {
     return connection -> {
       CalciteConnection calciteConnection =
           connection.unwrap(CalciteConnection.class);
       SchemaPlus rootSchema = calciteConnection.getRootSchema();
       SchemaPlus mapSchema = rootSchema.add("foo", new AbstractSchema());
       final String tableName = "bar";
-      final JdbcTest.AbstractModifiableTable table =
+      final AbstractModifiableTable table =
           mutable(tableName, initialData);
       mapSchema.add(tableName, table);
       return calciteConnection;
@@ -319,7 +320,7 @@
    * @param initialData record to be presented in table
    */
   public static Connection makeConnection(
-        final List<JdbcTest.Employee> initialData) throws Exception {
+        final List<Employee> initialData) throws Exception {
     Properties info = new Properties();
     Connection connection = DriverManager.getConnection("jdbc:calcite:", info);
     connection = makePostProcessor(initialData).apply(connection);
@@ -328,27 +329,27 @@
 
   /**
    * Creates a connection with an empty modifiable table with
-   * {@link JdbcTest.Employee} schema.
+   * {@link Employee} schema.
    */
   public static Connection makeConnection() throws Exception {
-    return makeConnection(new ArrayList<JdbcTest.Employee>());
+    return makeConnection(new ArrayList<Employee>());
   }
 
   private CalciteAssert.AssertThat mutable(
-      final List<JdbcTest.Employee> employees) {
-    employees.add(new JdbcTest.Employee(0, 0, "first", 0f, null));
+      final List<Employee> employees) {
+    employees.add(new Employee(0, 0, "first", 0f, null));
     return that()
         .with(CalciteAssert.Config.REGULAR)
         .with(makePostProcessor(employees));
   }
 
-  static JdbcTest.AbstractModifiableTable mutable(String tableName,
-      final List<JdbcTest.Employee> employees) {
-    return new JdbcTest.AbstractModifiableTable(tableName) {
+  static AbstractModifiableTable mutable(String tableName,
+                                         final List<Employee> employees) {
+    return new AbstractModifiableTable(tableName) {
       public RelDataType getRowType(
           RelDataTypeFactory typeFactory) {
         return ((JavaTypeFactory) typeFactory)
-            .createType(JdbcTest.Employee.class);
+            .createType(Employee.class);
       }
 
       public <T> Queryable<T> asQueryable(QueryProvider queryProvider,
@@ -363,7 +364,7 @@
       }
 
       public Type getElementType() {
-        return JdbcTest.Employee.class;
+        return Employee.class;
       }
 
       public Expression getExpression(SchemaPlus schema, String tableName,
@@ -379,7 +380,7 @@
   }
 
   @Test void testInsert2() {
-    final List<JdbcTest.Employee> employees = new ArrayList<>();
+    final List<Employee> employees = new ArrayList<>();
     CalciteAssert.AssertThat with = mutable(employees);
     with.query("insert into \"foo\".\"bar\" values (1, 1, 'second', 2, 2)")
         .updates(1);
@@ -396,7 +397,7 @@
 
   /** Local Statement insert. */
   @Test void testInsert3() throws Exception {
-    Connection connection = makeConnection(new ArrayList<JdbcTest.Employee>());
+    Connection connection = makeConnection(new ArrayList<Employee>());
     String sql = "insert into \"foo\".\"bar\" values (1, 1, 'second', 2, 2)";
 
     Statement statement = connection.createStatement();
@@ -410,7 +411,7 @@
 
   /** Local PreparedStatement insert WITHOUT bind variables. */
   @Test void testPreparedStatementInsert() throws Exception {
-    Connection connection = makeConnection(new ArrayList<JdbcTest.Employee>());
+    Connection connection = makeConnection(new ArrayList<Employee>());
     assertFalse(connection.isClosed());
 
     String sql = "insert into \"foo\".\"bar\" values (1, 1, 'second', 2, 2)";
@@ -431,7 +432,7 @@
 
   /** Some of the rows have the wrong number of columns. */
   @Test void testInsertMultipleRowMismatch() {
-    final List<JdbcTest.Employee> employees = new ArrayList<>();
+    final List<Employee> employees = new ArrayList<>();
     CalciteAssert.AssertThat with = mutable(employees);
     with.query("insert into \"foo\".\"bar\" values\n"
         + " (1, 3, 'third'),\n"
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 8bcd9d9..8dee886 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -48,33 +48,23 @@
 import org.apache.calcite.linq4j.QueryProvider;
 import org.apache.calcite.linq4j.Queryable;
 import org.apache.calcite.linq4j.function.Function0;
-import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptPlanner;
-import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.prepare.CalcitePrepareImpl;
 import org.apache.calcite.prepare.Prepare;
-import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.TableModify;
-import org.apache.calcite.rel.logical.LogicalTableModify;
 import org.apache.calcite.rel.rules.CoreRules;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelProtoDataType;
-import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.runtime.FlatLists;
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.runtime.SqlFunctions;
-import org.apache.calcite.schema.ModifiableTable;
-import org.apache.calcite.schema.ModifiableView;
-import org.apache.calcite.schema.QueryableTable;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.schema.SchemaFactory;
 import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.Table;
 import org.apache.calcite.schema.TableFactory;
 import org.apache.calcite.schema.TableMacro;
-import org.apache.calcite.schema.TranslatableTable;
 import org.apache.calcite.schema.impl.AbstractSchema;
 import org.apache.calcite.schema.impl.AbstractTable;
 import org.apache.calcite.schema.impl.AbstractTableQueryable;
@@ -90,6 +80,11 @@
 import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.sql.parser.SqlParserPos;
 import org.apache.calcite.sql.parser.impl.SqlParserImpl;
+import org.apache.calcite.test.schemata.catchall.CatchallSchema;
+import org.apache.calcite.test.schemata.foodmart.FoodmartSchema;
+import org.apache.calcite.test.schemata.hr.Department;
+import org.apache.calcite.test.schemata.hr.Employee;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 import org.apache.calcite.util.Bug;
 import org.apache.calcite.util.JsonBuilder;
 import org.apache.calcite.util.Pair;
@@ -140,7 +135,6 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
 import java.util.TimeZone;
@@ -175,25 +169,6 @@
  */
 public class JdbcTest {
 
-  public static final String FOODMART_SCHEMA = "     {\n"
-      + "       type: 'jdbc',\n"
-      + "       name: 'foodmart',\n"
-      + "       jdbcDriver: " + q(CalciteAssert.DB.foodmart.driver) + ",\n"
-      + "       jdbcUser: " + q(CalciteAssert.DB.foodmart.username) + ",\n"
-      + "       jdbcPassword: " + q(CalciteAssert.DB.foodmart.password) + ",\n"
-      + "       jdbcUrl: " + q(CalciteAssert.DB.foodmart.url) + ",\n"
-      + "       jdbcCatalog: " + q(CalciteAssert.DB.foodmart.catalog) + ",\n"
-      + "       jdbcSchema: " + q(CalciteAssert.DB.foodmart.schema) + "\n"
-      + "     }\n";
-
-  public static final String FOODMART_MODEL = "{\n"
-      + "  version: '1.0',\n"
-      + "  defaultSchema: 'foodmart',\n"
-      + "   schemas: [\n"
-      + FOODMART_SCHEMA
-      + "   ]\n"
-      + "}";
-
   public static final ConnectionSpec SCOTT =
       Util.first(CalciteAssert.DB.scott,
           CalciteAssert.DatabaseInstance.HSQLDB.scott);
@@ -239,7 +214,7 @@
   public static final String FOODMART_SCOTT_MODEL = "{\n"
       + "  version: '1.0',\n"
       + "   schemas: [\n"
-      + FOODMART_SCHEMA
+      + FoodmartSchema.FOODMART_SCHEMA
       + ",\n"
       + SCOTT_SCHEMA
       + "   ]\n"
@@ -1158,7 +1133,7 @@
     CalciteAssert.that()
         .with(CalciteConnectionProperty.LEX, Lex.JAVA)
         .with(CalciteConnectionProperty.FORCE_DECORRELATE, false)
-        .withSchema("s", new ReflectiveSchema(new JdbcTest.HrSchema()))
+        .withSchema("s", new ReflectiveSchema(new HrSchema()))
         .query(sql)
         .explainMatches("including all attributes ",
             CalciteAssert.checkResultContains("EnumerableCorrelate"));
@@ -2549,8 +2524,8 @@
         + "from \"hr\".\"emps\"";
     final String plan = ""
         + "      String case_when_value;\n"
-        + "              final org.apache.calcite.test.JdbcTest.Employee current = (org.apache"
-        + ".calcite.test.JdbcTest.Employee) inputEnumerator.current();\n"
+        + "              final org.apache.calcite.test.schemata.hr.Employee current = (org.apache"
+        + ".calcite.test.schemata.hr.Employee) inputEnumerator.current();\n"
         + "              if (current.empid > current.deptno * 10) {\n"
         + "                case_when_value = \"y\";\n"
         + "              } else {\n"
@@ -2573,8 +2548,8 @@
         + "from \"hr\".\"emps\"";
     final String plan = ""
         + "      String case_when_value;\n"
-        + "              final org.apache.calcite.test.JdbcTest.Employee current = (org.apache"
-        + ".calcite.test.JdbcTest.Employee) inputEnumerator.current();\n"
+        + "              final org.apache.calcite.test.schemata.hr.Employee current = (org.apache"
+        + ".calcite.test.schemata.hr.Employee) inputEnumerator.current();\n"
         + "              if (current.empid > current.deptno * 10) {\n"
         + "                case_when_value = current.name;\n"
         + "              } else {\n"
@@ -2596,8 +2571,8 @@
         + " \"deptno\"+case when CURRENT_PATH <> '' then 1 end)\n"
         + "from \"hr\".\"emps\"";
     final String plan = ""
-        + "              final org.apache.calcite.test.JdbcTest.Employee current"
-        + " = (org.apache.calcite.test.JdbcTest.Employee) inputEnumerator.current();\n"
+        + "              final org.apache.calcite.test.schemata.hr.Employee current"
+        + " = (org.apache.calcite.test.schemata.hr.Employee) inputEnumerator.current();\n"
         + "              final String input_value = current.name;\n"
         + "              Integer case_when_value;\n"
         + "              if ($L4J$C$org_apache_calcite_runtime_SqlFunctions_ne_) {\n"
@@ -2628,8 +2603,8 @@
         + "from\n"
         + "\"hr\".\"emps\"";
     final String plan = ""
-        + "              final org.apache.calcite.test.JdbcTest.Employee current ="
-        + " (org.apache.calcite.test.JdbcTest.Employee) inputEnumerator.current();\n"
+        + "              final org.apache.calcite.test.schemata.hr.Employee current ="
+        + " (org.apache.calcite.test.schemata.hr.Employee) inputEnumerator.current();\n"
         + "              final String input_value = current.name;\n"
         + "              final int input_value0 = current.deptno;\n"
         + "              Integer case_when_value;\n"
@@ -2692,8 +2667,8 @@
         + "from\n"
         + "\"hr\".\"emps\"";
     final String plan = ""
-        + "              final org.apache.calcite.test.JdbcTest.Employee current ="
-        + " (org.apache.calcite.test.JdbcTest.Employee) inputEnumerator.current();\n"
+        + "              final org.apache.calcite.test.schemata.hr.Employee current ="
+        + " (org.apache.calcite.test.schemata.hr.Employee) inputEnumerator.current();\n"
         + "              final String input_value = current.name;\n"
         + "              final int input_value0 = current.deptno;\n"
         + "              Integer case_when_value;\n"
@@ -4802,7 +4777,7 @@
     CalciteAssert.that()
         .withSchema("s",
             new ReflectiveSchema(
-                new ReflectiveSchemaTest.CatchallSchema()))
+                new CatchallSchema()))
         .query("select a.\"value\", b.\"value\"\n"
             + "  from \"bools\" a\n"
             + "     , \"bools\" b\n"
@@ -4823,7 +4798,7 @@
     CalciteAssert.that()
         .withSchema("s",
             new ReflectiveSchema(
-                new ReflectiveSchemaTest.CatchallSchema()))
+                new CatchallSchema()))
         .query(sql)
         .returnsUnordered("value=T; value=T",
             "value=T; value=F",
@@ -4859,7 +4834,7 @@
   }
 
   @Test void testVarcharEquals() {
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select \"lname\" from \"customer\" where \"lname\" = 'Nowmer'")
         .returns("lname=Nowmer\n");
 
@@ -4868,12 +4843,12 @@
     // type, thus lname would be cast to a varchar(40) in this case.
     // These sorts of casts are removed though when constructing the jdbc
     // sql, since e.g. HSQLDB does not support them.
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select count(*) as c from \"customer\" "
             + "where \"lname\" = 'this string is longer than 30 characters'")
         .returns("C=0\n");
 
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select count(*) as c from \"customer\" "
             + "where cast(\"customer_id\" as char(20)) = 'this string is longer than 30 characters'")
         .returns("C=0\n");
@@ -4886,7 +4861,7 @@
     final String sql = "select count(*) as c\n"
         + "from \"customer\" as c\n"
         + "join \"product\" as p on c.\"lname\" = p.\"brand_name\"";
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query(sql)
         .returns("C=607\n");
   }
@@ -4896,7 +4871,7 @@
         + "  select \"lname\" from \"customer\" as c\n"
         + "  intersect\n"
         + "  select \"brand_name\" from \"product\" as p)";
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query(sql)
         .returns("C=12\n");
   }
@@ -4962,12 +4937,12 @@
   }
 
   @Test void testTrim() {
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select trim(\"lname\") as \"lname\" "
             + "from \"customer\" where \"lname\" = 'Nowmer'")
         .returns("lname=Nowmer\n");
 
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select trim(leading 'N' from \"lname\") as \"lname\" "
             + "from \"customer\" where \"lname\" = 'Nowmer'")
         .returns("lname=owmer\n");
@@ -5423,7 +5398,7 @@
   /** Tests a JDBC connection that provides a model (a single schema based on
    * a JDBC database). */
   @Test void testModel() {
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .query("select count(*) as c from \"foodmart\".\"time_by_day\"")
         .returns("C=730\n");
   }
@@ -5436,8 +5411,8 @@
    * Allow comments in schema definitions</a>. */
   @Test void testModelWithComment() {
     final String model =
-        FOODMART_MODEL.replace("schemas:", "/* comment */ schemas:");
-    assertThat(model, not(equalTo(FOODMART_MODEL)));
+        FoodmartSchema.FOODMART_MODEL.replace("schemas:", "/* comment */ schemas:");
+    assertThat(model, not(equalTo(FoodmartSchema.FOODMART_MODEL)));
     CalciteAssert.model(model)
         .query("select count(*) as c from \"foodmart\".\"time_by_day\"")
         .returns("C=730\n");
@@ -5448,13 +5423,13 @@
    * are more comprehensive tests in {@link MaterializationTest}. */
   @Disabled("until JdbcSchema can define materialized views")
   @Test void testModelWithMaterializedView() {
-    CalciteAssert.model(FOODMART_MODEL)
+    CalciteAssert.model(FoodmartSchema.FOODMART_MODEL)
         .enable(false)
         .query(
             "select count(*) as c from \"foodmart\".\"sales_fact_1997\" join \"foodmart\".\"time_by_day\" using (\"time_id\")")
         .returns("C=86837\n");
     CalciteAssert.that().withMaterializations(
-        FOODMART_MODEL,
+        FoodmartSchema.FOODMART_MODEL,
         "agg_c_10_sales_fact_1997",
             "select t.`month_of_year`, t.`quarter`, t.`the_year`, sum(s.`store_sales`) as `store_sales`, sum(s.`store_cost`), sum(s.`unit_sales`), count(distinct s.`customer_id`), count(*) as `fact_count` from `time_by_day` as t join `sales_fact_1997` as s using (`time_id`) group by t.`month_of_year`, t.`quarter`, t.`the_year`")
         .query(
@@ -7784,302 +7759,6 @@
   // Disable checkstyle, so it doesn't complain about fields like "customer_id".
   //CHECKSTYLE: OFF
 
-  /** A schema that contains two tables by reflection.
-   *
-   * <p>Here is the SQL to create equivalent tables in Oracle:
-   *
-   * <blockquote>
-   * <pre>
-   * CREATE TABLE "emps" (
-   *   "empid" INTEGER NOT NULL,
-   *   "deptno" INTEGER NOT NULL,
-   *   "name" VARCHAR2(10) NOT NULL,
-   *   "salary" NUMBER(6, 2) NOT NULL,
-   *   "commission" INTEGER);
-   * INSERT INTO "emps" VALUES (100, 10, 'Bill', 10000, 1000);
-   * INSERT INTO "emps" VALUES (200, 20, 'Eric', 8000, 500);
-   * INSERT INTO "emps" VALUES (150, 10, 'Sebastian', 7000, null);
-   * INSERT INTO "emps" VALUES (110, 10, 'Theodore', 11500, 250);
-   *
-   * CREATE TABLE "depts" (
-   *   "deptno" INTEGER NOT NULL,
-   *   "name" VARCHAR2(10) NOT NULL,
-   *   "employees" ARRAY OF "Employee",
-   *   "location" "Location");
-   * INSERT INTO "depts" VALUES (10, 'Sales', null, (-122, 38));
-   * INSERT INTO "depts" VALUES (30, 'Marketing', null, (0, 52));
-   * INSERT INTO "depts" VALUES (40, 'HR', null, null);
-   * </pre>
-   * </blockquote>
-   */
-  public static class HrSchema {
-    @Override public String toString() {
-      return "HrSchema";
-    }
-
-    public final Employee[] emps = {
-      new Employee(100, 10, "Bill", 10000, 1000),
-      new Employee(200, 20, "Eric", 8000, 500),
-      new Employee(150, 10, "Sebastian", 7000, null),
-      new Employee(110, 10, "Theodore", 11500, 250),
-    };
-    public final Department[] depts = {
-      new Department(10, "Sales", Arrays.asList(emps[0], emps[2]),
-          new Location(-122, 38)),
-      new Department(30, "Marketing", ImmutableList.of(), new Location(0, 52)),
-      new Department(40, "HR", Collections.singletonList(emps[1]), null),
-    };
-    public final Dependent[] dependents = {
-      new Dependent(10, "Michael"),
-      new Dependent(10, "Jane"),
-    };
-    public final Dependent[] locations = {
-      new Dependent(10, "San Francisco"),
-      new Dependent(20, "San Diego"),
-    };
-
-    public QueryableTable foo(int count) {
-      return Smalls.generateStrings(count);
-    }
-
-    public TranslatableTable view(String s) {
-      return Smalls.view(s);
-    }
-  }
-
-  public static class HrSchemaBig {
-    @Override public String toString() {
-      return "HrSchema";
-    }
-
-    public final Employee[] emps = {
-        new Employee(1, 10, "Bill", 10000, 1000),
-        new Employee(2, 20, "Eric", 8000, 500),
-        new Employee(3, 10, "Sebastian", 7000, null),
-        new Employee(4, 10, "Theodore", 11500, 250),
-        new Employee(5, 10, "Marjorie", 10000, 1000),
-        new Employee(6, 20, "Guy", 8000, 500),
-        new Employee(7, 10, "Dieudonne", 7000, null),
-        new Employee(8, 10, "Haroun", 11500, 250),
-        new Employee(9, 10, "Sarah", 10000, 1000),
-        new Employee(10, 20, "Gabriel", 8000, 500),
-        new Employee(11, 10, "Pierre", 7000, null),
-        new Employee(12, 10, "Paul", 11500, 250),
-        new Employee(13, 10, "Jacques", 100, 1000),
-        new Employee(14, 20, "Khawla", 8000, 500),
-        new Employee(15, 10, "Brielle", 7000, null),
-        new Employee(16, 10, "Hyuna", 11500, 250),
-        new Employee(17, 10, "Ahmed", 10000, 1000),
-        new Employee(18, 20, "Lara", 8000, 500),
-        new Employee(19, 10, "Capucine", 7000, null),
-        new Employee(20, 10, "Michelle", 11500, 250),
-        new Employee(21, 10, "Cerise", 10000, 1000),
-        new Employee(22, 80, "Travis", 8000, 500),
-        new Employee(23, 10, "Taylor", 7000, null),
-        new Employee(24, 10, "Seohyun", 11500, 250),
-        new Employee(25, 70, "Helen", 10000, 1000),
-        new Employee(26, 50, "Patric", 8000, 500),
-        new Employee(27, 10, "Clara", 7000, null),
-        new Employee(28, 10, "Catherine", 11500, 250),
-        new Employee(29, 10, "Anibal", 10000, 1000),
-        new Employee(30, 30, "Ursula", 8000, 500),
-        new Employee(31, 10, "Arturito", 7000, null),
-        new Employee(32, 70, "Diane", 11500, 250),
-        new Employee(33, 10, "Phoebe", 10000, 1000),
-        new Employee(34, 20, "Maria", 8000, 500),
-        new Employee(35, 10, "Edouard", 7000, null),
-        new Employee(36, 110, "Isabelle", 11500, 250),
-        new Employee(37, 120, "Olivier", 10000, 1000),
-        new Employee(38, 20, "Yann", 8000, 500),
-        new Employee(39, 60, "Ralf", 7000, null),
-        new Employee(40, 60, "Emmanuel", 11500, 250),
-        new Employee(41, 10, "Berenice", 10000, 1000),
-        new Employee(42, 20, "Kylie", 8000, 500),
-        new Employee(43, 80, "Natacha", 7000, null),
-        new Employee(44, 100, "Henri", 11500, 250),
-        new Employee(45, 90, "Pascal", 10000, 1000),
-        new Employee(46, 90, "Sabrina", 8000, 500),
-        new Employee(47, 8, "Riyad", 7000, null),
-        new Employee(48, 5, "Andy", 11500, 250),
-    };
-    public final Department[] depts = {
-        new Department(10, "Sales", Arrays.asList(emps[0], emps[2]),
-            new Location(-122, 38)),
-        new Department(20, "Marketing", ImmutableList.of(), new Location(0, 52)),
-        new Department(30, "HR", Collections.singletonList(emps[1]), null),
-        new Department(40, "Administration", Arrays.asList(emps[0], emps[2]),
-            new Location(-122, 38)),
-        new Department(50, "Design", ImmutableList.of(), new Location(0, 52)),
-        new Department(60, "IT", Collections.singletonList(emps[1]), null),
-        new Department(70, "Production", Arrays.asList(emps[0], emps[2]),
-            new Location(-122, 38)),
-        new Department(80, "Finance", ImmutableList.of(), new Location(0, 52)),
-        new Department(90, "Accounting", Collections.singletonList(emps[1]), null),
-        new Department(100, "Research", Arrays.asList(emps[0], emps[2]),
-            new Location(-122, 38)),
-        new Department(110, "Maintenance", ImmutableList.of(), new Location(0, 52)),
-        new Department(120, "Client Support", Collections.singletonList(emps[1]), null),
-    };
-  }
-
-  public static class Employee {
-    public final int empid;
-    public final int deptno;
-    public final String name;
-    public final float salary;
-    public final Integer commission;
-
-    public Employee(int empid, int deptno, String name, float salary,
-        Integer commission) {
-      this.empid = empid;
-      this.deptno = deptno;
-      this.name = name;
-      this.salary = salary;
-      this.commission = commission;
-    }
-
-    @Override public String toString() {
-      return "Employee [empid: " + empid + ", deptno: " + deptno
-          + ", name: " + name + "]";
-    }
-
-    @Override public boolean equals(Object obj) {
-      return obj == this
-          || obj instanceof Employee
-          && empid == ((Employee) obj).empid;
-    }
-  }
-
-  public static class Department {
-    public final int deptno;
-    public final String name;
-
-    @org.apache.calcite.adapter.java.Array(component = Employee.class)
-    public final List<Employee> employees;
-    public final Location location;
-
-    public Department(int deptno, String name, List<Employee> employees,
-        Location location) {
-      this.deptno = deptno;
-      this.name = name;
-      this.employees = employees;
-      this.location = location;
-    }
-
-    @Override public String toString() {
-      return "Department [deptno: " + deptno + ", name: " + name
-          + ", employees: " + employees + ", location: " + location + "]";
-    }
-
-    @Override public boolean equals(Object obj) {
-      return obj == this
-          || obj instanceof Department
-          && deptno == ((Department) obj).deptno;
-    }
-  }
-
-  public static class DepartmentPlus extends Department {
-    public final Timestamp inceptionDate;
-
-    public DepartmentPlus(int deptno, String name, List<Employee> employees,
-        Location location, Timestamp inceptionDate) {
-      super(deptno, name, employees, location);
-      this.inceptionDate = inceptionDate;
-    }
-  }
-
-  public static class Location {
-    public final int x;
-    public final int y;
-
-    public Location(int x, int y) {
-      this.x = x;
-      this.y = y;
-    }
-
-    @Override public String toString() {
-      return "Location [x: " + x + ", y: " + y + "]";
-    }
-
-    @Override public boolean equals(Object obj) {
-      return obj == this
-          || obj instanceof Location
-          && x == ((Location) obj).x
-          && y == ((Location) obj).y;
-    }
-  }
-
-  public static class Dependent {
-    public final int empid;
-    public final String name;
-
-    public Dependent(int empid, String name) {
-      this.empid = empid;
-      this.name = name;
-    }
-
-    @Override public String toString() {
-      return "Dependent [empid: " + empid + ", name: " + name + "]";
-    }
-
-    @Override public boolean equals(Object obj) {
-      return obj == this
-          || obj instanceof Dependent
-          && empid == ((Dependent) obj).empid
-          && Objects.equals(name, ((Dependent) obj).name);
-    }
-  }
-
-  public static class Event {
-    public final int eventid;
-    public final Timestamp ts;
-
-    public Event(int eventid, Timestamp ts) {
-      this.eventid = eventid;
-      this.ts = ts;
-    }
-
-    @Override public String toString() {
-      return "Event [eventid: " + eventid + ", ts: " + ts + "]";
-    }
-
-    @Override public boolean equals(Object obj) {
-      return obj == this
-          || obj instanceof Event
-          && eventid == ((Event) obj).eventid;
-    }
-  }
-
-  public static class FoodmartSchema {
-    public final SalesFact[] sales_fact_1997 = {
-      new SalesFact(100, 10),
-      new SalesFact(150, 20),
-    };
-  }
-
-  public static class LingualSchema {
-    public final LingualEmp[] EMPS = {
-      new LingualEmp(1, 10),
-      new LingualEmp(2, 30)
-    };
-  }
-
-  public static class LingualEmp {
-    public final int EMPNO;
-    public final int DEPTNO;
-
-    public LingualEmp(int EMPNO, int DEPTNO) {
-      this.EMPNO = EMPNO;
-      this.DEPTNO = DEPTNO;
-    }
-
-    @Override public boolean equals(Object obj) {
-      return obj == this
-          || obj instanceof LingualEmp
-          && EMPNO == ((LingualEmp) obj).EMPNO;
-    }
-  }
-
   public static class FoodmartJdbcSchema extends JdbcSchema {
     public FoodmartJdbcSchema(DataSource dataSource, SqlDialect dialect,
         JdbcConvention convention, String catalog, String schema) {
@@ -8103,52 +7782,8 @@
     }
   }
 
-  public static class SalesFact {
-    public final int cust_id;
-    public final int prod_id;
-
-    public SalesFact(int cust_id, int prod_id) {
-      this.cust_id = cust_id;
-      this.prod_id = prod_id;
-    }
-
-    @Override public boolean equals(Object obj) {
-      return obj == this
-          || obj instanceof SalesFact
-          && cust_id == ((SalesFact) obj).cust_id
-          && prod_id == ((SalesFact) obj).prod_id;
-    }
-  }
-
   //CHECKSTYLE: ON
 
-  /** Abstract base class for implementations of {@link ModifiableTable}. */
-  public abstract static class AbstractModifiableTable
-      extends AbstractTable implements ModifiableTable {
-    protected AbstractModifiableTable(String tableName) {
-    }
-
-    public TableModify toModificationRel(
-        RelOptCluster cluster,
-        RelOptTable table,
-        Prepare.CatalogReader catalogReader,
-        RelNode child,
-        TableModify.Operation operation,
-        List<String> updateColumnList,
-        List<RexNode> sourceExpressionList,
-        boolean flattened) {
-      return LogicalTableModify.create(table, catalogReader, child, operation,
-          updateColumnList, sourceExpressionList, flattened);
-    }
-  }
-
-  /** Abstract base class for implementations of {@link ModifiableView}. */
-  public abstract static class AbstractModifiableView
-      extends AbstractTable implements ModifiableView {
-    protected AbstractModifiableView() {
-    }
-  }
-
   /** Factory for EMP and DEPT tables. */
   public static class EmpDeptTableFactory implements TableFactory<Table> {
     public static final TryThreadLocal<List<Employee>> THREAD_COLLECTION =
diff --git a/core/src/test/java/org/apache/calcite/test/LatticeTest.java b/core/src/test/java/org/apache/calcite/test/LatticeTest.java
index 16ea113..7e6e820 100644
--- a/core/src/test/java/org/apache/calcite/test/LatticeTest.java
+++ b/core/src/test/java/org/apache/calcite/test/LatticeTest.java
@@ -26,6 +26,7 @@
 import org.apache.calcite.rel.rules.materialize.MaterializedViewRules;
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.test.schemata.foodmart.FoodmartSchema;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.TestUtil;
 
@@ -173,7 +174,7 @@
         + "{\n"
         + "  version: '1.0',\n"
         + "   schemas: [\n"
-        + JdbcTest.FOODMART_SCHEMA
+        + FoodmartSchema.FOODMART_SCHEMA
         + ",\n"
         + "     {\n"
         + "       name: 'adhoc',\n"
@@ -844,7 +845,7 @@
         + "{\n"
         + "  version: '1.0',\n"
         + "   schemas: [\n"
-        + JdbcTest.FOODMART_SCHEMA
+        + FoodmartSchema.FOODMART_SCHEMA
         + ",\n"
         + "     {\n"
         + "       name: 'adhoc',\n"
@@ -900,7 +901,7 @@
         + "{\n"
         + "  version: '1.0',\n"
         + "   schemas: [\n"
-        + JdbcTest.FOODMART_SCHEMA
+        + FoodmartSchema.FOODMART_SCHEMA
         + ",\n"
         + "     {\n"
         + "       name: 'adhoc',\n"
diff --git a/core/src/test/java/org/apache/calcite/test/MaterializationTest.java b/core/src/test/java/org/apache/calcite/test/MaterializationTest.java
index 9bf37db..48a2f71 100644
--- a/core/src/test/java/org/apache/calcite/test/MaterializationTest.java
+++ b/core/src/test/java/org/apache/calcite/test/MaterializationTest.java
@@ -28,12 +28,12 @@
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.schema.QueryableTable;
 import org.apache.calcite.schema.TranslatableTable;
-import org.apache.calcite.test.JdbcTest.Department;
-import org.apache.calcite.test.JdbcTest.DepartmentPlus;
-import org.apache.calcite.test.JdbcTest.Dependent;
-import org.apache.calcite.test.JdbcTest.Employee;
-import org.apache.calcite.test.JdbcTest.Event;
-import org.apache.calcite.test.JdbcTest.Location;
+import org.apache.calcite.test.schemata.hr.Department;
+import org.apache.calcite.test.schemata.hr.DepartmentPlus;
+import org.apache.calcite.test.schemata.hr.Dependent;
+import org.apache.calcite.test.schemata.hr.Employee;
+import org.apache.calcite.test.schemata.hr.Event;
+import org.apache.calcite.test.schemata.hr.Location;
 import org.apache.calcite.util.JsonBuilder;
 import org.apache.calcite.util.Smalls;
 import org.apache.calcite.util.TryThreadLocal;
diff --git a/core/src/test/java/org/apache/calcite/test/MultiJdbcSchemaJoinTest.java b/core/src/test/java/org/apache/calcite/test/MultiJdbcSchemaJoinTest.java
index 0731ac4..a0bb349 100644
--- a/core/src/test/java/org/apache/calcite/test/MultiJdbcSchemaJoinTest.java
+++ b/core/src/test/java/org/apache/calcite/test/MultiJdbcSchemaJoinTest.java
@@ -25,6 +25,7 @@
 import org.apache.calcite.jdbc.CalciteSchema;
 import org.apache.calcite.jdbc.Driver;
 import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 
 import org.apache.commons.dbcp2.BasicDataSource;
 
@@ -141,7 +142,7 @@
         JdbcSchema.create(rootSchema, "DB",
             JdbcSchema.dataSource(db, "org.hsqldb.jdbcDriver", "", ""),
             null, null));
-    rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+    rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
     return connection;
   }
 
diff --git a/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java b/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java
index 859d2fb..2f1dfec 100644
--- a/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java
+++ b/core/src/test/java/org/apache/calcite/test/ReflectiveSchemaTest.java
@@ -27,12 +27,15 @@
 import org.apache.calcite.linq4j.function.Function1;
 import org.apache.calcite.linq4j.tree.Expressions;
 import org.apache.calcite.linq4j.tree.ParameterExpression;
-import org.apache.calcite.linq4j.tree.Primitive;
 import org.apache.calcite.linq4j.tree.Types;
 import org.apache.calcite.schema.SchemaPlus;
 import org.apache.calcite.schema.impl.AbstractSchema;
 import org.apache.calcite.schema.impl.TableMacroImpl;
 import org.apache.calcite.schema.impl.ViewTable;
+import org.apache.calcite.test.schemata.catchall.CatchallSchema;
+import org.apache.calcite.test.schemata.catchall.CatchallSchema.EveryType;
+import org.apache.calcite.test.schemata.hr.Employee;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 import org.apache.calcite.util.Smalls;
 import org.apache.calcite.util.TestUtil;
 import org.apache.calcite.util.Util;
@@ -44,7 +47,6 @@
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
-import java.math.BigDecimal;
 import java.sql.Connection;
 import java.sql.DriverManager;
 import java.sql.ResultSet;
@@ -55,12 +57,9 @@
 import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.BitSet;
-import java.util.Date;
 import java.util.List;
 import java.util.Properties;
 
-import static org.apache.calcite.test.JdbcTest.Employee;
-
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -100,7 +99,7 @@
                     null,
                     LINQ4J_AS_ENUMERABLE_METHOD,
                     Expressions.constant(
-                        new JdbcTest.HrSchema().emps)),
+                        new HrSchema().emps)),
                 "asQueryable"),
             Employee.class)
             .where(
@@ -149,7 +148,7 @@
                     Types.of(Enumerable.class, Employee.class),
                     null,
                     LINQ4J_AS_ENUMERABLE_METHOD,
-                    Expressions.constant(new JdbcTest.HrSchema().emps)),
+                    Expressions.constant(new HrSchema().emps)),
                 "asQueryable"),
             Employee.class)
             .select(
@@ -176,7 +175,7 @@
         TableMacroImpl.create(Smalls.GENERATE_STRINGS_METHOD));
     schema.add("StringUnion",
         TableMacroImpl.create(Smalls.STRING_UNION_METHOD));
-    rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+    rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
     ResultSet resultSet = connection.createStatement().executeQuery(
         "select *\n"
         + "from table(s.StringUnion(\n"
@@ -200,7 +199,7 @@
         ViewTable.viewMacro(schema,
             "select * from \"hr\".\"emps\" where \"deptno\" = 10",
             null, Arrays.asList("s", "emps_view"), null));
-    rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+    rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
     ResultSet resultSet = connection.createStatement().executeQuery(
         "select *\n"
         + "from \"s\".\"emps_view\"\n"
@@ -237,7 +236,7 @@
     schema.add("null_emps",
         ViewTable.viewMacro(schema, "select * from \"emps\"", null,
             ImmutableList.of("s", "null_emps"), null));
-    rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+    rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
     final Statement statement = connection.createStatement();
     ResultSet resultSet;
     resultSet = statement.executeQuery(
@@ -605,7 +604,7 @@
     with.query("select \"wrapperLong\" / \"wrapperLong\" as c\n"
         + " from \"s\".\"everyTypes\" where \"primitiveLong\" <> 0")
         .planContains(
-            "final Long input_value = ((org.apache.calcite.test.ReflectiveSchemaTest.EveryType) inputEnumerator.current()).wrapperLong;")
+            "final Long input_value = ((org.apache.calcite.test.schemata.catchall.CatchallSchema.EveryType) inputEnumerator.current()).wrapperLong;")
         .planContains(
             "return input_value == null ? (Long) null : Long.valueOf(input_value.longValue() / input_value.longValue());")
         .returns("C=null\n");
@@ -618,7 +617,7 @@
         + "+ \"wrapperLong\" / \"wrapperLong\" as c\n"
         + " from \"s\".\"everyTypes\" where \"primitiveLong\" <> 0")
         .planContains(
-            "final Long input_value = ((org.apache.calcite.test.ReflectiveSchemaTest.EveryType) inputEnumerator.current()).wrapperLong;")
+            "final Long input_value = ((org.apache.calcite.test.schemata.catchall.CatchallSchema.EveryType) inputEnumerator.current()).wrapperLong;")
         .planContains(
             "final Long binary_call_value = input_value == null ? (Long) null : Long.valueOf(input_value.longValue() / input_value.longValue());")
         .planContains(
@@ -678,17 +677,6 @@
         });
   }
 
-  private static boolean isNumeric(Class type) {
-    switch (Primitive.flavor(type)) {
-    case BOX:
-      return Primitive.ofBox(type).isNumeric();
-    case PRIMITIVE:
-      return Primitive.of(type).isNumeric();
-    default:
-      return Number.class.isAssignableFrom(type); // e.g. BigDecimal
-    }
-  }
-
   /** Tests that if a field of a relation has an unrecognized type (in this
    * case a {@link BitSet}) then it is treated as an object.
    *
@@ -743,7 +731,7 @@
   @Disabled
   @Test void testTableMacroIsView() throws Exception {
     CalciteAssert.that()
-        .withSchema("s", new ReflectiveSchema(new JdbcTest.HrSchema()))
+        .withSchema("s", new ReflectiveSchema(new HrSchema()))
         .query("select * from table(\"s\".\"view\"('abc'))")
         .returns(
             "empid=2; deptno=10; name=Ab; salary=0.0; commission=null\n"
@@ -754,7 +742,7 @@
   @Disabled
   @Test void testTableMacro() throws Exception {
     CalciteAssert.that()
-        .withSchema("s", new ReflectiveSchema(new JdbcTest.HrSchema()))
+        .withSchema("s", new ReflectiveSchema(new HrSchema()))
         .query("select * from table(\"s\".\"foo\"(3))")
         .returns(
             "empid=2; deptno=10; name=Ab; salary=0.0; commission=null\n"
@@ -884,173 +872,6 @@
     }
   }
 
-  /** Record that has a field of every interesting type. */
-  public static class EveryType {
-    public final boolean primitiveBoolean;
-    public final byte primitiveByte;
-    public final char primitiveChar;
-    public final short primitiveShort;
-    public final int primitiveInt;
-    public final long primitiveLong;
-    public final float primitiveFloat;
-    public final double primitiveDouble;
-    public final Boolean wrapperBoolean;
-    public final Byte wrapperByte;
-    public final Character wrapperCharacter;
-    public final Short wrapperShort;
-    public final Integer wrapperInteger;
-    public final Long wrapperLong;
-    public final Float wrapperFloat;
-    public final Double wrapperDouble;
-    public final java.sql.Date sqlDate;
-    public final Time sqlTime;
-    public final Timestamp sqlTimestamp;
-    public final Date utilDate;
-    public final String string;
-    public final BigDecimal bigDecimal;
-
-    public EveryType(
-        boolean primitiveBoolean,
-        byte primitiveByte,
-        char primitiveChar,
-        short primitiveShort,
-        int primitiveInt,
-        long primitiveLong,
-        float primitiveFloat,
-        double primitiveDouble,
-        Boolean wrapperBoolean,
-        Byte wrapperByte,
-        Character wrapperCharacter,
-        Short wrapperShort,
-        Integer wrapperInteger,
-        Long wrapperLong,
-        Float wrapperFloat,
-        Double wrapperDouble,
-        java.sql.Date sqlDate,
-        Time sqlTime,
-        Timestamp sqlTimestamp,
-        Date utilDate,
-        String string,
-        BigDecimal bigDecimal) {
-      this.primitiveBoolean = primitiveBoolean;
-      this.primitiveByte = primitiveByte;
-      this.primitiveChar = primitiveChar;
-      this.primitiveShort = primitiveShort;
-      this.primitiveInt = primitiveInt;
-      this.primitiveLong = primitiveLong;
-      this.primitiveFloat = primitiveFloat;
-      this.primitiveDouble = primitiveDouble;
-      this.wrapperBoolean = wrapperBoolean;
-      this.wrapperByte = wrapperByte;
-      this.wrapperCharacter = wrapperCharacter;
-      this.wrapperShort = wrapperShort;
-      this.wrapperInteger = wrapperInteger;
-      this.wrapperLong = wrapperLong;
-      this.wrapperFloat = wrapperFloat;
-      this.wrapperDouble = wrapperDouble;
-      this.sqlDate = sqlDate;
-      this.sqlTime = sqlTime;
-      this.sqlTimestamp = sqlTimestamp;
-      this.utilDate = utilDate;
-      this.string = string;
-      this.bigDecimal = bigDecimal;
-    }
-
-    static Enumerable<Field> fields() {
-      return Linq4j.asEnumerable(EveryType.class.getFields());
-    }
-
-    static Enumerable<Field> numericFields() {
-      return fields()
-          .where(v1 -> isNumeric(v1.getType()));
-    }
-  }
-
-  /** All field are private, therefore the resulting record has no fields. */
-  public static class AllPrivate {
-    private final int x = 0;
-  }
-
-  /** Table that has a field that cannot be recognized as a SQL type. */
-  public static class BadType {
-    public final int integer = 0;
-    public final BitSet bitSet = new BitSet(0);
-  }
-
-  /** Table that has integer and string fields. */
-  public static class IntAndString {
-    public final int id;
-    public final String value;
-
-    public IntAndString(int id, String value) {
-      this.id = id;
-      this.value = value;
-    }
-  }
-
-  /** Object whose fields are relations. Called "catch-all" because it's OK
-   * if tests add new fields. */
-  public static class CatchallSchema {
-    public final Enumerable<Employee> enumerable =
-        Linq4j.asEnumerable(
-            Arrays.asList(new JdbcTest.HrSchema().emps));
-
-    public final List<Employee> list =
-        Arrays.asList(new JdbcTest.HrSchema().emps);
-
-    public final BitSet bitSet = new BitSet(1);
-
-    public final EveryType[] everyTypes = {
-        new EveryType(
-            false, (byte) 0, (char) 0, (short) 0, 0, 0L, 0F, 0D,
-            false, (byte) 0, (char) 0, (short) 0, 0, 0L, 0F, 0D,
-            new java.sql.Date(0), new Time(0), new Timestamp(0),
-            new Date(0), "1", BigDecimal.ZERO),
-        new EveryType(
-            true, Byte.MAX_VALUE, Character.MAX_VALUE, Short.MAX_VALUE,
-            Integer.MAX_VALUE, Long.MAX_VALUE, Float.MAX_VALUE,
-            Double.MAX_VALUE,
-            null, null, null, null, null, null, null, null,
-            null, null, null, null, null, null),
-    };
-
-    public final AllPrivate[] allPrivates = { new AllPrivate() };
-
-    public final BadType[] badTypes = { new BadType() };
-
-    public final Employee[] prefixEmps = {
-        new Employee(1, 10, "A", 0f, null),
-        new Employee(2, 10, "Ab", 0f, null),
-        new Employee(3, 10, "Abc", 0f, null),
-        new Employee(4, 10, "Abd", 0f, null),
-    };
-
-    public final Integer[] primesBoxed = {1, 3, 5};
-
-    public final int[] primes = {1, 3, 5};
-
-    public final IntHolder[] primesCustomBoxed =
-        {new IntHolder(1), new IntHolder(3), new IntHolder(5)};
-
-    public final IntAndString[] nullables = {
-        new IntAndString(1, "A"), new IntAndString(2, "B"), new IntAndString(2, "C"),
-        new IntAndString(3, null)};
-
-    public final IntAndString[] bools = {
-        new IntAndString(1, "T"), new IntAndString(2, "F"), new IntAndString(3, null)};
-  }
-
-  /**
-   * Custom java class that holds just a single field.
-   */
-  public static class IntHolder {
-    public final int value;
-
-    public IntHolder(int value) {
-      this.value = value;
-    }
-  }
-
   /** Schema that contains a table with a date column. */
   public static class DateColumnSchema {
     public final EmployeeWithHireDate[] emps = {
diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
index c095dbb..a6c6232 100644
--- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
@@ -67,6 +67,7 @@
 import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlUserDefinedTableFunction;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.tools.Programs;
 import org.apache.calcite.tools.RelBuilder;
@@ -4390,7 +4391,7 @@
    * SqlStdOperatorTable.NOT_LIKE has a wrong implementor</a>. */
   @Test void testExecuteNotLike() {
     CalciteAssert.that()
-        .withSchema("s", new ReflectiveSchema(new JdbcTest.HrSchema()))
+        .withSchema("s", new ReflectiveSchema(new HrSchema()))
         .query("?")
         .withRel(
             builder -> builder
diff --git a/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java b/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java
index c17d2ae..75932b5 100644
--- a/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RexImplicationCheckerTest.java
@@ -16,28 +16,17 @@
  */
 package org.apache.calcite.test;
 
-import org.apache.calcite.DataContexts;
 import org.apache.calcite.avatica.util.TimeUnitRange;
-import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
-import org.apache.calcite.plan.RelOptPredicateList;
 import org.apache.calcite.plan.RexImplicationChecker;
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.rel.type.RelDataTypeSystem;
-import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexCall;
-import org.apache.calcite.rex.RexExecutorImpl;
-import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexSimplify;
 import org.apache.calcite.rex.RexUnknownAs;
-import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.util.DateString;
-import org.apache.calcite.util.NlsString;
 import org.apache.calcite.util.TimeString;
 import org.apache.calcite.util.TimestampString;
 import org.apache.calcite.util.Util;
@@ -46,15 +35,10 @@
 
 import org.junit.jupiter.api.Test;
 
-import java.math.BigDecimal;
-import java.sql.Date;
-import java.sql.Time;
-import java.sql.Timestamp;
+import static org.apache.calcite.test.RexImplicationCheckerFixtures.Fixture;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.Is.is;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * Unit tests for {@link RexImplicationChecker}.
@@ -446,198 +430,4 @@
     }
   }
 
-  /** Contains all the nourishment a test case could possibly need.
-   *
-   * <p>We put the data in here, rather than as fields in the test case, so that
-   * the data can be garbage-collected as soon as the test has executed.
-   */
-  @SuppressWarnings("WeakerAccess")
-  public static class Fixture {
-    public final RelDataTypeFactory typeFactory;
-    public final RexBuilder rexBuilder;
-    public final RelDataType boolRelDataType;
-    public final RelDataType intRelDataType;
-    public final RelDataType decRelDataType;
-    public final RelDataType longRelDataType;
-    public final RelDataType shortDataType;
-    public final RelDataType byteDataType;
-    public final RelDataType floatDataType;
-    public final RelDataType charDataType;
-    public final RelDataType dateDataType;
-    public final RelDataType timestampDataType;
-    public final RelDataType timeDataType;
-    public final RelDataType stringDataType;
-
-    public final RexNode bl; // a field of Java type "Boolean"
-    public final RexNode i; // a field of Java type "Integer"
-    public final RexNode dec; // a field of Java type "Double"
-    public final RexNode lg; // a field of Java type "Long"
-    public final RexNode sh; // a  field of Java type "Short"
-    public final RexNode by; // a field of Java type "Byte"
-    public final RexNode fl; // a field of Java type "Float" (not a SQL FLOAT)
-    public final RexNode d; // a field of Java type "Date"
-    public final RexNode ch; // a field of Java type "Character"
-    public final RexNode ts; // a field of Java type "Timestamp"
-    public final RexNode t; // a field of Java type "Time"
-    public final RexNode str; // a field of Java type "String"
-
-    public final RexImplicationChecker checker;
-    public final RelDataType rowType;
-    public final RexExecutorImpl executor;
-    public final RexSimplify simplify;
-
-    public Fixture() {
-      typeFactory = new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
-      rexBuilder = new RexBuilder(typeFactory);
-      boolRelDataType = typeFactory.createJavaType(Boolean.class);
-      intRelDataType = typeFactory.createJavaType(Integer.class);
-      decRelDataType = typeFactory.createJavaType(Double.class);
-      longRelDataType = typeFactory.createJavaType(Long.class);
-      shortDataType = typeFactory.createJavaType(Short.class);
-      byteDataType = typeFactory.createJavaType(Byte.class);
-      floatDataType = typeFactory.createJavaType(Float.class);
-      charDataType = typeFactory.createJavaType(Character.class);
-      dateDataType = typeFactory.createJavaType(Date.class);
-      timestampDataType = typeFactory.createJavaType(Timestamp.class);
-      timeDataType = typeFactory.createJavaType(Time.class);
-      stringDataType = typeFactory.createJavaType(String.class);
-
-      bl = ref(0, this.boolRelDataType);
-      i = ref(1, intRelDataType);
-      dec = ref(2, decRelDataType);
-      lg = ref(3, longRelDataType);
-      sh = ref(4, shortDataType);
-      by = ref(5, byteDataType);
-      fl = ref(6, floatDataType);
-      ch = ref(7, charDataType);
-      d = ref(8, dateDataType);
-      ts = ref(9, timestampDataType);
-      t = ref(10, timeDataType);
-      str = ref(11, stringDataType);
-
-      rowType = typeFactory.builder()
-          .add("bool", this.boolRelDataType)
-          .add("int", intRelDataType)
-          .add("dec", decRelDataType)
-          .add("long", longRelDataType)
-          .add("short", shortDataType)
-          .add("byte", byteDataType)
-          .add("float", floatDataType)
-          .add("char", charDataType)
-          .add("date", dateDataType)
-          .add("timestamp", timestampDataType)
-          .add("time", timeDataType)
-          .add("string", stringDataType)
-          .build();
-
-      executor = Frameworks.withPrepare(
-          (cluster, relOptSchema, rootSchema, statement) ->
-              new RexExecutorImpl(
-                  DataContexts.of(statement.getConnection(), rootSchema)));
-      simplify =
-          new RexSimplify(rexBuilder, RelOptPredicateList.EMPTY, executor)
-              .withParanoid(true);
-      checker = new RexImplicationChecker(rexBuilder, executor, rowType);
-    }
-
-    public RexInputRef ref(int i, RelDataType type) {
-      return new RexInputRef(i,
-          typeFactory.createTypeWithNullability(type, true));
-    }
-
-    public RexLiteral literal(int i) {
-      return rexBuilder.makeExactLiteral(new BigDecimal(i));
-    }
-
-    public RexNode gt(RexNode node1, RexNode node2) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN, node1, node2);
-    }
-
-    public RexNode ge(RexNode node1, RexNode node2) {
-      return rexBuilder.makeCall(
-          SqlStdOperatorTable.GREATER_THAN_OR_EQUAL, node1, node2);
-    }
-
-    public RexNode eq(RexNode node1, RexNode node2) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, node1, node2);
-    }
-
-    public RexNode ne(RexNode node1, RexNode node2) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.NOT_EQUALS, node1, node2);
-    }
-
-    public RexNode lt(RexNode node1, RexNode node2) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN, node1, node2);
-    }
-
-    public RexNode le(RexNode node1, RexNode node2) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN_OR_EQUAL, node1,
-          node2);
-    }
-
-    public RexNode notNull(RexNode node1) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, node1);
-    }
-
-    public RexNode isNull(RexNode node2) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, node2);
-    }
-
-    public RexNode and(RexNode... nodes) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.AND, nodes);
-    }
-
-    public RexNode or(RexNode... nodes) {
-      return rexBuilder.makeCall(SqlStdOperatorTable.OR, nodes);
-    }
-
-    public RexNode longLiteral(long value) {
-      return rexBuilder.makeLiteral(value, longRelDataType, true);
-    }
-
-    public RexNode shortLiteral(short value) {
-      return rexBuilder.makeLiteral(value, shortDataType, true);
-    }
-
-    public RexLiteral floatLiteral(double value) {
-      return rexBuilder.makeApproxLiteral(new BigDecimal(value));
-    }
-
-    public RexLiteral charLiteral(String z) {
-      return rexBuilder.makeCharLiteral(
-          new NlsString(z, null, SqlCollation.COERCIBLE));
-    }
-
-    public RexNode dateLiteral(DateString d) {
-      return rexBuilder.makeDateLiteral(d);
-    }
-
-    public RexNode timestampLiteral(TimestampString ts) {
-      return rexBuilder.makeTimestampLiteral(ts,
-          timestampDataType.getPrecision());
-    }
-
-    public RexNode timestampLocalTzLiteral(TimestampString ts) {
-      return rexBuilder.makeTimestampWithLocalTimeZoneLiteral(ts,
-          timestampDataType.getPrecision());
-    }
-
-    public RexNode timeLiteral(TimeString t) {
-      return rexBuilder.makeTimeLiteral(t, timeDataType.getPrecision());
-    }
-
-    public RexNode cast(RelDataType type, RexNode exp) {
-      return rexBuilder.makeCast(type, exp, true);
-    }
-
-    void checkImplies(RexNode node1, RexNode node2) {
-      assertTrue(checker.implies(node1, node2),
-          () -> node1 + " does not imply " + node2 + " when it should");
-    }
-
-    void checkNotImplies(RexNode node1, RexNode node2) {
-      assertFalse(checker.implies(node1, node2),
-          () -> node1 + " does implies " + node2 + " when it should not");
-    }
-  }
 }
diff --git a/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java b/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java
index ef794f2..dacc150 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlAdvisorJdbcTest.java
@@ -24,6 +24,7 @@
 import org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction;
 import org.apache.calcite.sql.advise.SqlAdvisorGetHintsFunction2;
 import org.apache.calcite.sql.parser.StringAndPos;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 
 import org.junit.jupiter.api.Test;
 
@@ -55,7 +56,7 @@
     CalciteConnection calciteConnection =
         connection.unwrap(CalciteConnection.class);
     SchemaPlus rootSchema = calciteConnection.getRootSchema();
-    rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+    rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
     SchemaPlus schema = rootSchema.add("s", new AbstractSchema());
     calciteConnection.setSchema("hr");
     final TableFunction getHints =
diff --git a/core/src/test/java/org/apache/calcite/test/StreamTest.java b/core/src/test/java/org/apache/calcite/test/StreamTest.java
index b106958..6192657 100644
--- a/core/src/test/java/org/apache/calcite/test/StreamTest.java
+++ b/core/src/test/java/org/apache/calcite/test/StreamTest.java
@@ -16,32 +16,14 @@
  */
 package org.apache.calcite.test;
 
-import org.apache.calcite.DataContext;
-import org.apache.calcite.avatica.util.DateTimeUtils;
-import org.apache.calcite.config.CalciteConnectionConfig;
-import org.apache.calcite.linq4j.Enumerable;
-import org.apache.calcite.linq4j.Linq4j;
-import org.apache.calcite.rel.RelCollations;
-import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.rel.type.RelProtoDataType;
-import org.apache.calcite.schema.ScannableTable;
-import org.apache.calcite.schema.Schema;
-import org.apache.calcite.schema.SchemaPlus;
-import org.apache.calcite.schema.Statistic;
-import org.apache.calcite.schema.Statistics;
-import org.apache.calcite.schema.StreamableTable;
-import org.apache.calcite.schema.Table;
 import org.apache.calcite.schema.TableFactory;
-import org.apache.calcite.schema.TemporalTable;
-import org.apache.calcite.sql.SqlCall;
-import org.apache.calcite.sql.SqlNode;
-import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.test.schemata.orderstream.InfiniteOrdersStreamTableFactory;
+import org.apache.calcite.test.schemata.orderstream.OrdersStreamTableFactory;
+import org.apache.calcite.test.schemata.orderstream.ProductsTableFactory;
 import org.apache.calcite.util.TestUtil;
 
 import com.google.common.collect.ImmutableList;
 
-import org.checkerframework.checker.nullness.qual.Nullable;
 import org.hamcrest.comparator.ComparatorMatcherBuilder;
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
@@ -50,8 +32,6 @@
 import java.sql.ResultSet;
 import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
-import java.util.Iterator;
-import java.util.Map;
 import java.util.function.Consumer;
 
 import static org.hamcrest.CoreMatchers.equalTo;
@@ -348,250 +328,5 @@
     };
   }
 
-  /**
-   * Base table for the Orders table. Manages the base schema used for the test tables and common
-   * functions.
-   */
-  private abstract static class BaseOrderStreamTable implements ScannableTable {
-    protected final RelProtoDataType protoRowType = a0 -> a0.builder()
-        .add("ROWTIME", SqlTypeName.TIMESTAMP)
-        .add("ID", SqlTypeName.INTEGER)
-        .add("PRODUCT", SqlTypeName.VARCHAR, 10)
-        .add("UNITS", SqlTypeName.INTEGER)
-        .build();
 
-    public RelDataType getRowType(RelDataTypeFactory typeFactory) {
-      return protoRowType.apply(typeFactory);
-    }
-
-    public Statistic getStatistic() {
-      return Statistics.of(100d, ImmutableList.of(),
-        RelCollations.createSingleton(0));
-    }
-
-    public Schema.TableType getJdbcTableType() {
-      return Schema.TableType.TABLE;
-    }
-
-    @Override public boolean isRolledUp(String column) {
-      return false;
-    }
-
-    @Override public boolean rolledUpColumnValidInsideAgg(String column,
-        SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
-      return false;
-    }
-  }
-
-  /** Mock table that returns a stream of orders from a fixed array. */
-  @SuppressWarnings("UnusedDeclaration")
-  public static class OrdersStreamTableFactory implements TableFactory<Table> {
-    // public constructor, per factory contract
-    public OrdersStreamTableFactory() {
-    }
-
-    public Table create(SchemaPlus schema, String name,
-        Map<String, Object> operand, @Nullable RelDataType rowType) {
-      return new OrdersTable(getRowList());
-    }
-
-    public static ImmutableList<Object[]> getRowList() {
-      final Object[][] rows = {
-          {ts(10, 15, 0), 1, "paint", 10},
-          {ts(10, 24, 15), 2, "paper", 5},
-          {ts(10, 24, 45), 3, "brush", 12},
-          {ts(10, 58, 0), 4, "paint", 3},
-          {ts(11, 10, 0), 5, "paint", 3}
-      };
-      return ImmutableList.copyOf(rows);
-    }
-
-    private static Object ts(int h, int m, int s) {
-      return DateTimeUtils.unixTimestamp(2015, 2, 15, h, m, s);
-    }
-  }
-
-  /** Table representing the ORDERS stream. */
-  public static class OrdersTable extends BaseOrderStreamTable
-      implements StreamableTable {
-    private final ImmutableList<Object[]> rows;
-
-    public OrdersTable(ImmutableList<Object[]> rows) {
-      this.rows = rows;
-    }
-
-    public Enumerable<@Nullable Object[]> scan(DataContext root) {
-      return Linq4j.asEnumerable(rows);
-    }
-
-    @Override public Table stream() {
-      return new OrdersTable(rows);
-    }
-
-    @Override public boolean isRolledUp(String column) {
-      return false;
-    }
-
-    @Override public boolean rolledUpColumnValidInsideAgg(String column,
-        SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
-      return false;
-    }
-  }
-
-  /**
-   * Mock table that returns a stream of orders from a fixed array.
-   */
-  @SuppressWarnings("UnusedDeclaration")
-  public static class InfiniteOrdersStreamTableFactory implements TableFactory<Table> {
-    // public constructor, per factory contract
-    public InfiniteOrdersStreamTableFactory() {
-    }
-
-    public Table create(SchemaPlus schema, String name,
-        Map<String, Object> operand, @Nullable RelDataType rowType) {
-      return new InfiniteOrdersTable();
-    }
-  }
-
-  /**
-   * Table representing an infinitely larger ORDERS stream.
-   */
-  public static class InfiniteOrdersTable extends BaseOrderStreamTable
-      implements StreamableTable {
-    public Enumerable<@Nullable Object[]> scan(DataContext root) {
-      return Linq4j.asEnumerable(() -> new Iterator<Object[]>() {
-        private final String[] items = {"paint", "paper", "brush"};
-        private int counter = 0;
-
-        public boolean hasNext() {
-          return true;
-        }
-
-        public Object[] next() {
-          final int index = counter++;
-          return new Object[]{
-              System.currentTimeMillis(), index, items[index % items.length], 10};
-        }
-
-        public void remove() {
-          throw new UnsupportedOperationException();
-        }
-      });
-    }
-
-    public Table stream() {
-      return this;
-    }
-  }
-
-  /** Table representing the history of the ORDERS stream. */
-  public static class OrdersHistoryTable extends BaseOrderStreamTable {
-    private final ImmutableList<Object[]> rows;
-
-    public OrdersHistoryTable(ImmutableList<Object[]> rows) {
-      this.rows = rows;
-    }
-
-    public Enumerable<@Nullable Object[]> scan(DataContext root) {
-      return Linq4j.asEnumerable(rows);
-    }
-  }
-
-  /**
-   * Mocks a simple relation to use for stream joining test.
-   */
-  public static class ProductsTableFactory implements TableFactory<Table> {
-    public Table create(SchemaPlus schema, String name,
-        Map<String, Object> operand, @Nullable RelDataType rowType) {
-      final Object[][] rows = {
-          {"paint", 1},
-          {"paper", 0},
-          {"brush", 1}
-      };
-      return new ProductsTable(ImmutableList.copyOf(rows));
-    }
-  }
-
-  /**
-   * Table representing the PRODUCTS relation.
-   */
-  public static class ProductsTable implements ScannableTable {
-    private final ImmutableList<Object[]> rows;
-
-    public ProductsTable(ImmutableList<Object[]> rows) {
-      this.rows = rows;
-    }
-
-    private final RelProtoDataType protoRowType = a0 -> a0.builder()
-        .add("ID", SqlTypeName.VARCHAR, 32)
-        .add("SUPPLIER", SqlTypeName.INTEGER)
-        .build();
-
-    public Enumerable<@Nullable Object[]> scan(DataContext root) {
-      return Linq4j.asEnumerable(rows);
-    }
-
-    public RelDataType getRowType(RelDataTypeFactory typeFactory) {
-      return protoRowType.apply(typeFactory);
-    }
-
-    public Statistic getStatistic() {
-      return Statistics.of(200d, ImmutableList.of());
-    }
-
-    public Schema.TableType getJdbcTableType() {
-      return Schema.TableType.TABLE;
-    }
-
-    @Override public boolean isRolledUp(String column) {
-      return false;
-    }
-
-    @Override public boolean rolledUpColumnValidInsideAgg(String column,
-        SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
-      return false;
-    }
-  }
-
-  /**
-   * Table representing the PRODUCTS_TEMPORAL temporal table.
-   */
-  public static class ProductsTemporalTable implements TemporalTable {
-
-    private final RelProtoDataType protoRowType = a0 -> a0.builder()
-        .add("ID", SqlTypeName.VARCHAR, 32)
-        .add("SUPPLIER", SqlTypeName.INTEGER)
-        .add("SYS_START", SqlTypeName.TIMESTAMP)
-        .add("SYS_END", SqlTypeName.TIMESTAMP)
-        .build();
-
-    @Override public String getSysStartFieldName() {
-      return "SYS_START";
-    }
-
-    @Override public String getSysEndFieldName() {
-      return "SYS_END";
-    }
-
-    @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
-      return protoRowType.apply(typeFactory);
-    }
-
-    @Override public Statistic getStatistic() {
-      return Statistics.of(200d, ImmutableList.of());
-    }
-
-    @Override public Schema.TableType getJdbcTableType() {
-      return Schema.TableType.TABLE;
-    }
-
-    @Override public boolean isRolledUp(String column) {
-      return false;
-    }
-
-    @Override public boolean rolledUpColumnValidInsideAgg(String column,
-        SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
-      return false;
-    }
-  }
 }
diff --git a/core/src/test/java/org/apache/calcite/test/UdfTest.java b/core/src/test/java/org/apache/calcite/test/UdfTest.java
index 9fee555..56216a5 100644
--- a/core/src/test/java/org/apache/calcite/test/UdfTest.java
+++ b/core/src/test/java/org/apache/calcite/test/UdfTest.java
@@ -34,6 +34,7 @@
 import org.apache.calcite.schema.impl.ScalarFunctionImpl;
 import org.apache.calcite.schema.impl.ViewTable;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 import org.apache.calcite.util.Smalls;
 
 import com.google.common.collect.ImmutableList;
@@ -278,7 +279,7 @@
     CalciteConnection calciteConnection =
         connection.unwrap(CalciteConnection.class);
     SchemaPlus rootSchema = calciteConnection.getRootSchema();
-    rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+    rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
 
     SchemaPlus post = rootSchema.add("POST", new AbstractSchema());
     post.add("MY_INCREMENT",
@@ -1000,7 +1001,7 @@
       CalciteConnection calciteConnection =
           connection.unwrap(CalciteConnection.class);
       SchemaPlus rootSchema = calciteConnection.getRootSchema();
-      rootSchema.add("hr", new ReflectiveSchema(new JdbcTest.HrSchema()));
+      rootSchema.add("hr", new ReflectiveSchema(new HrSchema()));
 
       SchemaPlus post = rootSchema.add("POST", new AbstractSchema());
       post.add("ARRAY_APPEND", new ArrayAppendDoubleFunction());
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableBatchNestedLoopJoinTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableBatchNestedLoopJoinTest.java
index ad588cc..c10edda 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableBatchNestedLoopJoinTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableBatchNestedLoopJoinTest.java
@@ -26,7 +26,8 @@
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.HrSchema;
+import org.apache.calcite.test.schemata.hr.HrSchemaBig;
 
 import org.junit.jupiter.api.Test;
 
@@ -39,7 +40,7 @@
 class EnumerableBatchNestedLoopJoinTest {
 
   @Test void simpleInnerBatchJoinTestBuilder() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           planner.removeRule(EnumerableRules.ENUMERABLE_CORRELATE_RULE);
@@ -63,7 +64,7 @@
   }
 
   @Test void simpleInnerBatchJoinTestSQL() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.name from emps e join depts d on d.deptno = e.deptno")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -76,7 +77,7 @@
   }
 
   @Test void simpleLeftBatchJoinTestSQL() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.name, d.deptno from emps e left join depts d on d.deptno = e.deptno")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -90,7 +91,7 @@
   }
 
   @Test void innerBatchJoinTestSQL() {
-    tester(false, new JdbcTest.HrSchemaBig())
+    tester(false, new HrSchemaBig())
         .query(
             "select count(e.name) from emps e join depts d on d.deptno = e.deptno")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -101,7 +102,7 @@
   }
 
   @Test void innerBatchJoinTestSQL2() {
-    tester(false, new JdbcTest.HrSchemaBig())
+    tester(false, new HrSchemaBig())
         .query(
             "select count(e.name) from emps e join depts d on d.deptno = e.empid")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -112,7 +113,7 @@
   }
 
   @Test void leftBatchJoinTestSQL() {
-    tester(false, new JdbcTest.HrSchemaBig())
+    tester(false, new HrSchemaBig())
         .query(
             "select count(d.deptno) from depts d left join emps e on d.deptno = e.deptno"
             + " where d.deptno <30 and d.deptno>10")
@@ -126,7 +127,7 @@
   @Test void testJoinSubQuery() {
     String sql = "SELECT count(name) FROM emps e WHERE e.deptno NOT IN "
         + "(SELECT d.deptno FROM depts d WHERE d.name = 'Sales')";
-    tester(false, new JdbcTest.HrSchemaBig())
+    tester(false, new HrSchemaBig())
         .query(sql)
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           planner.removeRule(EnumerableRules.ENUMERABLE_CORRELATE_RULE);
@@ -139,7 +140,7 @@
 
   @Test void testInnerJoinOnString() {
     String sql = "SELECT d.name, e.salary FROM depts d join emps e on d.name = e.name";
-    tester(false, new JdbcTest.HrSchemaBig())
+    tester(false, new HrSchemaBig())
         .query(sql)
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           planner.removeRule(EnumerableRules.ENUMERABLE_CORRELATE_RULE);
@@ -150,7 +151,7 @@
         .returnsUnordered("");
   }
   @Test void testSemiJoin() {
-    tester(false, new JdbcTest.HrSchemaBig())
+    tester(false, new HrSchemaBig())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           planner.removeRule(EnumerableRules.ENUMERABLE_CORRELATE_RULE);
@@ -177,7 +178,7 @@
   }
 
   @Test void testAntiJoin() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           planner.removeRule(EnumerableRules.ENUMERABLE_CORRELATE_RULE);
@@ -208,7 +209,7 @@
   }
 
   @Test void innerBatchJoinAndTestSQL() {
-    tester(false, new JdbcTest.HrSchemaBig())
+    tester(false, new HrSchemaBig())
         .query(
             "select count(e.name) from emps e join depts d on d.deptno = e.empid and d.deptno = e.deptno")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -223,7 +224,7 @@
    * Join with three tables causes IllegalArgumentException
    * in EnumerableBatchNestedLoopJoinRule</a>. */
   @Test void doubleInnerBatchJoinTestSQL() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("select e.name, d.name as dept, l.name as location "
             + "from emps e join depts d on d.deptno <> e.salary "
             + "join locations l on e.empid <> l.empid and d.deptno = l.empid")
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCalcTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCalcTest.java
index c5d5e1f..c13ff33 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCalcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCalcTest.java
@@ -20,7 +20,7 @@
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 
 import org.junit.jupiter.api.Test;
 
@@ -37,7 +37,7 @@
    */
   @Test void testCoalesceImplementation() {
     CalciteAssert.that()
-        .withSchema("s", new ReflectiveSchema(new JdbcTest.HrSchema()))
+        .withSchema("s", new ReflectiveSchema(new HrSchema()))
         .query("?")
         .withRel(
             builder -> builder
@@ -93,7 +93,7 @@
       SqlOperator operator,
       String... expectedResult) {
     CalciteAssert.that()
-        .withSchema("s", new ReflectiveSchema(new JdbcTest.HrSchema()))
+        .withSchema("s", new ReflectiveSchema(new HrSchema()))
         .query("?")
         .withRel(
             builder -> builder
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java
index fa15900..1182007 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableCorrelateTest.java
@@ -26,7 +26,7 @@
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 
 import org.junit.jupiter.api.Test;
 
@@ -42,7 +42,7 @@
    * NullPointerException when left outer join implemented with
    * EnumerableCorrelate</a>. */
   @Test void leftOuterJoinCorrelate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.empid, e.name, d.name as dept from emps e left outer join depts d on e.deptno=d.deptno")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -67,7 +67,7 @@
   }
 
   @Test void simpleCorrelateDecorrelated() {
-    tester(true, new JdbcTest.HrSchema())
+    tester(true, new HrSchema())
         .query(
             "select empid, name from emps e where exists (select 1 from depts d where d.deptno=e.deptno)")
         .explainContains(""
@@ -86,7 +86,7 @@
    * <a href="https://issues.apache.org/jira/browse/CALCITE-2621">[CALCITE-2621]
    * Add rule to execute semi joins with correlation</a>. */
   @Test void semiJoinCorrelate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select empid, name from emps e where e.deptno in (select d.deptno from depts d)")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -114,7 +114,7 @@
    * FilterCorrelateRule on a Correlate with SemiJoinType SEMI (or ANTI) throws
    * IllegalStateException</a>. */
   @Test void semiJoinCorrelateWithFilterCorrelateRule() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select empid, name from emps e where e.deptno in (select d.deptno from depts d) and e.empid > 100")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
@@ -139,7 +139,7 @@
   }
 
   @Test void simpleCorrelate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select empid, name from emps e where exists (select 1 from depts d where d.deptno=e.deptno)")
         .explainContains(""
@@ -159,7 +159,7 @@
   @Test void simpleCorrelateWithConditionIncludingBoxedPrimitive() {
     final String sql = "select empid from emps e where not exists (\n"
         + "  select 1 from depts d where d.deptno=e.commission)";
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(sql)
         .returnsUnordered(
             "empid=100",
@@ -172,7 +172,7 @@
    * <a href="https://issues.apache.org/jira/browse/CALCITE-2920">[CALCITE-2920]
    * RelBuilder: new method to create an anti-join</a>. */
   @Test void antiJoinCorrelate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           // force the antijoin to run via EnumerableCorrelate
@@ -201,7 +201,7 @@
   }
 
   @Test void nonEquiAntiJoinCorrelate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           // force the antijoin to run via EnumerableCorrelate
@@ -241,7 +241,7 @@
    * RelBuilder: new method to create an antijoin</a>. */
   @Test void antiJoinCorrelateWithNullValues() {
     final Integer salesDeptNo = 10;
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           // force the antijoin to run via EnumerableCorrelate
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java
index 183dccd..7a85d14 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java
@@ -24,7 +24,7 @@
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 
 import org.junit.jupiter.api.Test;
 
@@ -37,7 +37,7 @@
 class EnumerableHashJoinTest {
 
   @Test void innerJoin() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.empid, e.name, d.name as dept from emps e join depts "
                 + "d on e.deptno=d.deptno")
@@ -57,7 +57,7 @@
   }
 
   @Test void innerJoinWithPredicate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.empid, e.name, d.name as dept from emps e join depts d"
                 + " on e.deptno=d.deptno and e.empid<150 and e.empid>d.deptno")
@@ -75,7 +75,7 @@
   }
 
   @Test void leftOuterJoin() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.empid, e.name, d.name as dept from emps e  left outer "
                 + "join depts d on e.deptno=d.deptno")
@@ -96,7 +96,7 @@
   }
 
   @Test void rightOuterJoin() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.empid, e.name, d.name as dept from emps e  right outer "
                 + "join depts d on e.deptno=d.deptno")
@@ -116,7 +116,7 @@
   }
 
   @Test void leftOuterJoinWithPredicate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.empid, e.name, d.name as dept from emps e left outer "
                 + "join depts d on e.deptno=d.deptno and e.empid<150 and e"
@@ -139,7 +139,7 @@
   }
 
   @Test void rightOuterJoinWithPredicate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select e.empid, e.name, d.name as dept from emps e right outer "
                 + "join depts d on e.deptno=d.deptno and e.empid<150")
@@ -160,7 +160,7 @@
 
 
   @Test void semiJoin() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "SELECT d.deptno, d.name FROM depts d WHERE d.deptno in (SELECT e.deptno FROM emps e)")
         .explainContains("EnumerableHashJoin(condition=[=($0, $3)], "
@@ -173,7 +173,7 @@
   }
 
   @Test void semiJoinWithPredicate() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withRel(
             // Retrieve employees with the top salary in their department. Equivalent SQL:
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableJoinTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableJoinTest.java
index c8a20bc..58aa921 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableJoinTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableJoinTest.java
@@ -27,8 +27,8 @@
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.HierarchySchema;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.HierarchySchema;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 
 import org.junit.jupiter.api.Test;
 
@@ -43,7 +43,7 @@
    * <a href="https://issues.apache.org/jira/browse/CALCITE-2968">[CALCITE-2968]
    * New AntiJoin relational expression</a>. */
   @Test void equiAntiJoin() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withRel(
             // Retrieve departments without employees. Equivalent SQL:
@@ -69,7 +69,7 @@
    * <a href="https://issues.apache.org/jira/browse/CALCITE-2968">[CALCITE-2968]
    * New AntiJoin relational expression</a>. */
   @Test void nonEquiAntiJoin() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withRel(
             // Retrieve employees with the top salary in their department. Equivalent SQL:
@@ -103,7 +103,7 @@
    * New AntiJoin relational expression</a>. */
   @Test void equiAntiJoinWithNullValues() {
     final Integer salesDeptNo = 10;
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withRel(
             // Retrieve employees from any department other than Sales (deptno 10) whose
@@ -141,7 +141,7 @@
    * <a href="https://issues.apache.org/jira/browse/CALCITE-3170">[CALCITE-3170]
    * ANTI join on conditions push down generates wrong plan</a>. */
   @Test void testCanNotPushAntiJoinConditionsToLeft() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?").withRel(
             // build a rel equivalent to sql:
             // select * from emps
@@ -171,7 +171,7 @@
    * The test verifies if {@link EnumerableMergeJoin} can implement a join with non-equi conditions.
    */
   @Test void testSortMergeJoinWithNonEquiCondition() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           planner.addRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
@@ -224,7 +224,7 @@
    * <a href="https://issues.apache.org/jira/browse/CALCITE-3846">[CALCITE-3846]
    * EnumerableMergeJoin: wrong comparison of composite key with null values</a>. */
   @Test void testMergeJoinWithCompositeKeyAndNullValues() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("?")
         .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
           planner.addRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java
index 483c0fc..68bb56c 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableMergeUnionTest.java
@@ -23,7 +23,7 @@
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.HrSchemaBig;
 
 import org.junit.jupiter.api.Test;
 
@@ -37,7 +37,7 @@
 
   @Test void mergeUnionAllOrderByEmpid() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select empid, name from emps where name like 'G%' union all select empid, name from emps where name like '%l') order by empid")
         .explainContains("EnumerableMergeUnion(all=[true])\n"
             + "  EnumerableSort(sort0=[$0], dir0=[ASC])\n"
@@ -59,7 +59,7 @@
 
   @Test void mergeUnionOrderByEmpid() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select empid, name from emps where name like 'G%' union select empid, name from emps where name like '%l') order by empid")
         .explainContains("EnumerableMergeUnion(all=[false])\n"
             + "  EnumerableSort(sort0=[$0], dir0=[ASC])\n"
@@ -80,7 +80,7 @@
 
   @Test void mergeUnionAllOrderByName() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select empid, name from emps where name like 'G%' union all select empid, name from emps where name like '%l') order by name")
         .explainContains("EnumerableMergeUnion(all=[true])\n"
             + "  EnumerableSort(sort0=[$1], dir0=[ASC])\n"
@@ -102,7 +102,7 @@
 
   @Test void mergeUnionOrderByName() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select empid, name from emps where name like 'G%' union select empid, name from emps where name like '%l') order by name")
         .explainContains("EnumerableMergeUnion(all=[false])\n"
             + "  EnumerableSort(sort0=[$1], dir0=[ASC])\n"
@@ -123,7 +123,7 @@
 
   @Test void mergeUnionSingleColumnOrderByName() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select name from emps where name like 'G%' union select name from emps where name like '%l') order by name")
         .explainContains("EnumerableMergeUnion(all=[false])\n"
             + "  EnumerableSort(sort0=[$0], dir0=[ASC])\n"
@@ -144,7 +144,7 @@
 
   @Test void mergeUnionOrderByNameWithLimit() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select empid, name from emps where name like 'G%' union select empid, name from emps where name like '%l') order by name limit 3")
         .explainContains("EnumerableLimit(fetch=[3])\n"
             + "  EnumerableMergeUnion(all=[false])\n"
@@ -164,7 +164,7 @@
 
   @Test void mergeUnionOrderByNameWithOffset() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select empid, name from emps where name like 'G%' union select empid, name from emps where name like '%l') order by name offset 2")
         .explainContains("EnumerableLimit(offset=[2])\n"
             + "  EnumerableMergeUnion(all=[false])\n"
@@ -184,7 +184,7 @@
 
   @Test void mergeUnionOrderByNameWithLimitAndOffset() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select empid, name from emps where name like 'G%' union select empid, name from emps where name like '%l') order by name limit 3 offset 2")
         .explainContains("EnumerableLimit(offset=[2], fetch=[3])\n"
             + "  EnumerableMergeUnion(all=[false])\n"
@@ -204,7 +204,7 @@
 
   @Test void mergeUnionAllOrderByCommissionAscNullsFirstAndNameDesc() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select commission, name from emps where name like 'R%' union all select commission, name from emps where name like '%y%') order by commission asc nulls first, name desc")
         .explainContains("EnumerableMergeUnion(all=[true])\n"
             + "  EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC-nulls-first], dir1=[DESC])\n"
@@ -227,7 +227,7 @@
 
   @Test void mergeUnionOrderByCommissionAscNullsFirstAndNameDesc() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select commission, name from emps where name like 'R%' union select commission, name from emps where name like '%y%') order by commission asc nulls first, name desc")
         .explainContains("EnumerableMergeUnion(all=[false])\n"
             + "  EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC-nulls-first], dir1=[DESC])\n"
@@ -249,7 +249,7 @@
 
   @Test void mergeUnionAllOrderByCommissionAscNullsLastAndNameDesc() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select commission, name from emps where name like 'R%' union all select commission, name from emps where name like '%y%') order by commission asc nulls last, name desc")
         .explainContains("EnumerableMergeUnion(all=[true])\n"
             + "  EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC], dir1=[DESC])\n"
@@ -272,7 +272,7 @@
 
   @Test void mergeUnionOrderByCommissionAscNullsLastAndNameDesc() {
     tester(false,
-        new JdbcTest.HrSchemaBig(),
+        new HrSchemaBig(),
         "select * from (select commission, name from emps where name like 'R%' union select commission, name from emps where name like '%y%') order by commission asc nulls last, name desc")
         .explainContains("EnumerableMergeUnion(all=[false])\n"
             + "  EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC], dir1=[DESC])\n"
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableRepeatUnionHierarchyTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableRepeatUnionHierarchyTest.java
index 06fb7a0..a47b58e 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableRepeatUnionHierarchyTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableRepeatUnionHierarchyTest.java
@@ -23,7 +23,7 @@
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.schema.Schema;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.HierarchySchema;
+import org.apache.calcite.test.schemata.hr.HierarchySchema;
 import org.apache.calcite.tools.RelBuilder;
 
 import org.junit.jupiter.params.ParameterizedTest;
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableSortedAggregateTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableSortedAggregateTest.java
index f39696b..1945dab 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableSortedAggregateTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableSortedAggregateTest.java
@@ -23,7 +23,7 @@
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 
 import org.junit.jupiter.api.Test;
 
@@ -33,7 +33,7 @@
  * {@link org.apache.calcite.adapter.enumerable.EnumerableSortedAggregate}. */
 public class EnumerableSortedAggregateTest {
   @Test void sortedAgg() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query("select deptno, "
             + "max(salary) as max_salary, count(name) as num_employee "
             + "from emps group by deptno")
@@ -51,7 +51,7 @@
   }
 
   @Test void sortedAggTwoGroupKeys() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select deptno, commission, "
                 + "max(salary) as max_salary, count(name) as num_employee "
@@ -73,7 +73,7 @@
 
   // Outer sort is expected to be pushed through aggregation.
   @Test void sortedAggGroupbyXOrderbyX() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select deptno, "
                 + "max(salary) as max_salary, count(name) as num_employee "
@@ -93,7 +93,7 @@
 
   // Outer sort is not expected to be pushed through aggregation.
   @Test void sortedAggGroupbyXOrderbyY() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select deptno, "
                 + "max(salary) as max_salary, count(name) as num_employee "
@@ -113,7 +113,7 @@
   }
 
   @Test void sortedAggNullValueInSortedGroupByKeys() {
-    tester(false, new JdbcTest.HrSchema())
+    tester(false, new HrSchema())
         .query(
             "select commission, "
                 + "count(deptno) as num_dept "
diff --git a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableStringComparisonTest.java b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableStringComparisonTest.java
index e814713..b57eb28 100644
--- a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableStringComparisonTest.java
+++ b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableStringComparisonTest.java
@@ -36,8 +36,8 @@
 import org.apache.calcite.sql.SqlOperator;
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.test.CalciteAssert;
-import org.apache.calcite.test.JdbcTest;
 import org.apache.calcite.test.RelBuilderTest;
+import org.apache.calcite.test.schemata.hr.HrSchema;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.Util;
 
@@ -287,6 +287,6 @@
     return CalciteAssert.that()
         .with(CalciteConnectionProperty.LEX, Lex.JAVA)
         .with(CalciteConnectionProperty.FORCE_DECORRELATE, false)
-        .withSchema("s", new ReflectiveSchema(new JdbcTest.HrSchema()));
+        .withSchema("s", new ReflectiveSchema(new HrSchema()));
   }
 }
diff --git a/core/src/test/java/org/apache/calcite/tools/PlannerTest.java b/core/src/test/java/org/apache/calcite/tools/PlannerTest.java
index 1a70376..a75ac3c 100644
--- a/core/src/test/java/org/apache/calcite/tools/PlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/tools/PlannerTest.java
@@ -79,6 +79,7 @@
 import org.apache.calcite.sql.validate.SqlValidatorScope;
 import org.apache.calcite.test.CalciteAssert;
 import org.apache.calcite.test.RelBuilderTest;
+import org.apache.calcite.test.schemata.tpch.TpchSchema;
 import org.apache.calcite.util.Optionality;
 import org.apache.calcite.util.Smalls;
 import org.apache.calcite.util.Util;
diff --git a/druid/build.gradle.kts b/druid/build.gradle.kts
index c58a8bd..c649260 100644
--- a/druid/build.gradle.kts
+++ b/druid/build.gradle.kts
@@ -27,7 +27,7 @@
     implementation("com.google.guava:guava")
     implementation("org.apache.commons:commons-lang3")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("org.mockito:mockito-core")
     testRuntimeOnly("org.slf4j:slf4j-log4j12")
 }
diff --git a/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java b/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java
index 9767b73..3393154 100644
--- a/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java
+++ b/druid/src/test/java/org/apache/calcite/test/DruidDateRangeRulesTest.java
@@ -21,7 +21,7 @@
 import org.apache.calcite.rel.rules.DateRangeRules;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
-import org.apache.calcite.test.RexImplicationCheckerTest.Fixture;
+import org.apache.calcite.test.RexImplicationCheckerFixtures.Fixture;
 import org.apache.calcite.util.TimestampString;
 import org.apache.calcite.util.Util;
 
diff --git a/elasticsearch/build.gradle.kts b/elasticsearch/build.gradle.kts
index 147338b..7c595f4 100644
--- a/elasticsearch/build.gradle.kts
+++ b/elasticsearch/build.gradle.kts
@@ -58,7 +58,7 @@
     testImplementation("org.codelibs.elasticsearch.module:lang-painless")
     testImplementation("org.elasticsearch.plugin:transport-netty4-client")
     testImplementation("org.elasticsearch:elasticsearch")
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testRuntimeOnly("net.java.dev.jna:jna")
     testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j-impl")
 }
diff --git a/example/csv/build.gradle.kts b/example/csv/build.gradle.kts
index ce8e973..909ae32 100644
--- a/example/csv/build.gradle.kts
+++ b/example/csv/build.gradle.kts
@@ -31,9 +31,9 @@
     implementation("org.apache.calcite.avatica:avatica-core")
 
     testImplementation("sqlline:sqlline")
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
 
-    sqllineClasspath(project(":example:csv", "testClasses"))
+    sqllineClasspath(files(sourceSets.test.map { it.output }))
 }
 
 val buildSqllineClasspath by tasks.registering(Jar::class) {
diff --git a/file/build.gradle.kts b/file/build.gradle.kts
index a2a7391..1aeed01 100644
--- a/file/build.gradle.kts
+++ b/file/build.gradle.kts
@@ -29,5 +29,5 @@
     implementation("com.fasterxml.jackson.core:jackson-core")
     implementation("com.fasterxml.jackson.core:jackson-databind")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
 }
diff --git a/geode/build.gradle.kts b/geode/build.gradle.kts
index 8e1bf1a..4881825 100644
--- a/geode/build.gradle.kts
+++ b/geode/build.gradle.kts
@@ -25,7 +25,7 @@
     implementation("org.apache.calcite.avatica:avatica-core")
     implementation("org.apache.commons:commons-lang3")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("com.fasterxml.jackson.core:jackson-databind")
     testRuntimeOnly("org.slf4j:slf4j-log4j12")
 }
diff --git a/gradle.properties b/gradle.properties
index 422548d..3970b09 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -85,6 +85,7 @@
 commons-dbcp2.version=2.6.0
 commons-io.version=2.4
 commons-lang3.version=3.8
+commons-pool2.version=2.6.2
 dropwizard-metrics.version=4.0.5
 elasticsearch.version=7.0.1
 embedded-redis.version=0.6
diff --git a/innodb/build.gradle.kts b/innodb/build.gradle.kts
index 88239b6..2e8d01f 100644
--- a/innodb/build.gradle.kts
+++ b/innodb/build.gradle.kts
@@ -25,6 +25,6 @@
     implementation("org.apache.commons:commons-lang3")
     implementation("org.slf4j:slf4j-api")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testRuntimeOnly("org.slf4j:slf4j-log4j12")
 }
diff --git a/kafka/build.gradle.kts b/kafka/build.gradle.kts
index 853c427..b721118 100644
--- a/kafka/build.gradle.kts
+++ b/kafka/build.gradle.kts
@@ -22,5 +22,5 @@
 
     implementation("com.google.guava:guava")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
 }
diff --git a/mongodb/build.gradle.kts b/mongodb/build.gradle.kts
index 49411a8..c36e4fa 100644
--- a/mongodb/build.gradle.kts
+++ b/mongodb/build.gradle.kts
@@ -23,7 +23,7 @@
     implementation("org.apache.calcite.avatica:avatica-core")
     implementation("org.mongodb:mongo-java-driver")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("de.bwaldvogel:mongo-java-server-core")
     testImplementation("de.bwaldvogel:mongo-java-server-memory-backend")
     testImplementation("net.hydromatic:foodmart-data-json")
diff --git a/pig/build.gradle.kts b/pig/build.gradle.kts
index 533ebd1..6bd99cd 100644
--- a/pig/build.gradle.kts
+++ b/pig/build.gradle.kts
@@ -22,7 +22,7 @@
     implementation("org.apache.calcite.avatica:avatica-core")
     implementation("org.apache.pig:pig::h2")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("org.apache.hadoop:hadoop-client")
     testImplementation("org.apache.hadoop:hadoop-common")
     testImplementation("org.apache.pig:pigunit") {
diff --git a/piglet/build.gradle.kts b/piglet/build.gradle.kts
index 601bd04..2b33f72 100644
--- a/piglet/build.gradle.kts
+++ b/piglet/build.gradle.kts
@@ -30,7 +30,7 @@
     implementation("org.checkerframework:checker-qual")
     implementation("org.slf4j:slf4j-api")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("net.hydromatic:scott-data-hsqldb")
     testImplementation("org.apache.hadoop:hadoop-client")
     testImplementation("org.hsqldb:hsqldb")
diff --git a/core/src/test/java/org/apache/calcite/test/PigRelBuilderTest.java b/piglet/src/test/java/org/apache/calcite/test/PigRelBuilderTest.java
similarity index 91%
rename from core/src/test/java/org/apache/calcite/test/PigRelBuilderTest.java
rename to piglet/src/test/java/org/apache/calcite/test/PigRelBuilderTest.java
index 1335123..1c104bf 100644
--- a/core/src/test/java/org/apache/calcite/test/PigRelBuilderTest.java
+++ b/piglet/src/test/java/org/apache/calcite/test/PigRelBuilderTest.java
@@ -18,14 +18,19 @@
 
 import org.apache.calcite.plan.Contexts;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelTraitDef;
 import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.tools.PigRelBuilder;
+import org.apache.calcite.tools.Programs;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.Util;
 
 import org.junit.jupiter.api.Test;
 
+import java.util.List;
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
 
@@ -38,7 +43,13 @@
 class PigRelBuilderTest {
   /** Creates a config based on the "scott" schema. */
   public static Frameworks.ConfigBuilder config() {
-    return RelBuilderTest.config();
+    final SchemaPlus rootSchema = Frameworks.createRootSchema(true);
+    return Frameworks.newConfigBuilder()
+        .parserConfig(SqlParser.Config.DEFAULT)
+        .defaultSchema(
+            CalciteAssert.addSchema(rootSchema, CalciteAssert.SchemaSpec.SCOTT_WITH_TEMPORAL))
+        .traitDefs((List<RelTraitDef>) null)
+        .programs(Programs.heuristicJoinOrder(Programs.RULE_SET, true, 2));
   }
 
   static PigRelBuilder createBuilder(
diff --git a/plus/build.gradle.kts b/plus/build.gradle.kts
index e42bf37..f3ea54f 100644
--- a/plus/build.gradle.kts
+++ b/plus/build.gradle.kts
@@ -29,6 +29,6 @@
     implementation("org.apache.calcite.avatica:avatica-server")
     implementation("org.hsqldb:hsqldb")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("org.incava:java-diff")
 }
diff --git a/redis/build.gradle.kts b/redis/build.gradle.kts
index a0ad012..0579a5c 100644
--- a/redis/build.gradle.kts
+++ b/redis/build.gradle.kts
@@ -27,7 +27,7 @@
     implementation("org.apache.commons:commons-pool2")
     implementation("org.slf4j:slf4j-api")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("com.github.kstyrc:embedded-redis")
     testImplementation("org.mockito:mockito-core")
     testRuntimeOnly("org.slf4j:slf4j-log4j12")
diff --git a/server/build.gradle.kts b/server/build.gradle.kts
index 8c467b8..ede256e 100644
--- a/server/build.gradle.kts
+++ b/server/build.gradle.kts
@@ -28,7 +28,7 @@
     implementation("com.google.guava:guava")
     implementation("org.slf4j:slf4j-api")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testImplementation("net.hydromatic:quidem")
     testImplementation("net.hydromatic:scott-data-hsqldb")
     testImplementation("org.hsqldb:hsqldb")
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 9bc3d53..bd21e67 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -80,6 +80,7 @@
     "server",
     "spark",
     "splunk",
+    "testkit",
     "ubenchmark"
 )
 
diff --git a/spark/build.gradle.kts b/spark/build.gradle.kts
index f56f4e9..f2baa7a 100644
--- a/spark/build.gradle.kts
+++ b/spark/build.gradle.kts
@@ -27,5 +27,5 @@
     runtimeOnly("xalan:xalan")
     runtimeOnly("xerces:xercesImpl")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
 }
diff --git a/splunk/build.gradle.kts b/splunk/build.gradle.kts
index c48294a..74953c7 100644
--- a/splunk/build.gradle.kts
+++ b/splunk/build.gradle.kts
@@ -23,6 +23,6 @@
 
     implementation("net.sf.opencsv:opencsv")
 
-    testImplementation(project(":core", "testClasses"))
+    testImplementation(project(":testkit"))
     testRuntimeOnly("org.slf4j:slf4j-log4j12")
 }
diff --git a/splunk/src/test/java/org/apache/calcite/test/SplunkAdapterTest.java b/splunk/src/test/java/org/apache/calcite/test/SplunkAdapterTest.java
index 73eddf5..3a5a0cd 100644
--- a/splunk/src/test/java/org/apache/calcite/test/SplunkAdapterTest.java
+++ b/splunk/src/test/java/org/apache/calcite/test/SplunkAdapterTest.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.test;
 
 import org.apache.calcite.config.CalciteSystemProperty;
+import org.apache.calcite.test.schemata.foodmart.FoodmartSchema;
 import org.apache.calcite.util.TestUtil;
 
 import com.google.common.collect.ImmutableSet;
@@ -280,7 +281,7 @@
       info.put("url", SPLUNK_URL);
       info.put("user", SPLUNK_USER);
       info.put("password", SPLUNK_PASSWORD);
-      info.put("model", "inline:" + JdbcTest.FOODMART_MODEL);
+      info.put("model", "inline:" + FoodmartSchema.FOODMART_MODEL);
       connection = DriverManager.getConnection("jdbc:splunk:", info);
       statement = connection.createStatement();
       final ResultSet resultSet = statement.executeQuery(sql);
diff --git a/src/main/config/checkstyle/suppressions.xml b/src/main/config/checkstyle/suppressions.xml
index c6ee179..c51a9c3 100644
--- a/src/main/config/checkstyle/suppressions.xml
+++ b/src/main/config/checkstyle/suppressions.xml
@@ -38,6 +38,12 @@
   <!-- Don't complain about field names such as cust_id -->
   <suppress checks=".*Name" files="JdbcExample.java"/>
 
+  <!-- Don't complain about field names such as cust_id -->
+  <suppress checks=".*Name" files="src[/\\]main[/\\]java[/\\]org[/\\]apache[/\\]calcite[/\\]test[/\\]schemata[/\\]"/>
+
+  <!-- Documenting test schema-related packages adds little value -->
+  <suppress checks="JavadocPackage" files="src[/\\]main[/\\]java[/\\]org[/\\]apache[/\\]calcite[/\\]test[/\\]schemata[/\\]"/>
+
   <!-- Don't complain about method names in a class full of UDFs -->
   <suppress checks="MethodName" files="GeoFunctions.java"/>
 
diff --git a/testkit/build.gradle.kts b/testkit/build.gradle.kts
new file mode 100644
index 0000000..e8fbba3
--- /dev/null
+++ b/testkit/build.gradle.kts
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+plugins {
+    kotlin("jvm")
+}
+
+dependencies {
+    api(project(":core"))
+    api("org.checkerframework:checker-qual")
+
+    implementation(platform("org.junit:junit-bom"))
+    implementation(kotlin("stdlib-jdk8"))
+    implementation("net.hydromatic:quidem")
+    implementation("net.hydromatic:foodmart-data-hsqldb")
+    implementation("net.hydromatic:foodmart-queries")
+    implementation("net.hydromatic:scott-data-hsqldb")
+    implementation("org.apache.commons:commons-dbcp2")
+    implementation("org.apache.commons:commons-lang3")
+    implementation("org.apache.commons:commons-pool2")
+    implementation("org.hamcrest:hamcrest")
+    implementation("org.incava:java-diff")
+    implementation("org.junit.jupiter:junit-jupiter")
+
+    testImplementation(kotlin("test"))
+    testImplementation(kotlin("test-junit5"))
+}
diff --git a/core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/sql/parser/SqlParserTest.java
rename to testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/package-info.java b/testkit/src/main/java/org/apache/calcite/sql/parser/package-info.java
new file mode 100644
index 0000000..717d8d6
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes for testing SQL Parser.
+ */
+package org.apache.calcite.sql.parser;
diff --git a/core/src/test/java/org/apache/calcite/sql/test/AbstractSqlTester.java b/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/sql/test/AbstractSqlTester.java
rename to testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTestFactory.java b/testkit/src/main/java/org/apache/calcite/sql/test/SqlTestFactory.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/sql/test/SqlTestFactory.java
rename to testkit/src/main/java/org/apache/calcite/sql/test/SqlTestFactory.java
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTester.java b/testkit/src/main/java/org/apache/calcite/sql/test/SqlTester.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/sql/test/SqlTester.java
rename to testkit/src/main/java/org/apache/calcite/sql/test/SqlTester.java
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlTests.java b/testkit/src/main/java/org/apache/calcite/sql/test/SqlTests.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/sql/test/SqlTests.java
rename to testkit/src/main/java/org/apache/calcite/sql/test/SqlTests.java
diff --git a/core/src/test/java/org/apache/calcite/sql/test/SqlValidatorTester.java b/testkit/src/main/java/org/apache/calcite/sql/test/SqlValidatorTester.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/sql/test/SqlValidatorTester.java
rename to testkit/src/main/java/org/apache/calcite/sql/test/SqlValidatorTester.java
diff --git a/testkit/src/main/java/org/apache/calcite/sql/test/package-info.java b/testkit/src/main/java/org/apache/calcite/sql/test/package-info.java
new file mode 100644
index 0000000..3babd19
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/sql/test/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes for testing SQL.
+ */
+package org.apache.calcite.sql.test;
diff --git a/testkit/src/main/java/org/apache/calcite/test/AbstractModifiableTable.java b/testkit/src/main/java/org/apache/calcite/test/AbstractModifiableTable.java
new file mode 100644
index 0000000..3911d54
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/AbstractModifiableTable.java
@@ -0,0 +1,51 @@
+/*
+ * 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.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.prepare.Prepare;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.TableModify;
+import org.apache.calcite.rel.logical.LogicalTableModify;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.schema.ModifiableTable;
+import org.apache.calcite.schema.impl.AbstractTable;
+
+import java.util.List;
+
+/**
+ * Abstract base class for implementations of {@link ModifiableTable}.
+ */
+public abstract class AbstractModifiableTable
+    extends AbstractTable implements ModifiableTable {
+  protected AbstractModifiableTable(String tableName) {
+  }
+
+  public TableModify toModificationRel(
+      RelOptCluster cluster,
+      RelOptTable table,
+      Prepare.CatalogReader catalogReader,
+      RelNode child,
+      TableModify.Operation operation,
+      List<String> updateColumnList,
+      List<RexNode> sourceExpressionList,
+      boolean flattened) {
+    return LogicalTableModify.create(table, catalogReader, child, operation,
+        updateColumnList, sourceExpressionList, flattened);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/AbstractModifiableView.java b/testkit/src/main/java/org/apache/calcite/test/AbstractModifiableView.java
new file mode 100644
index 0000000..a32f8d1
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/AbstractModifiableView.java
@@ -0,0 +1,29 @@
+/*
+ * 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.schema.ModifiableView;
+import org.apache.calcite.schema.impl.AbstractTable;
+
+/**
+ * Abstract base class for implementations of {@link ModifiableView}.
+ */
+public abstract class AbstractModifiableView
+    extends AbstractTable implements ModifiableView {
+  protected AbstractModifiableView() {
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteAssert.java b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
similarity index 97%
rename from core/src/test/java/org/apache/calcite/test/CalciteAssert.java
rename to testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
index fcc3f66..d1e5999 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteAssert.java
+++ b/testkit/src/main/java/org/apache/calcite/test/CalciteAssert.java
@@ -65,10 +65,19 @@
 import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlConformanceEnum;
 import org.apache.calcite.sql.validate.SqlValidatorException;
+import org.apache.calcite.test.schemata.bookstore.BookstoreSchema;
+import org.apache.calcite.test.schemata.countries.CountriesTableFunction;
+import org.apache.calcite.test.schemata.countries.StatesTableFunction;
+import org.apache.calcite.test.schemata.foodmart.FoodmartSchema;
+import org.apache.calcite.test.schemata.hr.HrSchema;
+import org.apache.calcite.test.schemata.lingual.LingualSchema;
+import org.apache.calcite.test.schemata.orderstream.OrdersHistoryTable;
+import org.apache.calcite.test.schemata.orderstream.OrdersStreamTableFactory;
+import org.apache.calcite.test.schemata.orderstream.ProductsTemporalTable;
+import org.apache.calcite.test.schemata.tpch.TpchSchema;
 import org.apache.calcite.tools.FrameworkConfig;
 import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.tools.RelBuilder;
-import org.apache.calcite.tools.TpchSchema;
 import org.apache.calcite.util.Closer;
 import org.apache.calcite.util.Holder;
 import org.apache.calcite.util.JsonBuilder;
@@ -130,18 +139,17 @@
 import static org.apache.calcite.test.Matchers.isLinux;
 import static org.apache.calcite.util.DateTimeStringUtils.ISO_DATETIME_FORMAT;
 import static org.apache.calcite.util.DateTimeStringUtils.getDateFormatter;
-import static org.apache.calcite.util.Util.toLinux;
 
 import static org.apache.commons.lang3.StringUtils.countMatches;
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 /**
@@ -777,7 +785,7 @@
     switch (schema) {
     case REFLECTIVE_FOODMART:
       return rootSchema.add(schema.schemaName,
-          new ReflectiveSchema(new JdbcTest.FoodmartSchema()));
+          new ReflectiveSchema(new FoodmartSchema()));
     case JDBC_SCOTT:
       cs = DatabaseInstance.HSQLDB.scott;
       dataSource = JdbcSchema.dataSource(cs.url, cs.driver, cs.username,
@@ -812,10 +820,10 @@
       return rootSchema.add(schema.schemaName, new CloneSchema(jdbcScott));
     case SCOTT_WITH_TEMPORAL:
       scott = addSchemaIfNotExists(rootSchema, SchemaSpec.SCOTT);
-      scott.add("products_temporal", new StreamTest.ProductsTemporalTable());
+      scott.add("products_temporal", new ProductsTemporalTable());
       scott.add("orders",
-          new StreamTest.OrdersHistoryTable(
-              StreamTest.OrdersStreamTableFactory.getRowList()));
+          new OrdersHistoryTable(
+              OrdersStreamTableFactory.getRowList()));
       return scott;
 
     case TPCH:
@@ -860,18 +868,18 @@
       return s;
     case HR:
       return rootSchema.add(schema.schemaName,
-          new ReflectiveSchema(new JdbcTest.HrSchema()));
+          new ReflectiveSchema(new HrSchema()));
     case LINGUAL:
       return rootSchema.add(schema.schemaName,
-          new ReflectiveSchema(new JdbcTest.LingualSchema()));
+          new ReflectiveSchema(new LingualSchema()));
     case BLANK:
       return rootSchema.add(schema.schemaName, new AbstractSchema());
     case ORINOCO:
       final SchemaPlus orinoco =
           rootSchema.add(schema.schemaName, new AbstractSchema());
       orinoco.add("ORDERS",
-          new StreamTest.OrdersHistoryTable(
-              StreamTest.OrdersStreamTableFactory.getRowList()));
+          new OrdersHistoryTable(
+              OrdersStreamTableFactory.getRowList()));
       return orinoco;
     case POST:
       final SchemaPlus post =
@@ -1774,16 +1782,12 @@
         final String planSql;
         if (planSqls.size() == 1) {
           planSql = planSqls.get(0);
-          assertThat(planSql, is(expected.sql));
+          assertThat("Execution plan for sql " + sql, planSql, is(expected.sql));
         } else {
-          assertThat("contains " + planSqls + " expected " + expected,
-              planSqls.contains(expected.sql), is(true));
+          assertThat("Execution plan for sql " + sql, planSqls, hasItem(expected.sql));
         }
       } else {
-        final String message =
-            "Plan [" + plan + "] contains [" + expected.java + "]";
-        final String actualJava = toLinux(plan);
-        assertTrue(actualJava.contains(expected.java), message);
+        assertThat("Execution plan for sql " + sql, plan, containsStringLinux(expected.java));
       }
       return this;
     }
@@ -1937,8 +1941,8 @@
 
     /**
      * 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}.
+     * {@link HrSchema} and
+     * {@link FoodmartSchema}.
      */
     REGULAR,
 
diff --git a/core/src/test/java/org/apache/calcite/test/ConnectionSpec.java b/testkit/src/main/java/org/apache/calcite/test/ConnectionSpec.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/ConnectionSpec.java
rename to testkit/src/main/java/org/apache/calcite/test/ConnectionSpec.java
diff --git a/core/src/test/java/org/apache/calcite/test/DiffTestCase.java b/testkit/src/main/java/org/apache/calcite/test/DiffTestCase.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/DiffTestCase.java
rename to testkit/src/main/java/org/apache/calcite/test/DiffTestCase.java
diff --git a/core/src/test/java/org/apache/calcite/test/Matchers.java b/testkit/src/main/java/org/apache/calcite/test/Matchers.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/Matchers.java
rename to testkit/src/main/java/org/apache/calcite/test/Matchers.java
diff --git a/core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java b/testkit/src/main/java/org/apache/calcite/test/MockSqlOperatorTable.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/MockSqlOperatorTable.java
rename to testkit/src/main/java/org/apache/calcite/test/MockSqlOperatorTable.java
diff --git a/core/src/test/java/org/apache/calcite/test/QuidemTest.java b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
similarity index 95%
rename from core/src/test/java/org/apache/calcite/test/QuidemTest.java
rename to testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
index cb44d57..2932f21 100644
--- a/core/src/test/java/org/apache/calcite/test/QuidemTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
@@ -28,6 +28,7 @@
 import org.apache.calcite.schema.impl.AbstractSchema;
 import org.apache.calcite.schema.impl.AbstractTable;
 import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.test.schemata.catchall.CatchallSchema;
 import org.apache.calcite.util.Bug;
 import org.apache.calcite.util.Closer;
 import org.apache.calcite.util.Sources;
@@ -101,7 +102,7 @@
 
   protected static Collection<Object[]> data(String first) {
     // inUrl = "file:/home/fred/calcite/core/target/test-classes/sql/agg.iq"
-    final URL inUrl = JdbcTest.class.getResource("/" + n2u(first));
+    final URL inUrl = QuidemTest.class.getResource("/" + n2u(first));
     final File firstFile = Sources.of(inUrl).file();
     final int commonPrefixLength = firstFile.getAbsolutePath().length() - first.length();
     final File dir = firstFile.getParentFile();
@@ -124,7 +125,7 @@
     } else {
       // e.g. path = "sql/outer.iq"
       // inUrl = "file:/home/fred/calcite/core/target/test-classes/sql/outer.iq"
-      final URL inUrl = JdbcTest.class.getResource("/" + n2u(path));
+      final URL inUrl = QuidemTest.class.getResource("/" + n2u(path));
       inFile = Sources.of(inUrl).file();
       outFile = new File(inFile.getAbsoluteFile().getParent(), u2n("surefire/") + path);
     }
@@ -272,18 +273,12 @@
         return CalciteAssert.that()
             .withSchema("s",
                 new ReflectiveSchema(
-                    new ReflectiveSchemaTest.CatchallSchema()))
+                    new CatchallSchema()))
             .connect();
       case "orinoco":
         return CalciteAssert.that()
             .with(CalciteAssert.SchemaSpec.ORINOCO)
             .connect();
-      case "blank":
-        return CalciteAssert.that()
-            .with(CalciteConnectionProperty.PARSER_FACTORY,
-                ExtensionDdlExecutor.class.getName() + "#PARSER_FACTORY")
-            .with(CalciteAssert.SchemaSpec.BLANK)
-            .connect();
       case "seq":
         final Connection connection = CalciteAssert.that()
             .withSchema("s", new AbstractSchema())
diff --git a/testkit/src/main/java/org/apache/calcite/test/RexImplicationCheckerFixtures.java b/testkit/src/main/java/org/apache/calcite/test/RexImplicationCheckerFixtures.java
new file mode 100644
index 0000000..26db5c6
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/RexImplicationCheckerFixtures.java
@@ -0,0 +1,246 @@
+/*
+ * 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.DataContexts;
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.plan.RelOptPredicateList;
+import org.apache.calcite.plan.RexImplicationChecker;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexExecutorImpl;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexSimplify;
+import org.apache.calcite.sql.SqlCollation;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.tools.Frameworks;
+import org.apache.calcite.util.DateString;
+import org.apache.calcite.util.NlsString;
+import org.apache.calcite.util.TimeString;
+import org.apache.calcite.util.TimestampString;
+
+import java.math.BigDecimal;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Fixtures for verifying {@link RexImplicationChecker}.
+ */
+public interface RexImplicationCheckerFixtures {
+  /** Contains all the nourishment a test case could possibly need.
+   *
+   * <p>We put the data in here, rather than as fields in the test case, so that
+   * the data can be garbage-collected as soon as the test has executed.
+   */
+  @SuppressWarnings("WeakerAccess")
+  class Fixture {
+    public final RelDataTypeFactory typeFactory;
+    public final RexBuilder rexBuilder;
+    public final RelDataType boolRelDataType;
+    public final RelDataType intRelDataType;
+    public final RelDataType decRelDataType;
+    public final RelDataType longRelDataType;
+    public final RelDataType shortDataType;
+    public final RelDataType byteDataType;
+    public final RelDataType floatDataType;
+    public final RelDataType charDataType;
+    public final RelDataType dateDataType;
+    public final RelDataType timestampDataType;
+    public final RelDataType timeDataType;
+    public final RelDataType stringDataType;
+
+    public final RexNode bl; // a field of Java type "Boolean"
+    public final RexNode i; // a field of Java type "Integer"
+    public final RexNode dec; // a field of Java type "Double"
+    public final RexNode lg; // a field of Java type "Long"
+    public final RexNode sh; // a  field of Java type "Short"
+    public final RexNode by; // a field of Java type "Byte"
+    public final RexNode fl; // a field of Java type "Float" (not a SQL FLOAT)
+    public final RexNode d; // a field of Java type "Date"
+    public final RexNode ch; // a field of Java type "Character"
+    public final RexNode ts; // a field of Java type "Timestamp"
+    public final RexNode t; // a field of Java type "Time"
+    public final RexNode str; // a field of Java type "String"
+
+    public final RexImplicationChecker checker;
+    public final RelDataType rowType;
+    public final RexExecutorImpl executor;
+    public final RexSimplify simplify;
+
+    public Fixture() {
+      typeFactory = new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
+      rexBuilder = new RexBuilder(typeFactory);
+      boolRelDataType = typeFactory.createJavaType(Boolean.class);
+      intRelDataType = typeFactory.createJavaType(Integer.class);
+      decRelDataType = typeFactory.createJavaType(Double.class);
+      longRelDataType = typeFactory.createJavaType(Long.class);
+      shortDataType = typeFactory.createJavaType(Short.class);
+      byteDataType = typeFactory.createJavaType(Byte.class);
+      floatDataType = typeFactory.createJavaType(Float.class);
+      charDataType = typeFactory.createJavaType(Character.class);
+      dateDataType = typeFactory.createJavaType(Date.class);
+      timestampDataType = typeFactory.createJavaType(Timestamp.class);
+      timeDataType = typeFactory.createJavaType(Time.class);
+      stringDataType = typeFactory.createJavaType(String.class);
+
+      bl = ref(0, this.boolRelDataType);
+      i = ref(1, intRelDataType);
+      dec = ref(2, decRelDataType);
+      lg = ref(3, longRelDataType);
+      sh = ref(4, shortDataType);
+      by = ref(5, byteDataType);
+      fl = ref(6, floatDataType);
+      ch = ref(7, charDataType);
+      d = ref(8, dateDataType);
+      ts = ref(9, timestampDataType);
+      t = ref(10, timeDataType);
+      str = ref(11, stringDataType);
+
+      rowType = typeFactory.builder()
+          .add("bool", this.boolRelDataType)
+          .add("int", intRelDataType)
+          .add("dec", decRelDataType)
+          .add("long", longRelDataType)
+          .add("short", shortDataType)
+          .add("byte", byteDataType)
+          .add("float", floatDataType)
+          .add("char", charDataType)
+          .add("date", dateDataType)
+          .add("timestamp", timestampDataType)
+          .add("time", timeDataType)
+          .add("string", stringDataType)
+          .build();
+
+      executor = Frameworks.withPrepare(
+          (cluster, relOptSchema, rootSchema, statement) ->
+              new RexExecutorImpl(
+                  DataContexts.of(statement.getConnection(), rootSchema)));
+      simplify =
+          new RexSimplify(rexBuilder, RelOptPredicateList.EMPTY, executor)
+              .withParanoid(true);
+      checker = new RexImplicationChecker(rexBuilder, executor, rowType);
+    }
+
+    public RexInputRef ref(int i, RelDataType type) {
+      return new RexInputRef(i,
+          typeFactory.createTypeWithNullability(type, true));
+    }
+
+    public RexLiteral literal(int i) {
+      return rexBuilder.makeExactLiteral(new BigDecimal(i));
+    }
+
+    public RexNode gt(RexNode node1, RexNode node2) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN, node1, node2);
+    }
+
+    public RexNode ge(RexNode node1, RexNode node2) {
+      return rexBuilder.makeCall(
+          SqlStdOperatorTable.GREATER_THAN_OR_EQUAL, node1, node2);
+    }
+
+    public RexNode eq(RexNode node1, RexNode node2) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, node1, node2);
+    }
+
+    public RexNode ne(RexNode node1, RexNode node2) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.NOT_EQUALS, node1, node2);
+    }
+
+    public RexNode lt(RexNode node1, RexNode node2) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN, node1, node2);
+    }
+
+    public RexNode le(RexNode node1, RexNode node2) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN_OR_EQUAL, node1,
+          node2);
+    }
+
+    public RexNode notNull(RexNode node1) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, node1);
+    }
+
+    public RexNode isNull(RexNode node2) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, node2);
+    }
+
+    public RexNode and(RexNode... nodes) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.AND, nodes);
+    }
+
+    public RexNode or(RexNode... nodes) {
+      return rexBuilder.makeCall(SqlStdOperatorTable.OR, nodes);
+    }
+
+    public RexNode longLiteral(long value) {
+      return rexBuilder.makeLiteral(value, longRelDataType, true);
+    }
+
+    public RexNode shortLiteral(short value) {
+      return rexBuilder.makeLiteral(value, shortDataType, true);
+    }
+
+    public RexLiteral floatLiteral(double value) {
+      return rexBuilder.makeApproxLiteral(new BigDecimal(value));
+    }
+
+    public RexLiteral charLiteral(String z) {
+      return rexBuilder.makeCharLiteral(
+          new NlsString(z, null, SqlCollation.COERCIBLE));
+    }
+
+    public RexNode dateLiteral(DateString d) {
+      return rexBuilder.makeDateLiteral(d);
+    }
+
+    public RexNode timestampLiteral(TimestampString ts) {
+      return rexBuilder.makeTimestampLiteral(ts,
+          timestampDataType.getPrecision());
+    }
+
+    public RexNode timestampLocalTzLiteral(TimestampString ts) {
+      return rexBuilder.makeTimestampWithLocalTimeZoneLiteral(ts,
+          timestampDataType.getPrecision());
+    }
+
+    public RexNode timeLiteral(TimeString t) {
+      return rexBuilder.makeTimeLiteral(t, timeDataType.getPrecision());
+    }
+
+    public RexNode cast(RelDataType type, RexNode exp) {
+      return rexBuilder.makeCast(type, exp, true);
+    }
+
+    void checkImplies(RexNode node1, RexNode node2) {
+      assertTrue(checker.implies(node1, node2),
+          () -> node1 + " does not imply " + node2 + " when it should");
+    }
+
+    void checkNotImplies(RexNode node1, RexNode node2) {
+      assertFalse(checker.implies(node1, node2),
+          () -> node1 + " does implies " + node2 + " when it should not");
+    }
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTestCase.java b/testkit/src/main/java/org/apache/calcite/test/SqlValidatorTestCase.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/SqlValidatorTestCase.java
rename to testkit/src/main/java/org/apache/calcite/test/SqlValidatorTestCase.java
diff --git a/core/src/test/java/org/apache/calcite/test/Unsafe.java b/testkit/src/main/java/org/apache/calcite/test/Unsafe.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/Unsafe.java
rename to testkit/src/main/java/org/apache/calcite/test/Unsafe.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/CompoundNameColumn.java b/testkit/src/main/java/org/apache/calcite/test/catalog/CompoundNameColumn.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/CompoundNameColumn.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/CompoundNameColumn.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/CompoundNameColumnResolver.java b/testkit/src/main/java/org/apache/calcite/test/catalog/CompoundNameColumnResolver.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/CompoundNameColumnResolver.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/CompoundNameColumnResolver.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/CountingFactory.java b/testkit/src/main/java/org/apache/calcite/test/catalog/CountingFactory.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/CountingFactory.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/CountingFactory.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/EmpInitializerExpressionFactory.java b/testkit/src/main/java/org/apache/calcite/test/catalog/EmpInitializerExpressionFactory.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/EmpInitializerExpressionFactory.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/EmpInitializerExpressionFactory.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/Fixture.java b/testkit/src/main/java/org/apache/calcite/test/catalog/Fixture.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/Fixture.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/Fixture.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReader.java
similarity index 92%
rename from core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReader.java
index 553b1c3..d387c2a 100644
--- a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReader.java
+++ b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReader.java
@@ -42,12 +42,9 @@
 import org.apache.calcite.rel.logical.LogicalTableScan;
 import org.apache.calcite.rel.type.DynamicRecordTypeImpl;
 import org.apache.calcite.rel.type.RelDataType;
-import org.apache.calcite.rel.type.RelDataTypeComparability;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
-import org.apache.calcite.rel.type.RelDataTypeFamily;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rel.type.RelDataTypeImpl;
-import org.apache.calcite.rel.type.RelDataTypePrecedenceList;
 import org.apache.calcite.rel.type.RelDataTypeSystem;
 import org.apache.calcite.rel.type.RelProtoDataType;
 import org.apache.calcite.rel.type.StructKind;
@@ -71,12 +68,8 @@
 import org.apache.calcite.schema.impl.ViewTableMacro;
 import org.apache.calcite.sql.SqlAccessType;
 import org.apache.calcite.sql.SqlCall;
-import org.apache.calcite.sql.SqlCollation;
-import org.apache.calcite.sql.SqlIdentifier;
-import org.apache.calcite.sql.SqlIntervalQualifier;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlNode;
-import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.validate.SqlModality;
 import org.apache.calcite.sql.validate.SqlMonotonicity;
 import org.apache.calcite.sql.validate.SqlNameMatcher;
@@ -85,7 +78,8 @@
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.sql2rel.InitializerExpressionFactory;
 import org.apache.calcite.sql2rel.NullInitializerExpressionFactory;
-import org.apache.calcite.test.JdbcTest;
+import org.apache.calcite.test.AbstractModifiableTable;
+import org.apache.calcite.test.AbstractModifiableView;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.Pair;
@@ -97,7 +91,6 @@
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.lang.reflect.Type;
-import java.nio.charset.Charset;
 import java.util.AbstractList;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -379,7 +372,7 @@
     }
 
     /** Implementation of AbstractModifiableTable. */
-    private class ModifiableTable extends JdbcTest.AbstractModifiableTable
+    private class ModifiableTable extends AbstractModifiableTable
         implements ExtensibleTable, Wrapper {
       protected ModifiableTable(String tableName) {
         super(tableName);
@@ -803,7 +796,7 @@
     }
 
     /** Implementation of AbstractModifiableView. */
-    private class ModifiableView extends JdbcTest.AbstractModifiableView
+    private class ModifiableView extends AbstractModifiableView
         implements Wrapper {
       @Override public Table getTable() {
         return fromTable.unwrap(Table.class);
@@ -950,107 +943,6 @@
     }
   }
 
-  /** Struct type based on another struct type. */
-  private static class DelegateStructType implements RelDataType {
-    private RelDataType delegate;
-    private StructKind structKind;
-
-    DelegateStructType(RelDataType delegate, StructKind structKind) {
-      assert delegate.isStruct();
-      this.delegate = delegate;
-      this.structKind = structKind;
-    }
-
-    public boolean isStruct() {
-      return delegate.isStruct();
-    }
-
-    public boolean isDynamicStruct() {
-      return delegate.isDynamicStruct();
-    }
-
-    public List<RelDataTypeField> getFieldList() {
-      return delegate.getFieldList();
-    }
-
-    public List<String> getFieldNames() {
-      return delegate.getFieldNames();
-    }
-
-    public int getFieldCount() {
-      return delegate.getFieldCount();
-    }
-
-    public StructKind getStructKind() {
-      return structKind;
-    }
-
-    public RelDataTypeField getField(String fieldName, boolean caseSensitive,
-        boolean elideRecord) {
-      return delegate.getField(fieldName, caseSensitive, elideRecord);
-    }
-
-    public boolean isNullable() {
-      return delegate.isNullable();
-    }
-
-    public RelDataType getComponentType() {
-      return delegate.getComponentType();
-    }
-
-    public RelDataType getKeyType() {
-      return delegate.getKeyType();
-    }
-
-    public RelDataType getValueType() {
-      return delegate.getValueType();
-    }
-
-    public Charset getCharset() {
-      return delegate.getCharset();
-    }
-
-    public SqlCollation getCollation() {
-      return delegate.getCollation();
-    }
-
-    public SqlIntervalQualifier getIntervalQualifier() {
-      return delegate.getIntervalQualifier();
-    }
-
-    public int getPrecision() {
-      return delegate.getPrecision();
-    }
-
-    public int getScale() {
-      return delegate.getScale();
-    }
-
-    public SqlTypeName getSqlTypeName() {
-      return delegate.getSqlTypeName();
-    }
-
-    public SqlIdentifier getSqlIdentifier() {
-      return delegate.getSqlIdentifier();
-    }
-
-    public String getFullTypeString() {
-      return delegate.getFullTypeString();
-    }
-
-    public RelDataTypeFamily getFamily() {
-      return delegate.getFamily();
-    }
-
-    public RelDataTypePrecedenceList getPrecedenceList() {
-      return delegate.getPrecedenceList();
-    }
-
-    public RelDataTypeComparability getComparability() {
-      return delegate.getComparability();
-    }
-  }
-
   /** Wrapper around a {@link MockTable}, giving it a {@link Table} interface.
    * You can get the {@code MockTable} by calling {@link #unwrap(Class)}. */
   private static class WrapperTable implements Table, Wrapper {
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderDynamic.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderExtended.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java b/testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/MockCatalogReaderSimple.java
diff --git a/core/src/test/java/org/apache/calcite/test/catalog/VirtualColumnsExpressionFactory.java b/testkit/src/main/java/org/apache/calcite/test/catalog/VirtualColumnsExpressionFactory.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/test/catalog/VirtualColumnsExpressionFactory.java
rename to testkit/src/main/java/org/apache/calcite/test/catalog/VirtualColumnsExpressionFactory.java
diff --git a/testkit/src/main/java/org/apache/calcite/test/catalog/package-info.java b/testkit/src/main/java/org/apache/calcite/test/catalog/package-info.java
new file mode 100644
index 0000000..d8131ac
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/catalog/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes for testing Catalog.
+ */
+package org.apache.calcite.test.catalog;
diff --git a/testkit/src/main/java/org/apache/calcite/test/package-info.java b/testkit/src/main/java/org/apache/calcite/test/package-info.java
new file mode 100644
index 0000000..2c6b382
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes for testing Calcite.
+ */
+package org.apache.calcite.test;
diff --git a/core/src/test/java/org/apache/calcite/test/BookstoreSchema.java b/testkit/src/main/java/org/apache/calcite/test/schemata/bookstore/BookstoreSchema.java
similarity index 98%
rename from core/src/test/java/org/apache/calcite/test/BookstoreSchema.java
rename to testkit/src/main/java/org/apache/calcite/test/schemata/bookstore/BookstoreSchema.java
index a000b56..8c678e9 100644
--- a/core/src/test/java/org/apache/calcite/test/BookstoreSchema.java
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/bookstore/BookstoreSchema.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.test;
+package org.apache.calcite.test.schemata.bookstore;
 
 import java.math.BigDecimal;
 import java.util.Arrays;
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/catchall/CatchallSchema.java b/testkit/src/main/java/org/apache/calcite/test/schemata/catchall/CatchallSchema.java
new file mode 100644
index 0000000..43eeef8
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/catchall/CatchallSchema.java
@@ -0,0 +1,216 @@
+/*
+ * 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.schemata.catchall;
+
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.linq4j.tree.Primitive;
+import org.apache.calcite.test.schemata.hr.Employee;
+import org.apache.calcite.test.schemata.hr.HrSchema;
+
+import java.lang.reflect.Field;
+import java.math.BigDecimal;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Object whose fields are relations. Called "catch-all" because it's OK
+ * if tests add new fields.
+ */
+public class CatchallSchema {
+  public final Enumerable<Employee> enumerable =
+      Linq4j.asEnumerable(
+          Arrays.asList(new HrSchema().emps));
+
+  public final List<Employee> list =
+      Arrays.asList(new HrSchema().emps);
+
+  public final BitSet bitSet = new BitSet(1);
+
+  public final EveryType[] everyTypes = {
+      new EveryType(
+          false, (byte) 0, (char) 0, (short) 0, 0, 0L, 0F, 0D,
+          false, (byte) 0, (char) 0, (short) 0, 0, 0L, 0F, 0D,
+          new java.sql.Date(0), new Time(0), new Timestamp(0),
+          new Date(0), "1", BigDecimal.ZERO),
+      new EveryType(
+          true, Byte.MAX_VALUE, Character.MAX_VALUE, Short.MAX_VALUE,
+          Integer.MAX_VALUE, Long.MAX_VALUE, Float.MAX_VALUE,
+          Double.MAX_VALUE,
+          null, null, null, null, null, null, null, null,
+          null, null, null, null, null, null),
+  };
+
+  public final AllPrivate[] allPrivates =
+      {new AllPrivate()};
+
+  public final BadType[] badTypes = {new BadType()};
+
+  public final Employee[] prefixEmps = {
+      new Employee(1, 10, "A", 0f, null),
+      new Employee(2, 10, "Ab", 0f, null),
+      new Employee(3, 10, "Abc", 0f, null),
+      new Employee(4, 10, "Abd", 0f, null),
+  };
+
+  public final Integer[] primesBoxed = {1, 3, 5};
+
+  public final int[] primes = {1, 3, 5};
+
+  public final IntHolder[] primesCustomBoxed =
+      {new IntHolder(1), new IntHolder(3),
+          new IntHolder(5)};
+
+  public final IntAndString[] nullables = {
+      new IntAndString(1, "A"), new IntAndString(2,
+      "B"), new IntAndString(2, "C"),
+      new IntAndString(3, null)};
+
+  public final IntAndString[] bools = {
+      new IntAndString(1, "T"), new IntAndString(2,
+      "F"), new IntAndString(3, null)};
+
+  private static boolean isNumeric(Class type) {
+    switch (Primitive.flavor(type)) {
+    case BOX:
+      return Primitive.ofBox(type).isNumeric();
+    case PRIMITIVE:
+      return Primitive.of(type).isNumeric();
+    default:
+      return Number.class.isAssignableFrom(type); // e.g. BigDecimal
+    }
+  }
+
+  /** Record that has a field of every interesting type. */
+  public static class EveryType {
+    public final boolean primitiveBoolean;
+    public final byte primitiveByte;
+    public final char primitiveChar;
+    public final short primitiveShort;
+    public final int primitiveInt;
+    public final long primitiveLong;
+    public final float primitiveFloat;
+    public final double primitiveDouble;
+    public final Boolean wrapperBoolean;
+    public final Byte wrapperByte;
+    public final Character wrapperCharacter;
+    public final Short wrapperShort;
+    public final Integer wrapperInteger;
+    public final Long wrapperLong;
+    public final Float wrapperFloat;
+    public final Double wrapperDouble;
+    public final java.sql.Date sqlDate;
+    public final Time sqlTime;
+    public final Timestamp sqlTimestamp;
+    public final Date utilDate;
+    public final String string;
+    public final BigDecimal bigDecimal;
+
+    public EveryType(
+        boolean primitiveBoolean,
+        byte primitiveByte,
+        char primitiveChar,
+        short primitiveShort,
+        int primitiveInt,
+        long primitiveLong,
+        float primitiveFloat,
+        double primitiveDouble,
+        Boolean wrapperBoolean,
+        Byte wrapperByte,
+        Character wrapperCharacter,
+        Short wrapperShort,
+        Integer wrapperInteger,
+        Long wrapperLong,
+        Float wrapperFloat,
+        Double wrapperDouble,
+        java.sql.Date sqlDate,
+        Time sqlTime,
+        Timestamp sqlTimestamp,
+        Date utilDate,
+        String string,
+        BigDecimal bigDecimal) {
+      this.primitiveBoolean = primitiveBoolean;
+      this.primitiveByte = primitiveByte;
+      this.primitiveChar = primitiveChar;
+      this.primitiveShort = primitiveShort;
+      this.primitiveInt = primitiveInt;
+      this.primitiveLong = primitiveLong;
+      this.primitiveFloat = primitiveFloat;
+      this.primitiveDouble = primitiveDouble;
+      this.wrapperBoolean = wrapperBoolean;
+      this.wrapperByte = wrapperByte;
+      this.wrapperCharacter = wrapperCharacter;
+      this.wrapperShort = wrapperShort;
+      this.wrapperInteger = wrapperInteger;
+      this.wrapperLong = wrapperLong;
+      this.wrapperFloat = wrapperFloat;
+      this.wrapperDouble = wrapperDouble;
+      this.sqlDate = sqlDate;
+      this.sqlTime = sqlTime;
+      this.sqlTimestamp = sqlTimestamp;
+      this.utilDate = utilDate;
+      this.string = string;
+      this.bigDecimal = bigDecimal;
+    }
+
+    public static Enumerable<Field> fields() {
+      return Linq4j.asEnumerable(EveryType.class.getFields());
+    }
+
+    public static Enumerable<Field> numericFields() {
+      return fields()
+          .where(v1 -> isNumeric(v1.getType()));
+    }
+  }
+
+  /** All field are private, therefore the resulting record has no fields. */
+  public static class AllPrivate {
+    private final int x = 0;
+  }
+
+  /** Table that has a field that cannot be recognized as a SQL type. */
+  public static class BadType {
+    public final int integer = 0;
+    public final BitSet bitSet = new BitSet(0);
+  }
+
+  /** Table that has integer and string fields. */
+  public static class IntAndString {
+    public final int id;
+    public final String value;
+
+    public IntAndString(int id, String value) {
+      this.id = id;
+      this.value = value;
+    }
+  }
+
+  /**
+   * Custom java class that holds just a single field.
+   */
+  public static class IntHolder {
+    public final int value;
+
+    public IntHolder(int value) {
+      this.value = value;
+    }
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/CountriesTableFunction.java b/testkit/src/main/java/org/apache/calcite/test/schemata/countries/CountriesTableFunction.java
similarity index 99%
rename from core/src/test/java/org/apache/calcite/test/CountriesTableFunction.java
rename to testkit/src/main/java/org/apache/calcite/test/schemata/countries/CountriesTableFunction.java
index dab24ef..73143b9 100644
--- a/core/src/test/java/org/apache/calcite/test/CountriesTableFunction.java
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/countries/CountriesTableFunction.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.test;
+package org.apache.calcite.test.schemata.countries;
 
 import org.apache.calcite.DataContext;
 import org.apache.calcite.config.CalciteConnectionConfig;
diff --git a/core/src/test/java/org/apache/calcite/test/StatesTableFunction.java b/testkit/src/main/java/org/apache/calcite/test/schemata/countries/StatesTableFunction.java
similarity index 98%
rename from core/src/test/java/org/apache/calcite/test/StatesTableFunction.java
rename to testkit/src/main/java/org/apache/calcite/test/schemata/countries/StatesTableFunction.java
index 3d40787..c2e6803 100644
--- a/core/src/test/java/org/apache/calcite/test/StatesTableFunction.java
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/countries/StatesTableFunction.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.test;
+package org.apache.calcite.test.schemata.countries;
 
 import org.apache.calcite.DataContext;
 import org.apache.calcite.config.CalciteConnectionConfig;
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/foodmart/FoodmartSchema.java b/testkit/src/main/java/org/apache/calcite/test/schemata/foodmart/FoodmartSchema.java
new file mode 100644
index 0000000..d5bb005
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/foodmart/FoodmartSchema.java
@@ -0,0 +1,77 @@
+/*
+ * 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.schemata.foodmart;
+
+import org.apache.calcite.test.CalciteAssert;
+
+import java.util.Objects;
+
+/**
+ * Foodmart schema.
+ */
+public class FoodmartSchema {
+  public static final String FOODMART_SCHEMA = "     {\n"
+      + "       type: 'jdbc',\n"
+      + "       name: 'foodmart',\n"
+      + "       jdbcDriver: " + q(CalciteAssert.DB.foodmart.driver) + ",\n"
+      + "       jdbcUser: " + q(CalciteAssert.DB.foodmart.username) + ",\n"
+      + "       jdbcPassword: " + q(CalciteAssert.DB.foodmart.password) + ",\n"
+      + "       jdbcUrl: " + q(CalciteAssert.DB.foodmart.url) + ",\n"
+      + "       jdbcCatalog: " + q(CalciteAssert.DB.foodmart.catalog) + ",\n"
+      + "       jdbcSchema: " + q(CalciteAssert.DB.foodmart.schema) + "\n"
+      + "     }\n";
+  public static final String FOODMART_MODEL = "{\n"
+      + "  version: '1.0',\n"
+      + "  defaultSchema: 'foodmart',\n"
+      + "   schemas: [\n"
+      + FOODMART_SCHEMA
+      + "   ]\n"
+      + "}";
+
+  private static String q(String s) {
+    return s == null ? "null" : "'" + s + "'";
+  }
+
+  public final SalesFact[] sales_fact_1997 = {
+      new SalesFact(100, 10),
+      new SalesFact(150, 20),
+  };
+
+  /**
+   * Sales fact model.
+   */
+  public static class SalesFact {
+    public final int cust_id;
+    public final int prod_id;
+
+    public SalesFact(int cust_id, int prod_id) {
+      this.cust_id = cust_id;
+      this.prod_id = prod_id;
+    }
+
+    @Override public boolean equals(Object obj) {
+      return obj == this
+          || obj instanceof SalesFact
+          && cust_id == ((SalesFact) obj).cust_id
+          && prod_id == ((SalesFact) obj).prod_id;
+    }
+
+    @Override public int hashCode() {
+      return Objects.hash(cust_id, prod_id);
+    }
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Department.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Department.java
new file mode 100644
index 0000000..47274a3
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Department.java
@@ -0,0 +1,55 @@
+/*
+ * 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.schemata.hr;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Department model.
+ */
+public class Department {
+  public final int deptno;
+  public final String name;
+
+  @org.apache.calcite.adapter.java.Array(component = Employee.class)
+  public final List<Employee> employees;
+  public final Location location;
+
+  public Department(int deptno, String name, List<Employee> employees,
+      Location location) {
+    this.deptno = deptno;
+    this.name = name;
+    this.employees = employees;
+    this.location = location;
+  }
+
+  @Override public String toString() {
+    return "Department [deptno: " + deptno + ", name: " + name
+        + ", employees: " + employees + ", location: " + location + "]";
+  }
+
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof Department
+        && deptno == ((Department) obj).deptno;
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(deptno);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/DepartmentPlus.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/DepartmentPlus.java
new file mode 100644
index 0000000..f4120c2
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/DepartmentPlus.java
@@ -0,0 +1,33 @@
+/*
+ * 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.schemata.hr;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+/**
+ * Department with inception date model.
+ */
+public class DepartmentPlus extends Department {
+  public final Timestamp inceptionDate;
+
+  public DepartmentPlus(int deptno, String name, List<Employee> employees,
+                        Location location, Timestamp inceptionDate) {
+    super(deptno, name, employees, location);
+    this.inceptionDate = inceptionDate;
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Dependent.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Dependent.java
new file mode 100644
index 0000000..d8a04fd
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Dependent.java
@@ -0,0 +1,47 @@
+/*
+ * 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.schemata.hr;
+
+import java.util.Objects;
+
+/**
+ * Employee dependents model.
+ */
+public class Dependent {
+  public final int empid;
+  public final String name;
+
+  public Dependent(int empid, String name) {
+    this.empid = empid;
+    this.name = name;
+  }
+
+  @Override public String toString() {
+    return "Dependent [empid: " + empid + ", name: " + name + "]";
+  }
+
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof Dependent
+        && empid == ((Dependent) obj).empid
+        && Objects.equals(name, ((Dependent) obj).name);
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(empid, name);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Employee.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Employee.java
new file mode 100644
index 0000000..1cded57
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Employee.java
@@ -0,0 +1,54 @@
+/*
+ * 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.schemata.hr;
+
+import java.util.Objects;
+
+/**
+ * Employee model.
+ */
+public class Employee {
+  public final int empid;
+  public final int deptno;
+  public final String name;
+  public final float salary;
+  public final Integer commission;
+
+  public Employee(int empid, int deptno, String name, float salary,
+      Integer commission) {
+    this.empid = empid;
+    this.deptno = deptno;
+    this.name = name;
+    this.salary = salary;
+    this.commission = commission;
+  }
+
+  @Override public String toString() {
+    return "Employee [empid: " + empid + ", deptno: " + deptno
+        + ", name: " + name + "]";
+  }
+
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof Employee
+        && empid == ((Employee) obj).empid;
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(empid);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Event.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Event.java
new file mode 100644
index 0000000..67beff0
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Event.java
@@ -0,0 +1,47 @@
+/*
+ * 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.schemata.hr;
+
+import java.sql.Timestamp;
+import java.util.Objects;
+
+/**
+ * Event.
+ */
+public class Event {
+  public final int eventid;
+  public final Timestamp ts;
+
+  public Event(int eventid, Timestamp ts) {
+    this.eventid = eventid;
+    this.ts = ts;
+  }
+
+  @Override public String toString() {
+    return "Event [eventid: " + eventid + ", ts: " + ts + "]";
+  }
+
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof Event
+        && eventid == ((Event) obj).eventid;
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(eventid);
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/HierarchySchema.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HierarchySchema.java
similarity index 82%
rename from core/src/test/java/org/apache/calcite/test/HierarchySchema.java
rename to testkit/src/main/java/org/apache/calcite/test/schemata/hr/HierarchySchema.java
index 3721f02..f6c3eeb 100644
--- a/core/src/test/java/org/apache/calcite/test/HierarchySchema.java
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HierarchySchema.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.test;
+package org.apache.calcite.test.schemata.hr;
 
 import java.util.Arrays;
 import java.util.Objects;
@@ -31,20 +31,20 @@
     return "HierarchySchema";
   }
 
-  public final JdbcTest.Employee[] emps = {
-      new JdbcTest.Employee(1, 10, "Emp1", 10000, 1000),
-      new JdbcTest.Employee(2, 10, "Emp2", 8000, 500),
-      new JdbcTest.Employee(3, 10, "Emp3", 7000, null),
-      new JdbcTest.Employee(4, 10, "Emp4", 8000, 500),
-      new JdbcTest.Employee(5, 10, "Emp5", 7000, null),
+  public final Employee[] emps = {
+      new Employee(1, 10, "Emp1", 10000, 1000),
+      new Employee(2, 10, "Emp2", 8000, 500),
+      new Employee(3, 10, "Emp3", 7000, null),
+      new Employee(4, 10, "Emp4", 8000, 500),
+      new Employee(5, 10, "Emp5", 7000, null),
   };
 
-  public final JdbcTest.Department[] depts = {
-      new JdbcTest.Department(
+  public final Department[] depts = {
+      new Department(
           10,
           "Dept",
           Arrays.asList(emps[0], emps[1], emps[2], emps[3], emps[4]),
-          new JdbcTest.Location(-122, 38)),
+          new Location(-122, 38)),
   };
 
   //      Emp1
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HrSchema.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HrSchema.java
new file mode 100644
index 0000000..82200c4
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HrSchema.java
@@ -0,0 +1,90 @@
+/*
+ * 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.schemata.hr;
+
+import org.apache.calcite.schema.QueryableTable;
+import org.apache.calcite.schema.TranslatableTable;
+import org.apache.calcite.util.Smalls;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * A schema that contains two tables by reflection.
+ *
+ * <p>Here is the SQL to create equivalent tables in Oracle:
+ *
+ * <blockquote>
+ * <pre>
+ * CREATE TABLE "emps" (
+ *   "empid" INTEGER NOT NULL,
+ *   "deptno" INTEGER NOT NULL,
+ *   "name" VARCHAR2(10) NOT NULL,
+ *   "salary" NUMBER(6, 2) NOT NULL,
+ *   "commission" INTEGER);
+ * INSERT INTO "emps" VALUES (100, 10, 'Bill', 10000, 1000);
+ * INSERT INTO "emps" VALUES (200, 20, 'Eric', 8000, 500);
+ * INSERT INTO "emps" VALUES (150, 10, 'Sebastian', 7000, null);
+ * INSERT INTO "emps" VALUES (110, 10, 'Theodore', 11500, 250);
+ *
+ * CREATE TABLE "depts" (
+ *   "deptno" INTEGER NOT NULL,
+ *   "name" VARCHAR2(10) NOT NULL,
+ *   "employees" ARRAY OF "Employee",
+ *   "location" "Location");
+ * INSERT INTO "depts" VALUES (10, 'Sales', null, (-122, 38));
+ * INSERT INTO "depts" VALUES (30, 'Marketing', null, (0, 52));
+ * INSERT INTO "depts" VALUES (40, 'HR', null, null);
+ * </pre>
+ * </blockquote>
+ */
+public class HrSchema {
+  @Override public String toString() {
+    return "HrSchema";
+  }
+
+  public final Employee[] emps = {
+      new Employee(100, 10, "Bill", 10000, 1000),
+      new Employee(200, 20, "Eric", 8000, 500),
+      new Employee(150, 10, "Sebastian", 7000, null),
+      new Employee(110, 10, "Theodore", 11500, 250),
+  };
+  public final Department[] depts = {
+      new Department(10, "Sales", Arrays.asList(emps[0], emps[2]),
+          new Location(-122, 38)),
+      new Department(30, "Marketing", ImmutableList.of(), new Location(0, 52)),
+      new Department(40, "HR", Collections.singletonList(emps[1]), null),
+  };
+  public final Dependent[] dependents = {
+      new Dependent(10, "Michael"),
+      new Dependent(10, "Jane"),
+  };
+  public final Dependent[] locations = {
+      new Dependent(10, "San Francisco"),
+      new Dependent(20, "San Diego"),
+  };
+
+  public QueryableTable foo(int count) {
+    return Smalls.generateStrings(count);
+  }
+
+  public TranslatableTable view(String s) {
+    return Smalls.view(s);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HrSchemaBig.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HrSchemaBig.java
new file mode 100644
index 0000000..639cc4e
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/HrSchemaBig.java
@@ -0,0 +1,100 @@
+/*
+ * 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.schemata.hr;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * HR schema with more data than in {@link HrSchema}.
+ */
+public class HrSchemaBig {
+  @Override public String toString() {
+    return "HrSchema";
+  }
+
+  public final Employee[] emps = {
+      new Employee(1, 10, "Bill", 10000, 1000),
+      new Employee(2, 20, "Eric", 8000, 500),
+      new Employee(3, 10, "Sebastian", 7000, null),
+      new Employee(4, 10, "Theodore", 11500, 250),
+      new Employee(5, 10, "Marjorie", 10000, 1000),
+      new Employee(6, 20, "Guy", 8000, 500),
+      new Employee(7, 10, "Dieudonne", 7000, null),
+      new Employee(8, 10, "Haroun", 11500, 250),
+      new Employee(9, 10, "Sarah", 10000, 1000),
+      new Employee(10, 20, "Gabriel", 8000, 500),
+      new Employee(11, 10, "Pierre", 7000, null),
+      new Employee(12, 10, "Paul", 11500, 250),
+      new Employee(13, 10, "Jacques", 100, 1000),
+      new Employee(14, 20, "Khawla", 8000, 500),
+      new Employee(15, 10, "Brielle", 7000, null),
+      new Employee(16, 10, "Hyuna", 11500, 250),
+      new Employee(17, 10, "Ahmed", 10000, 1000),
+      new Employee(18, 20, "Lara", 8000, 500),
+      new Employee(19, 10, "Capucine", 7000, null),
+      new Employee(20, 10, "Michelle", 11500, 250),
+      new Employee(21, 10, "Cerise", 10000, 1000),
+      new Employee(22, 80, "Travis", 8000, 500),
+      new Employee(23, 10, "Taylor", 7000, null),
+      new Employee(24, 10, "Seohyun", 11500, 250),
+      new Employee(25, 70, "Helen", 10000, 1000),
+      new Employee(26, 50, "Patric", 8000, 500),
+      new Employee(27, 10, "Clara", 7000, null),
+      new Employee(28, 10, "Catherine", 11500, 250),
+      new Employee(29, 10, "Anibal", 10000, 1000),
+      new Employee(30, 30, "Ursula", 8000, 500),
+      new Employee(31, 10, "Arturito", 7000, null),
+      new Employee(32, 70, "Diane", 11500, 250),
+      new Employee(33, 10, "Phoebe", 10000, 1000),
+      new Employee(34, 20, "Maria", 8000, 500),
+      new Employee(35, 10, "Edouard", 7000, null),
+      new Employee(36, 110, "Isabelle", 11500, 250),
+      new Employee(37, 120, "Olivier", 10000, 1000),
+      new Employee(38, 20, "Yann", 8000, 500),
+      new Employee(39, 60, "Ralf", 7000, null),
+      new Employee(40, 60, "Emmanuel", 11500, 250),
+      new Employee(41, 10, "Berenice", 10000, 1000),
+      new Employee(42, 20, "Kylie", 8000, 500),
+      new Employee(43, 80, "Natacha", 7000, null),
+      new Employee(44, 100, "Henri", 11500, 250),
+      new Employee(45, 90, "Pascal", 10000, 1000),
+      new Employee(46, 90, "Sabrina", 8000, 500),
+      new Employee(47, 8, "Riyad", 7000, null),
+      new Employee(48, 5, "Andy", 11500, 250),
+  };
+  public final Department[] depts = {
+      new Department(10, "Sales", Arrays.asList(emps[0], emps[2]),
+          new Location(-122, 38)),
+      new Department(20, "Marketing", ImmutableList.of(), new Location(0, 52)),
+      new Department(30, "HR", Collections.singletonList(emps[1]), null),
+      new Department(40, "Administration", Arrays.asList(emps[0], emps[2]),
+          new Location(-122, 38)),
+      new Department(50, "Design", ImmutableList.of(), new Location(0, 52)),
+      new Department(60, "IT", Collections.singletonList(emps[1]), null),
+      new Department(70, "Production", Arrays.asList(emps[0], emps[2]),
+          new Location(-122, 38)),
+      new Department(80, "Finance", ImmutableList.of(), new Location(0, 52)),
+      new Department(90, "Accounting", Collections.singletonList(emps[1]), null),
+      new Department(100, "Research", Arrays.asList(emps[0], emps[2]),
+          new Location(-122, 38)),
+      new Department(110, "Maintenance", ImmutableList.of(), new Location(0, 52)),
+      new Department(120, "Client Support", Collections.singletonList(emps[1]), null),
+  };
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Location.java b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Location.java
new file mode 100644
index 0000000..769c619
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/hr/Location.java
@@ -0,0 +1,47 @@
+/*
+ * 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.schemata.hr;
+
+import java.util.Objects;
+
+/**
+ * Location model.
+ */
+public class  Location {
+  public final int x;
+  public final int y;
+
+  public Location(int x, int y) {
+    this.x = x;
+    this.y = y;
+  }
+
+  @Override public String toString() {
+    return "Location [x: " + x + ", y: " + y + "]";
+  }
+
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof Location
+        && x == ((Location) obj).x
+        && y == ((Location) obj).y;
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(x, y);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/lingual/LingualEmp.java b/testkit/src/main/java/org/apache/calcite/test/schemata/lingual/LingualEmp.java
new file mode 100644
index 0000000..32509b3
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/lingual/LingualEmp.java
@@ -0,0 +1,42 @@
+/*
+ * 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.schemata.lingual;
+
+import java.util.Objects;
+
+/**
+ * Lingual emp model.
+ */
+public class LingualEmp {
+  public final int EMPNO;
+  public final int DEPTNO;
+
+  public LingualEmp(int EMPNO, int DEPTNO) {
+    this.EMPNO = EMPNO;
+    this.DEPTNO = DEPTNO;
+  }
+
+  @Override public boolean equals(Object obj) {
+    return obj == this
+        || obj instanceof LingualEmp
+        && EMPNO == ((LingualEmp) obj).EMPNO;
+  }
+
+  @Override public int hashCode() {
+    return Objects.hash(EMPNO);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/lingual/LingualSchema.java b/testkit/src/main/java/org/apache/calcite/test/schemata/lingual/LingualSchema.java
new file mode 100644
index 0000000..411ef5d
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/lingual/LingualSchema.java
@@ -0,0 +1,27 @@
+/*
+ * 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.schemata.lingual;
+
+/**
+ * Lingual schema.
+ */
+public class LingualSchema {
+  public final LingualEmp[] EMPS = {
+      new LingualEmp(1, 10),
+      new LingualEmp(2, 30)
+  };
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/BaseOrderStreamTable.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/BaseOrderStreamTable.java
new file mode 100644
index 0000000..e6eb2fe
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/BaseOrderStreamTable.java
@@ -0,0 +1,69 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.config.CalciteConnectionConfig;
+import org.apache.calcite.rel.RelCollations;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelProtoDataType;
+import org.apache.calcite.schema.ScannableTable;
+import org.apache.calcite.schema.Schema;
+import org.apache.calcite.schema.Statistic;
+import org.apache.calcite.schema.Statistics;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Base table for the Orders table. Manages the base schema used for the test tables and common
+ * functions.
+ */
+public abstract class BaseOrderStreamTable implements ScannableTable {
+  protected final RelProtoDataType protoRowType = a0 -> a0.builder()
+      .add("ROWTIME", SqlTypeName.TIMESTAMP)
+      .add("ID", SqlTypeName.INTEGER)
+      .add("PRODUCT", SqlTypeName.VARCHAR, 10)
+      .add("UNITS", SqlTypeName.INTEGER)
+      .build();
+
+  public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+    return protoRowType.apply(typeFactory);
+  }
+
+  public Statistic getStatistic() {
+    return Statistics.of(100d, ImmutableList.of(),
+        RelCollations.createSingleton(0));
+  }
+
+  public Schema.TableType getJdbcTableType() {
+    return Schema.TableType.TABLE;
+  }
+
+  @Override public boolean isRolledUp(String column) {
+    return false;
+  }
+
+  @Override public boolean rolledUpColumnValidInsideAgg(String column,
+      SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
+    return false;
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/InfiniteOrdersStreamTableFactory.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/InfiniteOrdersStreamTableFactory.java
new file mode 100644
index 0000000..4bb64e7
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/InfiniteOrdersStreamTableFactory.java
@@ -0,0 +1,41 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.TableFactory;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Map;
+
+/**
+ * Mock table that returns a stream of orders from a fixed array.
+ */
+@SuppressWarnings("UnusedDeclaration")
+public class InfiniteOrdersStreamTableFactory implements TableFactory<Table> {
+  // public constructor, per factory contract
+  public InfiniteOrdersStreamTableFactory() {
+  }
+
+  public Table create(SchemaPlus schema, String name,
+      Map<String, Object> operand, @Nullable RelDataType rowType) {
+    return new InfiniteOrdersTable();
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/InfiniteOrdersTable.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/InfiniteOrdersTable.java
new file mode 100644
index 0000000..1316ee9
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/InfiniteOrdersTable.java
@@ -0,0 +1,58 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.schema.StreamableTable;
+import org.apache.calcite.schema.Table;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Iterator;
+
+/**
+ * Table representing an infinitely larger ORDERS stream.
+ */
+public class InfiniteOrdersTable extends BaseOrderStreamTable
+    implements StreamableTable {
+  public Enumerable<@Nullable Object[]> scan(DataContext root) {
+    return Linq4j.asEnumerable(() -> new Iterator<Object[]>() {
+      private final String[] items = {"paint", "paper", "brush"};
+      private int counter = 0;
+
+      public boolean hasNext() {
+        return true;
+      }
+
+      public Object[] next() {
+        final int index = counter++;
+        return new Object[]{
+            System.currentTimeMillis(), index, items[index % items.length], 10};
+      }
+
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    });
+  }
+
+  public Table stream() {
+    return this;
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersHistoryTable.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersHistoryTable.java
new file mode 100644
index 0000000..1394542
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersHistoryTable.java
@@ -0,0 +1,38 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Linq4j;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/** Table representing the history of the ORDERS stream. */
+public class OrdersHistoryTable extends BaseOrderStreamTable {
+  private final ImmutableList<Object[]> rows;
+
+  public OrdersHistoryTable(ImmutableList<Object[]> rows) {
+    this.rows = rows;
+  }
+
+  public Enumerable<@Nullable Object[]> scan(DataContext root) {
+    return Linq4j.asEnumerable(rows);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersStreamTableFactory.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersStreamTableFactory.java
new file mode 100644
index 0000000..15c5ce9
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersStreamTableFactory.java
@@ -0,0 +1,57 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.avatica.util.DateTimeUtils;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.TableFactory;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Map;
+
+/** Mock table that returns a stream of orders from a fixed array. */
+@SuppressWarnings("UnusedDeclaration")
+public class OrdersStreamTableFactory implements TableFactory<Table> {
+  // public constructor, per factory contract
+  public OrdersStreamTableFactory() {
+  }
+
+  public Table create(SchemaPlus schema, String name,
+      Map<String, Object> operand, @Nullable RelDataType rowType) {
+    return new OrdersTable(getRowList());
+  }
+
+  public static ImmutableList<Object[]> getRowList() {
+    final Object[][] rows = {
+        {ts(10, 15, 0), 1, "paint", 10},
+        {ts(10, 24, 15), 2, "paper", 5},
+        {ts(10, 24, 45), 3, "brush", 12},
+        {ts(10, 58, 0), 4, "paint", 3},
+        {ts(11, 10, 0), 5, "paint", 3}
+    };
+    return ImmutableList.copyOf(rows);
+  }
+
+  private static Object ts(int h, int m, int s) {
+    return DateTimeUtils.unixTimestamp(2015, 2, 15, h, m, s);
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersTable.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersTable.java
new file mode 100644
index 0000000..6558cfb
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/OrdersTable.java
@@ -0,0 +1,59 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.config.CalciteConnectionConfig;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.schema.StreamableTable;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlNode;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Table representing the ORDERS stream.
+ */
+public class OrdersTable extends BaseOrderStreamTable
+    implements StreamableTable {
+  private final ImmutableList<Object[]> rows;
+
+  public OrdersTable(ImmutableList<Object[]> rows) {
+    this.rows = rows;
+  }
+
+  public Enumerable<@Nullable Object[]> scan(DataContext root) {
+    return Linq4j.asEnumerable(rows);
+  }
+
+  @Override public Table stream() {
+    return new OrdersTable(rows);
+  }
+
+  @Override public boolean isRolledUp(String column) {
+    return false;
+  }
+
+  @Override public boolean rolledUpColumnValidInsideAgg(String column,
+      SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
+    return false;
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTable.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTable.java
new file mode 100644
index 0000000..6bd9eb5
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTable.java
@@ -0,0 +1,77 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.DataContext;
+import org.apache.calcite.config.CalciteConnectionConfig;
+import org.apache.calcite.linq4j.Enumerable;
+import org.apache.calcite.linq4j.Linq4j;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelProtoDataType;
+import org.apache.calcite.schema.ScannableTable;
+import org.apache.calcite.schema.Schema;
+import org.apache.calcite.schema.Statistic;
+import org.apache.calcite.schema.Statistics;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Table representing the PRODUCTS relation.
+ */
+public class ProductsTable implements ScannableTable {
+  private final ImmutableList<Object[]> rows;
+
+  public ProductsTable(ImmutableList<Object[]> rows) {
+    this.rows = rows;
+  }
+
+  private final RelProtoDataType protoRowType = a0 -> a0.builder()
+      .add("ID", SqlTypeName.VARCHAR, 32)
+      .add("SUPPLIER", SqlTypeName.INTEGER)
+      .build();
+
+  public Enumerable<@Nullable Object[]> scan(DataContext root) {
+    return Linq4j.asEnumerable(rows);
+  }
+
+  public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+    return protoRowType.apply(typeFactory);
+  }
+
+  public Statistic getStatistic() {
+    return Statistics.of(200d, ImmutableList.of());
+  }
+
+  public Schema.TableType getJdbcTableType() {
+    return Schema.TableType.TABLE;
+  }
+
+  @Override public boolean isRolledUp(String column) {
+    return false;
+  }
+
+  @Override public boolean rolledUpColumnValidInsideAgg(String column,
+      SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
+    return false;
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTableFactory.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTableFactory.java
new file mode 100644
index 0000000..d2fd83a
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTableFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.Table;
+import org.apache.calcite.schema.TableFactory;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Map;
+
+/**
+ * Mocks a simple relation to use for stream joining test.
+ */
+public class ProductsTableFactory implements TableFactory<Table> {
+  public Table create(SchemaPlus schema, String name,
+      Map<String, Object> operand, @Nullable RelDataType rowType) {
+    final Object[][] rows = {
+        {"paint", 1},
+        {"paper", 0},
+        {"brush", 1}
+    };
+    return new ProductsTable(ImmutableList.copyOf(rows));
+  }
+}
diff --git a/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTemporalTable.java b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTemporalTable.java
new file mode 100644
index 0000000..92a7d6e
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/orderstream/ProductsTemporalTable.java
@@ -0,0 +1,75 @@
+/*
+ * 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.schemata.orderstream;
+
+import org.apache.calcite.config.CalciteConnectionConfig;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rel.type.RelProtoDataType;
+import org.apache.calcite.schema.Schema;
+import org.apache.calcite.schema.Statistic;
+import org.apache.calcite.schema.Statistics;
+import org.apache.calcite.schema.TemporalTable;
+import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Table representing the PRODUCTS_TEMPORAL temporal table.
+ */
+public class ProductsTemporalTable implements TemporalTable {
+
+  private final RelProtoDataType protoRowType = a0 -> a0.builder()
+      .add("ID", SqlTypeName.VARCHAR, 32)
+      .add("SUPPLIER", SqlTypeName.INTEGER)
+      .add("SYS_START", SqlTypeName.TIMESTAMP)
+      .add("SYS_END", SqlTypeName.TIMESTAMP)
+      .build();
+
+  @Override public String getSysStartFieldName() {
+    return "SYS_START";
+  }
+
+  @Override public String getSysEndFieldName() {
+    return "SYS_END";
+  }
+
+  @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+    return protoRowType.apply(typeFactory);
+  }
+
+  @Override public Statistic getStatistic() {
+    return Statistics.of(200d, ImmutableList.of());
+  }
+
+  @Override public Schema.TableType getJdbcTableType() {
+    return Schema.TableType.TABLE;
+  }
+
+  @Override public boolean isRolledUp(String column) {
+    return false;
+  }
+
+  @Override public boolean rolledUpColumnValidInsideAgg(String column,
+      SqlCall call, @Nullable SqlNode parent, @Nullable CalciteConnectionConfig config) {
+    return false;
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/tools/TpchSchema.java b/testkit/src/main/java/org/apache/calcite/test/schemata/tpch/TpchSchema.java
similarity index 97%
rename from core/src/test/java/org/apache/calcite/tools/TpchSchema.java
rename to testkit/src/main/java/org/apache/calcite/test/schemata/tpch/TpchSchema.java
index 13900d0..f2cfcdb 100644
--- a/core/src/test/java/org/apache/calcite/tools/TpchSchema.java
+++ b/testkit/src/main/java/org/apache/calcite/test/schemata/tpch/TpchSchema.java
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.calcite.tools;
+package org.apache.calcite.test.schemata.tpch;
 
 /**
  * TPC-H table schema.
diff --git a/core/src/test/java/org/apache/calcite/util/Smalls.java b/testkit/src/main/java/org/apache/calcite/util/Smalls.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/util/Smalls.java
rename to testkit/src/main/java/org/apache/calcite/util/Smalls.java
diff --git a/core/src/test/java/org/apache/calcite/util/TestUtil.java b/testkit/src/main/java/org/apache/calcite/util/TestUtil.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/util/TestUtil.java
rename to testkit/src/main/java/org/apache/calcite/util/TestUtil.java
diff --git a/testkit/src/main/java/org/apache/calcite/util/package-info.java b/testkit/src/main/java/org/apache/calcite/util/package-info.java
new file mode 100644
index 0000000..d1fa6d4
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/util/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Classes for testing Calcite.
+ */
+package org.apache.calcite.util;
diff --git a/core/src/test/kotlin/org/apache/calcite/testlib/WithLocaleExtension.kt b/testkit/src/main/kotlin/org/apache/calcite/testlib/WithLocaleExtension.kt
similarity index 100%
rename from core/src/test/kotlin/org/apache/calcite/testlib/WithLocaleExtension.kt
rename to testkit/src/main/kotlin/org/apache/calcite/testlib/WithLocaleExtension.kt
diff --git a/core/src/test/kotlin/org/apache/calcite/testlib/annotations/LocaleEnUs.kt b/testkit/src/main/kotlin/org/apache/calcite/testlib/annotations/LocaleEnUs.kt
similarity index 100%
rename from core/src/test/kotlin/org/apache/calcite/testlib/annotations/LocaleEnUs.kt
rename to testkit/src/main/kotlin/org/apache/calcite/testlib/annotations/LocaleEnUs.kt
diff --git a/core/src/test/kotlin/org/apache/calcite/testlib/annotations/WithLex.kt b/testkit/src/main/kotlin/org/apache/calcite/testlib/annotations/WithLex.kt
similarity index 100%
rename from core/src/test/kotlin/org/apache/calcite/testlib/annotations/WithLex.kt
rename to testkit/src/main/kotlin/org/apache/calcite/testlib/annotations/WithLex.kt
diff --git a/core/src/test/kotlin/org/apache/calcite/testlib/annotations/WithLocale.kt b/testkit/src/main/kotlin/org/apache/calcite/testlib/annotations/WithLocale.kt
similarity index 100%
rename from core/src/test/kotlin/org/apache/calcite/testlib/annotations/WithLocale.kt
rename to testkit/src/main/kotlin/org/apache/calcite/testlib/annotations/WithLocale.kt
diff --git a/core/src/test/java/org/apache/calcite/util/TestUtilTest.java b/testkit/src/test/java/org/apache/calcite/util/TestUtilTest.java
similarity index 100%
rename from core/src/test/java/org/apache/calcite/util/TestUtilTest.java
rename to testkit/src/test/java/org/apache/calcite/util/TestUtilTest.java
diff --git a/core/src/test/kotlin/org/apache/calcite/TestKtTest.kt b/testkit/src/test/kotlin/org/apache/calcite/TestKtTest.kt
similarity index 100%
rename from core/src/test/kotlin/org/apache/calcite/TestKtTest.kt
rename to testkit/src/test/kotlin/org/apache/calcite/TestKtTest.kt