You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2016/03/17 21:10:13 UTC

[6/8] calcite git commit: [CALCITE-1147] Allow multiple providers for the same kind of metadata

[CALCITE-1147] Allow multiple providers for the same kind of metadata

Make it possible to extend RelMetadataQuery for a user-defined metadata
type, and create a test case illustrating.

Give an explicit error when metadata provider does not have a RelNode
handler (that is, a safety net if there are no handlers for more specific
RelNode sub-classes).

Improve how we handle ExecutionException.


Project: http://git-wip-us.apache.org/repos/asf/calcite/repo
Commit: http://git-wip-us.apache.org/repos/asf/calcite/commit/94f8837c
Tree: http://git-wip-us.apache.org/repos/asf/calcite/tree/94f8837c
Diff: http://git-wip-us.apache.org/repos/asf/calcite/diff/94f8837c

Branch: refs/heads/master
Commit: 94f8837c4974edc33e58b16b9ee2f061125b9b60
Parents: 22aa8a8
Author: Julian Hyde <jh...@apache.org>
Authored: Fri Mar 11 15:35:59 2016 -0800
Committer: Julian Hyde <jh...@apache.org>
Committed: Thu Mar 17 12:32:01 2016 -0700

----------------------------------------------------------------------
 .../CachingLatticeStatisticProvider.java        |   5 +-
 .../plan/hep/HepRelMetadataProvider.java        |   8 +-
 .../volcano/VolcanoRelMetadataProvider.java     |   8 +-
 .../metadata/CachingRelMetadataProvider.java    |   3 +-
 .../metadata/ChainedRelMetadataProvider.java    |  10 +-
 .../rel/metadata/JaninoRelMetadataProvider.java |  11 +-
 .../rel/metadata/MetadataFactoryImpl.java       |  10 +-
 .../metadata/ReflectiveRelMetadataProvider.java |  52 +++---
 .../rel/metadata/RelMetadataProvider.java       |   5 +-
 .../calcite/rel/metadata/RelMetadataQuery.java  |  93 +++++-----
 .../org/apache/calcite/test/CalciteAssert.java  |   5 +-
 .../apache/calcite/test/RelMetadataTest.java    | 170 ++++++++++++++-----
 .../apache/calcite/test/RelOptRulesTest.java    |   2 +-
 .../apache/calcite/test/SqlToRelTestBase.java   |  29 +++-
 14 files changed, 256 insertions(+), 155 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/materialize/CachingLatticeStatisticProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/materialize/CachingLatticeStatisticProvider.java b/core/src/main/java/org/apache/calcite/materialize/CachingLatticeStatisticProvider.java
index bf70e5d..bf2702c 100644
--- a/core/src/main/java/org/apache/calcite/materialize/CachingLatticeStatisticProvider.java
+++ b/core/src/main/java/org/apache/calcite/materialize/CachingLatticeStatisticProvider.java
@@ -22,6 +22,7 @@ import com.google.common.base.Throwables;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 
 import java.util.concurrent.ExecutionException;
 
@@ -48,8 +49,8 @@ class CachingLatticeStatisticProvider implements LatticeStatisticProvider {
   public int cardinality(Lattice lattice, Lattice.Column column) {
     try {
       return cache.get(Pair.of(lattice, column));
-    } catch (ExecutionException e) {
-      throw Throwables.propagate(e);
+    } catch (UncheckedExecutionException | ExecutionException e) {
+      throw Throwables.propagate(e.getCause());
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/plan/hep/HepRelMetadataProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/plan/hep/HepRelMetadataProvider.java
index 317fe94..3ed2b96 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepRelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepRelMetadataProvider.java
@@ -24,10 +24,10 @@ import org.apache.calcite.rel.metadata.RelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.metadata.UnboundMetadata;
 
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
 
 import java.lang.reflect.Method;
-import java.util.Map;
 
 /**
  * HepRelMetadataProvider implements the {@link RelMetadataProvider} interface
@@ -62,9 +62,9 @@ class HepRelMetadataProvider implements RelMetadataProvider {
     };
   }
 
-  public <M extends Metadata> Map<Method, MetadataHandler<M>>
+  public <M extends Metadata> Multimap<Method, MetadataHandler<M>>
   handlers(MetadataDef<M> def) {
-    return ImmutableMap.of();
+    return ImmutableMultimap.of();
   }
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRelMetadataProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRelMetadataProvider.java
index c32d7d3..f996707 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRelMetadataProvider.java
@@ -24,10 +24,10 @@ import org.apache.calcite.rel.metadata.RelMetadataProvider;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.metadata.UnboundMetadata;
 
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
 
 import java.lang.reflect.Method;
-import java.util.Map;
 
 /**
  * VolcanoRelMetadataProvider implements the {@link RelMetadataProvider}
@@ -115,9 +115,9 @@ public class VolcanoRelMetadataProvider implements RelMetadataProvider {
     };
   }
 
-  public <M extends Metadata> Map<Method, MetadataHandler<M>>
+  public <M extends Metadata> Multimap<Method, MetadataHandler<M>>
   handlers(MetadataDef<M> def) {
-    return ImmutableMap.of();
+    return ImmutableMultimap.of();
   }
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/rel/metadata/CachingRelMetadataProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/CachingRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/rel/metadata/CachingRelMetadataProvider.java
index 202bf4c..eb63604 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/CachingRelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/CachingRelMetadataProvider.java
@@ -22,6 +22,7 @@ import org.apache.calcite.rel.RelNode;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Multimap;
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
@@ -77,7 +78,7 @@ public class CachingRelMetadataProvider implements RelMetadataProvider {
     };
   }
 
-  public <M extends Metadata> Map<Method, MetadataHandler<M>>
+  public <M extends Metadata> Multimap<Method, MetadataHandler<M>>
   handlers(MetadataDef<M> def) {
     return underlyingProvider.handlers(def);
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/rel/metadata/ChainedRelMetadataProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/ChainedRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/rel/metadata/ChainedRelMetadataProvider.java
index 698bc9c..1ecec0f 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/ChainedRelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/ChainedRelMetadataProvider.java
@@ -20,8 +20,9 @@ import org.apache.calcite.rel.RelNode;
 
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
@@ -29,7 +30,6 @@ import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 
 /**
  * Implementation of the {@link RelMetadataProvider}
@@ -104,10 +104,10 @@ public class ChainedRelMetadataProvider implements RelMetadataProvider {
     }
   }
 
-  public <M extends Metadata> Map<Method, MetadataHandler<M>>
+  public <M extends Metadata> Multimap<Method, MetadataHandler<M>>
   handlers(MetadataDef<M> def) {
-    final ImmutableMap.Builder<Method, MetadataHandler<M>> builder =
-        ImmutableMap.builder();
+    final ImmutableMultimap.Builder<Method, MetadataHandler<M>> builder =
+        ImmutableMultimap.builder();
     for (RelMetadataProvider provider : providers.reverse()) {
       builder.putAll(provider.handlers(def));
     }

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java
index ac7f4d1..ac395e4 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/JaninoRelMetadataProvider.java
@@ -63,6 +63,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.LinkedHashMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 
 import org.codehaus.commons.compiler.CompileException;
 import org.codehaus.commons.compiler.CompilerFactoryFactory;
@@ -182,14 +183,14 @@ public class JaninoRelMetadataProvider implements RelMetadataProvider {
     throw new UnsupportedOperationException();
   }
 
-  public <M extends Metadata> Map<Method, MetadataHandler<M>>
+  public <M extends Metadata> Multimap<Method, MetadataHandler<M>>
   handlers(MetadataDef<M> def) {
     return provider.handlers(def);
   }
 
   private static <M extends Metadata>
   MetadataHandler<M> load3(MetadataDef<M> def,
-      Map<Method, MetadataHandler<M>> map,
+      Multimap<Method, MetadataHandler<M>> map,
       ImmutableList<Class<? extends RelNode>> relClasses) {
     final StringBuilder buff = new StringBuilder();
     final String name =
@@ -198,7 +199,7 @@ public class JaninoRelMetadataProvider implements RelMetadataProvider {
     final List<Pair<String, MetadataHandler>> providerList = new ArrayList<>();
     //noinspection unchecked
     final ReflectiveRelMetadataProvider.Space space =
-        new ReflectiveRelMetadataProvider.Space((Map) map);
+        new ReflectiveRelMetadataProvider.Space((Multimap) map);
     for (MetadataHandler provider : space.providerMap.values()) {
       if (providerSet.add(provider)) {
         providerList.add(Pair.of("provider" + (providerSet.size() - 1),
@@ -445,8 +446,8 @@ public class JaninoRelMetadataProvider implements RelMetadataProvider {
           ImmutableList.copyOf(ALL_RELS));
       //noinspection unchecked
       return (H) HANDLERS.get(key);
-    } catch (ExecutionException e) {
-      throw Throwables.propagate(e);
+    } catch (UncheckedExecutionException | ExecutionException e) {
+      throw Throwables.propagate(e.getCause());
     }
   }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/rel/metadata/MetadataFactoryImpl.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/MetadataFactoryImpl.java b/core/src/main/java/org/apache/calcite/rel/metadata/MetadataFactoryImpl.java
index f5b5717..09446be 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/MetadataFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/MetadataFactoryImpl.java
@@ -19,9 +19,11 @@ package org.apache.calcite.rel.metadata;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.util.Pair;
 
+import com.google.common.base.Throwables;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 
 import java.util.concurrent.ExecutionException;
 
@@ -70,12 +72,8 @@ public class MetadataFactoryImpl implements MetadataFactory {
           (Pair) Pair.of(rel.getClass(), metadataClazz);
       final Metadata apply = cache.get(key).bind(rel, mq);
       return metadataClazz.cast(apply);
-    } catch (ExecutionException e) {
-      if (e.getCause() instanceof RuntimeException) {
-        throw (RuntimeException) e.getCause();
-      } else {
-        throw (Error) e.getCause();
-      }
+    } catch (UncheckedExecutionException | ExecutionException e) {
+      throw Throwables.propagate(e.getCause());
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/rel/metadata/ReflectiveRelMetadataProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/ReflectiveRelMetadataProvider.java b/core/src/main/java/org/apache/calcite/rel/metadata/ReflectiveRelMetadataProvider.java
index 5f6d680..baceb35 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/ReflectiveRelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/ReflectiveRelMetadataProvider.java
@@ -25,9 +25,11 @@ import org.apache.calcite.util.Pair;
 import org.apache.calcite.util.ReflectiveVisitor;
 import org.apache.calcite.util.Util;
 
+import com.google.common.base.Preconditions;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Multimap;
 
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.InvocationTargetException;
@@ -63,7 +65,7 @@ public class ReflectiveRelMetadataProvider
   //~ Instance fields --------------------------------------------------------
   private final ConcurrentMap<Class<RelNode>, UnboundMetadata> map;
   private final Class<? extends Metadata> metadataClass0;
-  private final ImmutableMap<Method, MetadataHandler> handlerMap;
+  private final ImmutableMultimap<Method, MetadataHandler> handlerMap;
 
   //~ Constructors -----------------------------------------------------------
 
@@ -77,11 +79,11 @@ public class ReflectiveRelMetadataProvider
   protected ReflectiveRelMetadataProvider(
       ConcurrentMap<Class<RelNode>, UnboundMetadata> map,
       Class<? extends Metadata> metadataClass0,
-      Map<Method, MetadataHandler> handlerMap) {
+      Multimap<Method, MetadataHandler> handlerMap) {
     assert !map.isEmpty() : "are your methods named wrong?";
     this.map = map;
     this.metadataClass0 = metadataClass0;
-    this.handlerMap = ImmutableMap.copyOf(handlerMap);
+    this.handlerMap = ImmutableMultimap.copyOf(handlerMap);
   }
 
   /** Returns an implementation of {@link RelMetadataProvider} that scans for
@@ -210,11 +212,11 @@ public class ReflectiveRelMetadataProvider
         space.providerMap);
   }
 
-  public <M extends Metadata> Map<Method, MetadataHandler<M>>
+  public <M extends Metadata> Multimap<Method, MetadataHandler<M>>
   handlers(MetadataDef<M> def) {
-    final ImmutableMap.Builder<Method, MetadataHandler<M>> builder =
-        ImmutableMap.builder();
-    for (Map.Entry<Method, MetadataHandler> entry : handlerMap.entrySet()) {
+    final ImmutableMultimap.Builder<Method, MetadataHandler<M>> builder =
+        ImmutableMultimap.builder();
+    for (Map.Entry<Method, MetadataHandler> entry : handlerMap.entries()) {
       if (def.methods.contains(entry.getKey())) {
         //noinspection unchecked
         builder.put(entry.getKey(), entry.getValue());
@@ -288,14 +290,14 @@ public class ReflectiveRelMetadataProvider
   static class Space {
     final Set<Class<RelNode>> classes = new HashSet<>();
     final Map<Pair<Class<RelNode>, Method>, Method> handlerMap = new HashMap<>();
-    final ImmutableMap<Method, MetadataHandler> providerMap;
+    final ImmutableMultimap<Method, MetadataHandler> providerMap;
 
-    Space(Map<Method, MetadataHandler> providerMap) {
-      this.providerMap = ImmutableMap.copyOf(providerMap);
+    Space(Multimap<Method, MetadataHandler> providerMap) {
+      this.providerMap = ImmutableMultimap.copyOf(providerMap);
 
       // Find the distinct set of RelNode classes handled by this provider,
       // ordered base-class first.
-      for (Map.Entry<Method, MetadataHandler> entry : providerMap.entrySet()) {
+      for (Map.Entry<Method, MetadataHandler> entry : providerMap.entries()) {
         final Method method = entry.getKey();
         final MetadataHandler provider = entry.getValue();
         for (final Method handlerMethod : provider.getClass().getMethods()) {
@@ -313,14 +315,14 @@ public class ReflectiveRelMetadataProvider
      * nearest base class. Assumes that base classes have already been added to
      * {@code map}. */
     @SuppressWarnings({ "unchecked", "SuspiciousMethodCalls" })
-    Method find(Class<? extends RelNode> relNodeClass, Method method) {
-      Method implementingMethod;
-      while (relNodeClass != null) {
-        implementingMethod = handlerMap.get(Pair.of(relNodeClass, method));
+    Method find(final Class<? extends RelNode> relNodeClass, Method method) {
+      Preconditions.checkNotNull(relNodeClass);
+      for (Class r = relNodeClass;;) {
+        Method implementingMethod = handlerMap.get(Pair.of(r, method));
         if (implementingMethod != null) {
           return implementingMethod;
         }
-        for (Class<?> clazz : relNodeClass.getInterfaces()) {
+        for (Class<?> clazz : r.getInterfaces()) {
           if (RelNode.class.isAssignableFrom(clazz)) {
             implementingMethod = handlerMap.get(Pair.of(clazz, method));
             if (implementingMethod != null) {
@@ -328,13 +330,13 @@ public class ReflectiveRelMetadataProvider
             }
           }
         }
-        if (RelNode.class.isAssignableFrom(relNodeClass.getSuperclass())) {
-          relNodeClass = (Class<RelNode>) relNodeClass.getSuperclass();
-        } else {
-          relNodeClass = null;
+        r = r.getSuperclass();
+        if (r == null || !RelNode.class.isAssignableFrom(r)) {
+          throw new IllegalArgumentException("No handler for method [" + method
+              + "] applied to argument of type [" + relNodeClass
+              + "]; we recommend you create a catch-all (RelNode) handler");
         }
       }
-      return null;
     }
   }
 
@@ -343,7 +345,7 @@ public class ReflectiveRelMetadataProvider
     private Class<Metadata> metadataClass0;
 
     public Space2(Class<Metadata> metadataClass0,
-        ImmutableMap<Method, MetadataHandler> providerMap) {
+        ImmutableMultimap<Method, MetadataHandler> providerMap) {
       super(providerMap);
       this.metadataClass0 = metadataClass0;
     }
@@ -359,8 +361,8 @@ public class ReflectiveRelMetadataProvider
         assert method.getDeclaringClass() == metadataClass0;
       }
 
-      final ImmutableMap.Builder<Method, MetadataHandler> providerBuilder =
-          ImmutableMap.builder();
+      final ImmutableMultimap.Builder<Method, MetadataHandler> providerBuilder =
+          ImmutableMultimap.builder();
       for (final Method method : methods) {
         providerBuilder.put(method, target);
       }

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataProvider.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataProvider.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataProvider.java
index c1d5f39..b4ddbe2 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataProvider.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataProvider.java
@@ -18,8 +18,9 @@ package org.apache.calcite.rel.metadata;
 
 import org.apache.calcite.rel.RelNode;
 
+import com.google.common.collect.Multimap;
+
 import java.lang.reflect.Method;
-import java.util.Map;
 
 /**
  * RelMetadataProvider defines an interface for obtaining metadata about
@@ -65,7 +66,7 @@ public interface RelMetadataProvider {
   apply(Class<? extends RelNode> relClass,
       Class<? extends M> metadataClass);
 
-  <M extends Metadata> Map<Method, MetadataHandler<M>>
+  <M extends Metadata> Multimap<Method, MetadataHandler<M>>
   handlers(MetadataDef<M> def);
 }
 

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
----------------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
index abca58e..4e07612 100644
--- a/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
+++ b/core/src/main/java/org/apache/calcite/rel/metadata/RelMetadataQuery.java
@@ -80,7 +80,7 @@ public class RelMetadataQuery {
 
   public final JaninoRelMetadataProvider metadataProvider;
 
-  private static final RelMetadataQuery EMPTY = new RelMetadataQuery(false);
+  protected static final RelMetadataQuery EMPTY = new RelMetadataQuery(false);
 
   private BuiltInMetadata.Collation.Handler collationHandler;
   private BuiltInMetadata.ColumnOrigin.Handler columnOriginHandler;
@@ -108,7 +108,7 @@ public class RelMetadataQuery {
         }
       };
 
-  private RelMetadataQuery(JaninoRelMetadataProvider metadataProvider,
+  protected RelMetadataQuery(JaninoRelMetadataProvider metadataProvider,
       RelMetadataQuery prototype) {
     this.metadataProvider = Preconditions.checkNotNull(metadataProvider);
     this.collationHandler = prototype.collationHandler;
@@ -131,7 +131,7 @@ public class RelMetadataQuery {
     this.uniqueKeysHandler = prototype.uniqueKeysHandler;
   }
 
-  private static <H> H initialHandler(Class<H> handlerClass) {
+  protected static <H> H initialHandler(Class<H> handlerClass) {
     return handlerClass.cast(
         Proxy.newProxyInstance(RelMetadataQuery.class.getClassLoader(),
             new Class[] {handlerClass},
@@ -179,6 +179,13 @@ public class RelMetadataQuery {
     this.uniqueKeysHandler = initialHandler(BuiltInMetadata.UniqueKeys.Handler.class);
   }
 
+  /** Re-generates the handler for a given kind of metadata, adding support for
+   * {@code class_} if it is not already present. */
+  protected <M extends Metadata, H extends MetadataHandler<M>> H
+  revise(Class<? extends RelNode> class_, MetadataDef<M> def) {
+    return metadataProvider.revise(class_, def);
+  }
+
   /**
    * Returns the
    * {@link BuiltInMetadata.RowCount#getRowCount()}
@@ -194,8 +201,7 @@ public class RelMetadataQuery {
         Double result = rowCountHandler.getRowCount(rel, this);
         return validateResult(result);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        rowCountHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.RowCount.DEF);
+        rowCountHandler = revise(e.relClass, BuiltInMetadata.RowCount.DEF);
       }
     }
   }
@@ -213,8 +219,8 @@ public class RelMetadataQuery {
       try {
         return maxRowCountHandler.getMaxRowCount(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        maxRowCountHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.MaxRowCount.DEF);
+        maxRowCountHandler =
+            revise(e.relClass, BuiltInMetadata.MaxRowCount.DEF);
       }
     }
   }
@@ -232,8 +238,8 @@ public class RelMetadataQuery {
       try {
         return cumulativeCostHandler.getCumulativeCost(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        cumulativeCostHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.CumulativeCost.DEF);
+        cumulativeCostHandler =
+            revise(e.relClass, BuiltInMetadata.CumulativeCost.DEF);
       }
     }
   }
@@ -251,8 +257,8 @@ public class RelMetadataQuery {
       try {
         return nonCumulativeCostHandler.getNonCumulativeCost(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        nonCumulativeCostHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.NonCumulativeCost.DEF);
+        nonCumulativeCostHandler =
+            revise(e.relClass, BuiltInMetadata.NonCumulativeCost.DEF);
       }
     }
   }
@@ -273,8 +279,8 @@ public class RelMetadataQuery {
             percentageOriginalRowsHandler.getPercentageOriginalRows(rel, this);
         return validatePercentage(result);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        percentageOriginalRowsHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.PercentageOriginalRows.DEF);
+        percentageOriginalRowsHandler =
+            revise(e.relClass, BuiltInMetadata.PercentageOriginalRows.DEF);
       }
     }
   }
@@ -295,8 +301,8 @@ public class RelMetadataQuery {
       try {
         return columnOriginHandler.getColumnOrigins(rel, this, column);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        columnOriginHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.ColumnOrigin.DEF);
+        columnOriginHandler =
+            revise(e.relClass, BuiltInMetadata.ColumnOrigin.DEF);
       }
     }
   }
@@ -359,8 +365,8 @@ public class RelMetadataQuery {
         Double result = selectivityHandler.getSelectivity(rel, this, predicate);
         return validatePercentage(result);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        selectivityHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Selectivity.DEF);
+        selectivityHandler =
+            revise(e.relClass, BuiltInMetadata.Selectivity.DEF);
       }
     }
   }
@@ -396,8 +402,8 @@ public class RelMetadataQuery {
       try {
         return uniqueKeysHandler.getUniqueKeys(rel, this, ignoreNulls);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        uniqueKeysHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.UniqueKeys.DEF);
+        uniqueKeysHandler =
+            revise(e.relClass, BuiltInMetadata.UniqueKeys.DEF);
       }
     }
   }
@@ -455,8 +461,8 @@ public class RelMetadataQuery {
         return columnUniquenessHandler.areColumnsUnique(rel, this, columns,
             ignoreNulls);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        columnUniquenessHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.ColumnUniqueness.DEF);
+        columnUniquenessHandler =
+            revise(e.relClass, BuiltInMetadata.ColumnUniqueness.DEF);
       }
     }
   }
@@ -475,8 +481,7 @@ public class RelMetadataQuery {
       try {
         return collationHandler.collations(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        collationHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Collation.DEF);
+        collationHandler = revise(e.relClass, BuiltInMetadata.Collation.DEF);
       }
     }
   }
@@ -495,8 +500,8 @@ public class RelMetadataQuery {
       try {
         return distributionHandler.distribution(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        distributionHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Distribution.DEF);
+        distributionHandler =
+            revise(e.relClass, BuiltInMetadata.Distribution.DEF);
       }
     }
   }
@@ -521,8 +526,8 @@ public class RelMetadataQuery {
             populationSizeHandler.getPopulationSize(rel, this, groupKey);
         return validateResult(result);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        populationSizeHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.PopulationSize.DEF);
+        populationSizeHandler =
+            revise(e.relClass, BuiltInMetadata.PopulationSize.DEF);
       }
     }
   }
@@ -540,8 +545,7 @@ public class RelMetadataQuery {
       try {
         return sizeHandler.averageRowSize(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        sizeHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Size.DEF);
+        sizeHandler = revise(e.relClass, BuiltInMetadata.Size.DEF);
       }
     }
   }
@@ -561,8 +565,7 @@ public class RelMetadataQuery {
       try {
         return sizeHandler.averageColumnSizes(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        sizeHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Size.DEF);
+        sizeHandler = revise(e.relClass, BuiltInMetadata.Size.DEF);
       }
     }
   }
@@ -591,8 +594,8 @@ public class RelMetadataQuery {
       try {
         return parallelismHandler.isPhaseTransition(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        parallelismHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Parallelism.DEF);
+        parallelismHandler =
+            revise(e.relClass, BuiltInMetadata.Parallelism.DEF);
       }
     }
   }
@@ -610,8 +613,8 @@ public class RelMetadataQuery {
       try {
         return parallelismHandler.splitCount(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        parallelismHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Parallelism.DEF);
+        parallelismHandler =
+            revise(e.relClass, BuiltInMetadata.Parallelism.DEF);
       }
     }
   }
@@ -631,8 +634,7 @@ public class RelMetadataQuery {
       try {
         return memoryHandler.memory(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        memoryHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Memory.DEF);
+        memoryHandler = revise(e.relClass, BuiltInMetadata.Memory.DEF);
       }
     }
   }
@@ -652,8 +654,7 @@ public class RelMetadataQuery {
       try {
         return memoryHandler.cumulativeMemoryWithinPhase(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        memoryHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Memory.DEF);
+        memoryHandler = revise(e.relClass, BuiltInMetadata.Memory.DEF);
       }
     }
   }
@@ -673,8 +674,7 @@ public class RelMetadataQuery {
       try {
         return memoryHandler.cumulativeMemoryWithinPhaseSplit(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        memoryHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Memory.DEF);
+        memoryHandler = revise(e.relClass, BuiltInMetadata.Memory.DEF);
       }
     }
   }
@@ -701,8 +701,8 @@ public class RelMetadataQuery {
                 predicate);
         return validateResult(result);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        distinctRowCountHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.DistinctRowCount.DEF);
+        distinctRowCountHandler =
+            revise(e.relClass, BuiltInMetadata.DistinctRowCount.DEF);
       }
     }
   }
@@ -720,8 +720,7 @@ public class RelMetadataQuery {
       try {
         return predicatesHandler.getPredicates(rel, this);
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        predicatesHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.Predicates.DEF);
+        predicatesHandler = revise(e.relClass, BuiltInMetadata.Predicates.DEF);
       }
     }
   }
@@ -744,8 +743,8 @@ public class RelMetadataQuery {
             explainLevel);
         return b == null || b;
       } catch (JaninoRelMetadataProvider.NoHandler e) {
-        explainVisibilityHandler = metadataProvider.revise(e.relClass,
-            BuiltInMetadata.ExplainVisibility.DEF);
+        explainVisibilityHandler =
+            revise(e.relClass, BuiltInMetadata.ExplainVisibility.DEF);
       }
     }
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/test/java/org/apache/calcite/test/CalciteAssert.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/CalciteAssert.java b/core/src/test/java/org/apache/calcite/test/CalciteAssert.java
index 729326a..7a962a0 100644
--- a/core/src/test/java/org/apache/calcite/test/CalciteAssert.java
+++ b/core/src/test/java/org/apache/calcite/test/CalciteAssert.java
@@ -48,6 +48,7 @@ import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMultiset;
 import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.UncheckedExecutionException;
 
 import net.hydromatic.foodmart.data.hsqldb.FoodmartHsqldb;
 import net.hydromatic.scott.data.hsqldb.ScottHsqldb;
@@ -1072,9 +1073,9 @@ public class CalciteAssert {
     public Connection createConnection() throws SQLException {
       try {
         return Pool.POOL.get(factory);
-      } catch (ExecutionException e) {
+      } catch (UncheckedExecutionException | ExecutionException e) {
         throw new SQLException(
-            "Unable to get pooled connection for " + factory, e);
+            "Unable to get pooled connection for " + factory, e.getCause());
       }
     }
   }

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
index ab7e02d..562286a 100644
--- a/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelMetadataTest.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.test;
 
 import org.apache.calcite.adapter.enumerable.EnumerableMergeJoin;
+import org.apache.calcite.linq4j.tree.Types;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptPredicateList;
@@ -51,6 +52,7 @@ import org.apache.calcite.rel.logical.LogicalValues;
 import org.apache.calcite.rel.metadata.CachingRelMetadataProvider;
 import org.apache.calcite.rel.metadata.ChainedRelMetadataProvider;
 import org.apache.calcite.rel.metadata.DefaultRelMetadataProvider;
+import org.apache.calcite.rel.metadata.JaninoRelMetadataProvider;
 import org.apache.calcite.rel.metadata.Metadata;
 import org.apache.calcite.rel.metadata.MetadataDef;
 import org.apache.calcite.rel.metadata.MetadataHandler;
@@ -71,6 +73,7 @@ import org.apache.calcite.tools.Frameworks;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
 
+import com.google.common.base.Function;
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -99,6 +102,7 @@ import static org.hamcrest.CoreMatchers.nullValue;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 /**
  * Unit test for {@link DefaultRelMetadataProvider}. See
@@ -860,38 +864,88 @@ public class RelMetadataTest extends SqlToRelTestBase {
     assertUniqueConsistent(rel);
   }
 
+  @Test public void testBrokenCustomProvider() {
+    final List<String> buf = Lists.newArrayList();
+    ColTypeImpl.THREAD_LIST.set(buf);
+
+    final String sql = "select deptno, count(*) from emp where deptno > 10 "
+        + "group by deptno having count(*) = 0";
+    final RelRoot root = tester
+        .withClusterFactory(
+            new Function<RelOptCluster, RelOptCluster>() {
+              public RelOptCluster apply(RelOptCluster cluster) {
+                cluster.setMetadataProvider(
+                    ChainedRelMetadataProvider.of(
+                        ImmutableList.of(BrokenColTypeImpl.SOURCE,
+                            cluster.getMetadataProvider())));
+                return cluster;
+              }
+            })
+        .convertSqlToRel(sql);
+
+    final RelNode rel = root.rel;
+    assertThat(rel, instanceOf(LogicalFilter.class));
+    final MyRelMetadataQuery mq = new MyRelMetadataQuery();
+
+    try {
+      assertThat(colType(mq, rel, 0), equalTo("DEPTNO-rel"));
+      fail("expected error");
+    } catch (IllegalArgumentException e) {
+      final String value = "No handler for method [public abstract java.lang.String "
+          + "org.apache.calcite.test.RelMetadataTest$ColType.getColType(int)] "
+          + "applied to argument of type [interface org.apache.calcite.rel.RelNode]; "
+          + "we recommend you create a catch-all (RelNode) handler";
+      assertThat(e.getMessage(), is(value));
+    }
+  }
+
+  public String colType(RelMetadataQuery mq, RelNode rel, int column) {
+    if (mq instanceof MyRelMetadataQuery) {
+      return ((MyRelMetadataQuery) mq).colType(rel, column);
+    } else {
+      return rel.metadata(ColType.class, mq).getColType(column);
+    }
+  }
+
   @Test public void testCustomProvider() {
     final List<String> buf = Lists.newArrayList();
     ColTypeImpl.THREAD_LIST.set(buf);
 
-    RelNode rel =
-        convertSql("select deptno, count(*) from emp where deptno > 10 "
-            + "group by deptno having count(*) = 0");
-    rel.getCluster().setMetadataProvider(
-        ChainedRelMetadataProvider.of(
-            ImmutableList.of(
-                ColTypeImpl.SOURCE, rel.getCluster().getMetadataProvider())));
+    final String sql = "select deptno, count(*) from emp where deptno > 10 "
+        + "group by deptno having count(*) = 0";
+    final RelRoot root = tester
+        .withClusterFactory(
+            new Function<RelOptCluster, RelOptCluster>() {
+              public RelOptCluster apply(RelOptCluster cluster) {
+                // Create a custom provider that includes ColType.
+                // Include the same provider twice just to be devious.
+                final ImmutableList<RelMetadataProvider> list =
+                    ImmutableList.of(ColTypeImpl.SOURCE, ColTypeImpl.SOURCE,
+                        cluster.getMetadataProvider());
+                cluster.setMetadataProvider(
+                    ChainedRelMetadataProvider.of(list));
+                return cluster;
+              }
+            })
+        .convertSqlToRel(sql);
+    final RelNode rel = root.rel;
 
     // Top node is a filter. Its metadata uses getColType(RelNode, int).
     assertThat(rel, instanceOf(LogicalFilter.class));
     final RelMetadataQuery mq = RelMetadataQuery.instance();
-    assertThat(rel.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-rel"));
-    assertThat(rel.metadata(ColType.class, mq).getColType(1),
-        equalTo("EXPR$1-rel"));
+    assertThat(colType(mq, rel, 0), equalTo("DEPTNO-rel"));
+    assertThat(colType(mq, rel, 1), equalTo("EXPR$1-rel"));
 
     // Next node is an aggregate. Its metadata uses
     // getColType(LogicalAggregate, int).
     final RelNode input = rel.getInput(0);
     assertThat(input, instanceOf(LogicalAggregate.class));
-    assertThat(input.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-agg"));
+    assertThat(colType(mq, input, 0), equalTo("DEPTNO-agg"));
 
     // There is no caching. Another request causes another call to the provider.
     assertThat(buf.toString(), equalTo("[DEPTNO-rel, EXPR$1-rel, DEPTNO-agg]"));
     assertThat(buf.size(), equalTo(3));
-    assertThat(input.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-agg"));
+    assertThat(colType(mq, input, 0), equalTo("DEPTNO-agg"));
     assertThat(buf.size(), equalTo(4));
 
     // Now add a cache. Only the first request for each piece of metadata
@@ -900,31 +954,24 @@ public class RelMetadataTest extends SqlToRelTestBase {
     rel.getCluster().setMetadataProvider(
         new CachingRelMetadataProvider(
             rel.getCluster().getMetadataProvider(), planner));
-    assertThat(input.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-agg"));
+    assertThat(colType(mq, input, 0), equalTo("DEPTNO-agg"));
     assertThat(buf.size(), equalTo(5));
-    assertThat(input.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-agg"));
+    assertThat(colType(mq, input, 0), equalTo("DEPTNO-agg"));
     assertThat(buf.size(), equalTo(5));
-    assertThat(input.metadata(ColType.class, mq).getColType(1),
-        equalTo("EXPR$1-agg"));
+    assertThat(colType(mq, input, 1), equalTo("EXPR$1-agg"));
     assertThat(buf.size(), equalTo(6));
-    assertThat(input.metadata(ColType.class, mq).getColType(1),
-        equalTo("EXPR$1-agg"));
+    assertThat(colType(mq, input, 1), equalTo("EXPR$1-agg"));
     assertThat(buf.size(), equalTo(6));
-    assertThat(input.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-agg"));
+    assertThat(colType(mq, input, 0), equalTo("DEPTNO-agg"));
     assertThat(buf.size(), equalTo(6));
 
     // With a different timestamp, a metadata item is re-computed on first call.
     long timestamp = planner.getRelMetadataTimestamp(rel);
     assertThat(timestamp, equalTo(0L));
     ((MockRelOptPlanner) planner).setRelMetadataTimestamp(timestamp + 1);
-    assertThat(input.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-agg"));
+    assertThat(colType(mq, input, 0), equalTo("DEPTNO-agg"));
     assertThat(buf.size(), equalTo(7));
-    assertThat(input.metadata(ColType.class, mq).getColType(0),
-        equalTo("DEPTNO-agg"));
+    assertThat(colType(mq, input, 0), equalTo("DEPTNO-agg"));
     assertThat(buf.size(), equalTo(7));
   }
 
@@ -1383,28 +1430,27 @@ public class RelMetadataTest extends SqlToRelTestBase {
 
   /** Custom metadata interface. */
   public interface ColType extends Metadata {
+    Method METHOD = Types.lookupMethod(ColType.class, "getColType", int.class);
+
+    MetadataDef<ColType> DEF =
+        MetadataDef.of(ColType.class, ColType.Handler.class, METHOD);
+
     String getColType(int column);
+
+    /** Handler API. */
+    interface Handler extends MetadataHandler<ColType> {
+      String getColType(RelNode r, RelMetadataQuery mq, int column);
+    }
   }
 
   /** A provider for {@link org.apache.calcite.test.RelMetadataTest.ColType} via
    * reflection. */
-  public static class ColTypeImpl implements MetadataHandler {
+  public abstract static class PartialColTypeImpl
+      implements MetadataHandler<ColType> {
     static final ThreadLocal<List<String>> THREAD_LIST = new ThreadLocal<>();
-    static final Method METHOD;
-    static {
-      try {
-        METHOD = ColType.class.getMethod("getColType", int.class);
-      } catch (NoSuchMethodException e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    public static final RelMetadataProvider SOURCE =
-        ReflectiveRelMetadataProvider.reflectiveSource(
-            METHOD, new ColTypeImpl());
 
-    public MetadataDef getDef() {
-      throw new UnsupportedOperationException();
+    public MetadataDef<ColType> getDef() {
+      return ColType.DEF;
     }
 
     /** Implementation of {@link ColType#getColType(int)} for
@@ -1417,6 +1463,13 @@ public class RelMetadataTest extends SqlToRelTestBase {
       THREAD_LIST.get().add(name);
       return name;
     }
+  }
+
+  /** A provider for {@link org.apache.calcite.test.RelMetadataTest.ColType} via
+   * reflection. */
+  public static class ColTypeImpl extends PartialColTypeImpl {
+    public static final RelMetadataProvider SOURCE =
+        ReflectiveRelMetadataProvider.reflectiveSource(ColType.METHOD, new ColTypeImpl());
 
     /** Implementation of {@link ColType#getColType(int)} for
      * {@link RelNode}, called via reflection. */
@@ -1428,6 +1481,35 @@ public class RelMetadataTest extends SqlToRelTestBase {
       return name;
     }
   }
+
+  /** Implementation of {@link ColType} that has no fall-back for {@link RelNode}. */
+  public static class BrokenColTypeImpl extends PartialColTypeImpl {
+    public static final RelMetadataProvider SOURCE =
+        ReflectiveRelMetadataProvider.reflectiveSource(ColType.METHOD,
+            new BrokenColTypeImpl());
+  }
+
+  /** Extension to {@link RelMetadataQuery} to support {@link ColType}.
+   *
+   * <p>Illustrates how you would package up a user-defined metadata type. */
+  private static class MyRelMetadataQuery extends RelMetadataQuery {
+    private ColType.Handler colTypeHandler;
+
+    public MyRelMetadataQuery() {
+      super(THREAD_PROVIDERS.get(), EMPTY);
+      colTypeHandler = initialHandler(ColType.Handler.class);
+    }
+
+    public String colType(RelNode rel, int column) {
+      for (;;) {
+        try {
+          return colTypeHandler.getColType(rel, this, column);
+        } catch (JaninoRelMetadataProvider.NoHandler e) {
+          colTypeHandler = revise(e.relClass, ColType.DEF);
+        }
+      }
+    }
+  }
 }
 
 // End RelMetadataTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 7bd85cb..9a2c3db 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -2036,7 +2036,7 @@ public class RelOptRulesTest extends RelOptTestBase {
    * Wrong collation trait in SortJoinTransposeRule for right joins</a>. */
   @Test public void testSortJoinTranspose4() {
     // Create a customized test with RelCollation trait in the test cluster.
-    Tester tester = new TesterImpl(getDiffRepos(), true, true, false, null) {
+    Tester tester = new TesterImpl(getDiffRepos(), true, true, false, null, null) {
       @Override public RelOptPlanner createPlanner() {
         return new MockRelOptPlanner() {
           @Override public List<RelTraitDef> getRelTraitDefs() {

http://git-wip-us.apache.org/repos/asf/calcite/blob/94f8837c/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
----------------------------------------------------------------------
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
index 7e818af..3d7f609 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelTestBase.java
@@ -92,7 +92,7 @@ public abstract class SqlToRelTestBase {
   }
 
   protected Tester createTester() {
-    return new TesterImpl(getDiffRepos(), false, false, true, null);
+    return new TesterImpl(getDiffRepos(), false, false, true, null, null);
   }
 
   /**
@@ -221,6 +221,8 @@ public abstract class SqlToRelTestBase {
 
     /** Returns a tester that optionally trims unused fields. */
     Tester withTrim(boolean enable);
+
+    Tester withClusterFactory(Function<RelOptCluster, RelOptCluster> function);
   }
 
   //~ Inner Classes ----------------------------------------------------------
@@ -463,6 +465,7 @@ public abstract class SqlToRelTestBase {
     private final boolean enableExpand;
     private final Function<RelDataTypeFactory, Prepare.CatalogReader>
     catalogReaderFactory;
+    private final Function<RelOptCluster, RelOptCluster> clusterFactory;
     private RelDataTypeFactory typeFactory;
 
     /**
@@ -473,16 +476,19 @@ public abstract class SqlToRelTestBase {
      * @param enableTrim Whether to trim unused fields
      * @param enableExpand Whether to expand sub-queries
      * @param catalogReaderFactory Function to create catalog reader, or null
+     * @param clusterFactory Called after a cluster has been created
      */
     protected TesterImpl(DiffRepository diffRepos, boolean enableDecorrelate,
         boolean enableTrim, boolean enableExpand,
         Function<RelDataTypeFactory, Prepare.CatalogReader>
-            catalogReaderFactory) {
+            catalogReaderFactory,
+        Function<RelOptCluster, RelOptCluster> clusterFactory) {
       this.diffRepos = diffRepos;
       this.enableDecorrelate = enableDecorrelate;
       this.enableTrim = enableTrim;
       this.enableExpand = enableExpand;
       this.catalogReaderFactory = catalogReaderFactory;
+      this.clusterFactory = clusterFactory;
     }
 
     public RelRoot convertSqlToRel(String sql) {
@@ -528,8 +534,11 @@ public abstract class SqlToRelTestBase {
         final Prepare.CatalogReader catalogReader,
         final RelDataTypeFactory typeFactory) {
       final RexBuilder rexBuilder = new RexBuilder(typeFactory);
-      final RelOptCluster cluster =
+      RelOptCluster cluster =
           RelOptCluster.create(getPlanner(), rexBuilder);
+      if (clusterFactory != null) {
+        cluster = clusterFactory.apply(cluster);
+      }
       return new SqlToRelConverter(null, validator, catalogReader, cluster,
           StandardConvertletTable.INSTANCE);
     }
@@ -659,27 +668,33 @@ public abstract class SqlToRelTestBase {
       return this.enableDecorrelate == enable
           ? this
           : new TesterImpl(diffRepos, enable, enableTrim, enableExpand,
-              catalogReaderFactory);
+              catalogReaderFactory, clusterFactory);
     }
 
     public Tester withTrim(boolean enable) {
       return this.enableTrim == enable
           ? this
           : new TesterImpl(diffRepos, enableDecorrelate, enable, enableExpand,
-              catalogReaderFactory);
+              catalogReaderFactory, clusterFactory);
     }
 
     public Tester withExpand(boolean expand) {
       return this.enableExpand == expand
           ? this
           : new TesterImpl(diffRepos, enableDecorrelate, enableTrim, expand,
-              catalogReaderFactory);
+              catalogReaderFactory, clusterFactory);
     }
 
     public Tester withCatalogReaderFactory(
         Function<RelDataTypeFactory, Prepare.CatalogReader> factory) {
       return new TesterImpl(diffRepos, enableDecorrelate, false, enableExpand,
-          factory);
+          factory, clusterFactory);
+    }
+
+    public Tester withClusterFactory(
+        Function<RelOptCluster, RelOptCluster> clusterFactory) {
+      return new TesterImpl(diffRepos, enableDecorrelate, false, enableExpand,
+          catalogReaderFactory, clusterFactory);
     }
   }