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 2015/04/03 15:02:36 UTC

[03/10] incubator-calcite git commit: [CALCITE-652] Move server pieces of avatica into avatica-server (Nick Dimiduk)

[CALCITE-652] Move server pieces of avatica into avatica-server (Nick Dimiduk)

Close apache/incubator-calcite#69


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

Branch: refs/heads/master
Commit: fa9bdc4ac8b7d0106ebf5fa875cd9edc00d47865
Parents: 17d7a8e
Author: Nick Dimiduk <nd...@gmail.com>
Authored: Tue Mar 31 16:04:43 2015 -0700
Committer: Julian Hyde <jh...@apache.org>
Committed: Fri Apr 3 01:09:29 2015 -0700

----------------------------------------------------------------------
 avatica-server/pom.xml                          |  47 +-
 .../apache/calcite/avatica/jdbc/JdbcMeta.java   | 765 +++++++++++++++++++
 .../calcite/avatica/jdbc/JdbcResultSet.java     | 106 +++
 .../calcite/avatica/jdbc/package-info.java      |  22 +
 .../calcite/avatica/RemoteDriverTest.java       | 431 +++++++++++
 .../calcite/avatica/test/AvaticaSuite.java      |  36 +
 avatica/pom.xml                                 |  35 +-
 .../apache/calcite/avatica/jdbc/JdbcMeta.java   | 765 -------------------
 .../calcite/avatica/jdbc/JdbcResultSet.java     | 106 ---
 .../calcite/avatica/jdbc/package-info.java      |  22 -
 .../calcite/avatica/test/AvaticaSuite.java      |  33 -
 .../calcite/avatica/test/RemoteDriverTest.java  | 435 -----------
 pom.xml                                         |  41 +-
 13 files changed, 1461 insertions(+), 1383 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica-server/pom.xml
----------------------------------------------------------------------
diff --git a/avatica-server/pom.xml b/avatica-server/pom.xml
index 4b35978..2da32ec 100644
--- a/avatica-server/pom.xml
+++ b/avatica-server/pom.xml
@@ -40,11 +40,56 @@ limitations under the License.
       <groupId>org.apache.calcite</groupId>
       <artifactId>calcite-avatica</artifactId>
     </dependency>
-
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+    </dependency>
     <dependency>
       <groupId>org.eclipse.jetty</groupId>
       <artifactId>jetty-server</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-util</artifactId>
+    </dependency>
+
+    <!-- test dependencies -->
+    <dependency>
+      <groupId>org.apache.calcite</groupId>
+      <artifactId>calcite-avatica</artifactId>
+      <type>test-jar</type>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <!-- Used in RemoteDriverTest but dependency not detected by maven-dependency-plugin:2.8:analyze -->
+      <groupId>net.hydromatic</groupId>
+      <artifactId>scott-data-hsqldb</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <!-- Used in RemoteDriverTest but dependency not detected by maven-dependency-plugin:2.8:analyze -->
+      <groupId>org.hsqldb</groupId>
+      <artifactId>hsqldb</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
----------------------------------------------------------------------
diff --git a/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java b/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
new file mode 100644
index 0000000..90e8c2a
--- /dev/null
+++ b/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
@@ -0,0 +1,765 @@
+/*
+ * 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.avatica.jdbc;
+
+import org.apache.calcite.avatica.AvaticaParameter;
+import org.apache.calcite.avatica.ColumnMetaData;
+import org.apache.calcite.avatica.Meta;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Type;
+import java.math.BigDecimal;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+/** Implementation of {@link Meta} upon an existing JDBC data source. */
+public class JdbcMeta implements Meta {
+
+  private static final Log LOG = LogFactory.getLog(JdbcMeta.class);
+
+  /**
+   * JDBC Types Mapped to Java Types
+   *
+   * @see <a href="https://docs.oracle.com/javase/1.5.0/docs/guide/jdbc/getstart/mapping.html#1051555">JDBC Types Mapped to Java Types</a>
+   */
+  protected static final Map<Integer, Type> SQL_TYPE_TO_JAVA_TYPE =
+      new HashMap<>();
+  static {
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.CHAR, String.class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.VARCHAR, String.class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.LONGNVARCHAR, String.class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.NUMERIC, BigDecimal.class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.DECIMAL, BigDecimal.class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.BIT, Boolean.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.TINYINT, Byte.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.SMALLINT, Short.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.INTEGER, Integer.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.BIGINT, Long.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.REAL, Float.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.FLOAT, Double.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.DOUBLE, Double.TYPE);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.BINARY, byte[].class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.VARBINARY, byte[].class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.LONGVARBINARY, byte[].class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.DATE, java.sql.Date.class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.TIME, java.sql.Time.class);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.TIMESTAMP, java.sql.Timestamp.class);
+    //put(Types.CLOB, Clob);
+    //put(Types.BLOB, Blob);
+    SQL_TYPE_TO_JAVA_TYPE.put(Types.ARRAY, Array.class);
+  }
+
+  /** A "magic column" descriptor used to return statement execute update count via a
+   * {@link Meta.MetaResultSet}. */
+  private static final ColumnMetaData UPDATE_COL = new ColumnMetaData(1, false, false, false,
+      false, ResultSetMetaData.columnNoNulls, true, 10, "u", "u", null, 15, 15, "update_result",
+      "avatica_internal", ColumnMetaData.scalar(Types.INTEGER, "INTEGER",
+        ColumnMetaData.Rep.INTEGER), true, false, false, "java.lang.Integer");
+
+  private static final String CONN_CACHE_KEY_BASE = "avatica.connectioncache";
+
+  /** Configurable connection cache settings. */
+  public enum ConnectionCacheSettings {
+    /** JDBC connection property for setting connection cache concurrency level. */
+    CONCURRENCY_LEVEL(CONN_CACHE_KEY_BASE + ".concurrency", "10"),
+
+    /** JDBC connection property for setting connection cache initial capacity. */
+    INITIAL_CAPACITY(CONN_CACHE_KEY_BASE + ".initialcapacity", "100"),
+
+    /** JDBC connection property for setting connection cache maximum capacity. */
+    MAX_CAPACITY(CONN_CACHE_KEY_BASE + ".maxcapacity", "1000"),
+
+    /** JDBC connection property for setting connection cache expiration duration. */
+    EXPIRY_DURATION(CONN_CACHE_KEY_BASE + ".expirydiration", "10"),
+
+    /** JDBC connection property for setting connection cache expiration unit. */
+    EXPIRY_UNIT(CONN_CACHE_KEY_BASE + ".expiryunit", TimeUnit.MINUTES.name());
+
+    private final String key;
+    private final String defaultValue;
+
+    private ConnectionCacheSettings(String key, String defaultValue) {
+      this.key = key;
+      this.defaultValue = defaultValue;
+    }
+
+    /** The configuration key for specifying this setting. */
+    public String key() {
+      return key;
+    }
+
+    /** The default value for this setting. */
+    public String defaultValue() {
+      return defaultValue;
+    }
+  }
+
+  private static final String STMT_CACHE_KEY_BASE = "avatica.statementcache";
+
+  /** Configurable statement cache settings. */
+  public enum StatementCacheSettings {
+    /** JDBC connection property for setting connection cache concurrency level. */
+    CONCURRENCY_LEVEL(STMT_CACHE_KEY_BASE + ".concurrency", "100"),
+
+    /** JDBC connection property for setting connection cache initial capacity. */
+    INITIAL_CAPACITY(STMT_CACHE_KEY_BASE + ".initialcapacity", "1000"),
+
+    /** JDBC connection property for setting connection cache maximum capacity. */
+    MAX_CAPACITY(STMT_CACHE_KEY_BASE + ".maxcapacity", "10000"),
+
+    /** JDBC connection property for setting connection cache expiration duration.
+     *
+     * <p>Used in conjunction with {@link #EXPIRY_UNIT}.</p>
+     */
+    EXPIRY_DURATION(STMT_CACHE_KEY_BASE + ".expirydiration", "5"),
+
+    /** JDBC connection property for setting connection cache expiration unit.
+     *
+     * <p>Used in conjunction with {@link #EXPIRY_DURATION}.</p>
+     */
+    EXPIRY_UNIT(STMT_CACHE_KEY_BASE + ".expiryunit", TimeUnit.MINUTES.name());
+
+    private final String key;
+    private final String defaultValue;
+
+    private StatementCacheSettings(String key, String defaultValue) {
+      this.key = key;
+      this.defaultValue = defaultValue;
+    }
+
+    /** The configuration key for specifying this setting. */
+    public String key() {
+      return key;
+    }
+
+    /** The default value for this setting. */
+    public String defaultValue() {
+      return defaultValue;
+    }
+  }
+
+  private static final String DEFAULT_CONN_ID =
+      UUID.fromString("00000000-0000-0000-0000-000000000000").toString();
+
+  private final String url;
+  private final Properties info;
+  private final Connection connection; // TODO: remove default connection
+  private final Cache<String, Connection> connectionCache;
+  private final Cache<Integer, StatementInfo> statementCache;
+
+  /**
+   * Convert from JDBC metadata to Avatica columns.
+   */
+  protected static List<ColumnMetaData>
+  columns(ResultSetMetaData metaData) throws SQLException {
+    final List<ColumnMetaData> columns = new ArrayList<>();
+    for (int i = 1; i <= metaData.getColumnCount(); i++) {
+      final Type javaType =
+          SQL_TYPE_TO_JAVA_TYPE.get(metaData.getColumnType(i));
+      ColumnMetaData.AvaticaType t =
+          ColumnMetaData.scalar(metaData.getColumnType(i),
+              metaData.getColumnTypeName(i), ColumnMetaData.Rep.of(javaType));
+      ColumnMetaData md =
+          new ColumnMetaData(i - 1, metaData.isAutoIncrement(i),
+              metaData.isCaseSensitive(i), metaData.isSearchable(i),
+              metaData.isCurrency(i), metaData.isNullable(i),
+              metaData.isSigned(i), metaData.getColumnDisplaySize(i),
+              metaData.getColumnLabel(i), metaData.getColumnName(i),
+              metaData.getSchemaName(i), metaData.getPrecision(i),
+              metaData.getScale(i), metaData.getTableName(i),
+              metaData.getCatalogName(i), t, metaData.isReadOnly(i),
+              metaData.isWritable(i), metaData.isDefinitelyWritable(i),
+              metaData.getColumnClassName(i));
+      columns.add(md);
+    }
+    return columns;
+  }
+
+  /**
+   * Converts from JDBC metadata to AvaticaParameters
+   */
+  protected static List<AvaticaParameter> parameters(ParameterMetaData metaData)
+      throws SQLException {
+    if (metaData == null) {
+      return Collections.emptyList();
+    }
+    final List<AvaticaParameter> params = new ArrayList<>();
+    for (int i = 1; i <= metaData.getParameterCount(); i++) {
+      params.add(
+          new AvaticaParameter(metaData.isSigned(i), metaData.getPrecision(i),
+              metaData.getScale(i), metaData.getParameterType(i),
+              metaData.getParameterTypeName(i),
+              metaData.getParameterClassName(i), "?" + i));
+    }
+    return params;
+  }
+
+  protected static Signature signature(ResultSetMetaData metaData,
+      ParameterMetaData parameterMetaData, String sql) throws  SQLException {
+    return new Signature(columns(metaData), sql, parameters(parameterMetaData),
+        null, CursorFactory.LIST /* LIST because JdbcResultSet#frame */);
+  }
+
+  protected static Signature signature(ResultSetMetaData metaData)
+      throws SQLException {
+    return signature(metaData, null, null);
+  }
+
+  /** Callback for {@link #connectionCache} member expiration. */
+  private class ConnectionExpiryHandler
+      implements RemovalListener<String, Connection> {
+
+    public void onRemoval(RemovalNotification<String, Connection> notification) {
+      String connectionId = notification.getKey();
+      Connection doomed = notification.getValue();
+      // is String.equals() more efficient?
+      if (notification.getValue() == connection) {
+        return;
+      }
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("Expiring connection " + connectionId + " because "
+            + notification.getCause());
+      }
+      try {
+        if (doomed != null) {
+          doomed.close();
+        }
+      } catch (Throwable t) {
+        LOG.info("Exception thrown while expiring connection " + connectionId, t);
+      }
+    }
+  }
+
+  /** Callback for {@link #statementCache} member expiration. */
+  private class StatementExpiryHandler
+      implements RemovalListener<Integer, StatementInfo> {
+    public void onRemoval(RemovalNotification<Integer, StatementInfo> notification) {
+      Integer stmtId = notification.getKey();
+      StatementInfo doomed = notification.getValue();
+      if (doomed == null) {
+        // log/throw?
+        return;
+      }
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("Expiring statement " + stmtId + " because "
+            + notification.getCause());
+      }
+      try {
+        if (doomed.resultSet != null) {
+          doomed.resultSet.close();
+        }
+        if (doomed.statement != null) {
+          doomed.statement.close();
+        }
+      } catch (Throwable t) {
+        LOG.info("Exception thrown while expiring statement " + stmtId);
+      }
+    }
+  }
+
+  /**
+   * @param url a database url of the form
+   *  <code> jdbc:<em>subprotocol</em>:<em>subname</em></code>
+   */
+  public JdbcMeta(String url) throws SQLException {
+    this(url, new Properties());
+  }
+
+  /**
+   * @param url a database url of the form
+   * <code>jdbc:<em>subprotocol</em>:<em>subname</em></code>
+   * @param user the database user on whose behalf the connection is being
+   *   made
+   * @param password the user's password
+   */
+  public JdbcMeta(final String url, final String user, final String password)
+      throws SQLException {
+    this(url, new Properties() {
+      {
+        put("user", user);
+        put("password", password);
+      }
+    });
+  }
+
+  /**
+   * @param url a database url of the form
+   * <code> jdbc:<em>subprotocol</em>:<em>subname</em></code>
+   * @param info a list of arbitrary string tag/value pairs as
+   * connection arguments; normally at least a "user" and
+   * "password" property should be included
+   */
+  public JdbcMeta(String url, Properties info) throws SQLException {
+    this.url = url;
+    this.info = info;
+    this.connection = DriverManager.getConnection(url, info);
+
+    int concurrencyLevel = Integer.parseInt(
+        info.getProperty(ConnectionCacheSettings.CONCURRENCY_LEVEL.key(),
+            ConnectionCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
+    int initialCapacity = Integer.parseInt(
+        info.getProperty(ConnectionCacheSettings.INITIAL_CAPACITY.key(),
+            ConnectionCacheSettings.INITIAL_CAPACITY.defaultValue()));
+    long maxCapacity = Long.parseLong(
+        info.getProperty(ConnectionCacheSettings.MAX_CAPACITY.key(),
+            ConnectionCacheSettings.MAX_CAPACITY.defaultValue()));
+    long connectionExpiryDuration = Long.parseLong(
+        info.getProperty(ConnectionCacheSettings.EXPIRY_DURATION.key(),
+            ConnectionCacheSettings.EXPIRY_DURATION.defaultValue()));
+    TimeUnit connectionExpiryUnit = TimeUnit.valueOf(
+        info.getProperty(ConnectionCacheSettings.EXPIRY_UNIT.key(),
+            ConnectionCacheSettings.EXPIRY_UNIT.defaultValue()));
+    this.connectionCache = CacheBuilder.newBuilder()
+        .concurrencyLevel(concurrencyLevel)
+        .initialCapacity(initialCapacity)
+        .maximumSize(maxCapacity)
+        .expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
+        .removalListener(new ConnectionExpiryHandler())
+        .build();
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("instantiated connection cache: " + connectionCache.stats());
+    }
+
+    concurrencyLevel = Integer.parseInt(
+        info.getProperty(StatementCacheSettings.CONCURRENCY_LEVEL.key(),
+            StatementCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
+    initialCapacity = Integer.parseInt(
+        info.getProperty(StatementCacheSettings.INITIAL_CAPACITY.key(),
+            StatementCacheSettings.INITIAL_CAPACITY.defaultValue()));
+    maxCapacity = Long.parseLong(
+        info.getProperty(StatementCacheSettings.MAX_CAPACITY.key(),
+            StatementCacheSettings.MAX_CAPACITY.defaultValue()));
+    connectionExpiryDuration = Long.parseLong(
+        info.getProperty(StatementCacheSettings.EXPIRY_DURATION.key(),
+            StatementCacheSettings.EXPIRY_DURATION.defaultValue()));
+    connectionExpiryUnit = TimeUnit.valueOf(
+        info.getProperty(StatementCacheSettings.EXPIRY_UNIT.key(),
+            StatementCacheSettings.EXPIRY_UNIT.defaultValue()));
+    this.statementCache = CacheBuilder.newBuilder()
+        .concurrencyLevel(concurrencyLevel)
+        .initialCapacity(initialCapacity)
+        .maximumSize(maxCapacity)
+        .expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
+        .removalListener(new StatementExpiryHandler())
+        .build();
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("instantiated statement cache: " + statementCache.stats());
+    }
+  }
+
+  public String getSqlKeywords() {
+    try {
+      return connection.getMetaData().getSQLKeywords();
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public String getNumericFunctions() {
+    try {
+      return connection.getMetaData().getNumericFunctions();
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public String getStringFunctions() {
+    return null;
+  }
+
+  public String getSystemFunctions() {
+    return null;
+  }
+
+  public String getTimeDateFunctions() {
+    return null;
+  }
+
+  public MetaResultSet getTables(String catalog, Pat schemaPattern,
+      Pat tableNamePattern, List<String> typeList) {
+    try {
+      String[] types = new String[typeList == null ? 0 : typeList.size()];
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getTables(catalog, schemaPattern.s,
+              tableNamePattern.s,
+              typeList == null ? types : typeList.toArray(types)));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getColumns(String catalog, Pat schemaPattern,
+      Pat tableNamePattern, Pat columnNamePattern) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getColumns(catalog, schemaPattern.s,
+              tableNamePattern.s, columnNamePattern.s));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getSchemas(String catalog, Pat schemaPattern) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getSchemas(catalog, schemaPattern.s));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getCatalogs() {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getCatalogs());
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getTableTypes() {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getTableTypes());
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getProcedures(String catalog, Pat schemaPattern,
+      Pat procedureNamePattern) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getProcedures(catalog, schemaPattern.s,
+              procedureNamePattern.s));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getProcedureColumns(String catalog, Pat schemaPattern,
+      Pat procedureNamePattern, Pat columnNamePattern) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getProcedureColumns(catalog,
+              schemaPattern.s, procedureNamePattern.s, columnNamePattern.s));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getColumnPrivileges(String catalog, String schema,
+      String table, Pat columnNamePattern) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getColumnPrivileges(catalog, schema,
+              table, columnNamePattern.s));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getTablePrivileges(String catalog, Pat schemaPattern,
+      Pat tableNamePattern) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getTablePrivileges(catalog,
+              schemaPattern.s, tableNamePattern.s));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getBestRowIdentifier(String catalog, String schema,
+      String table, int scope, boolean nullable) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getBestRowIdentifier(catalog, schema,
+              table, scope, nullable));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getVersionColumns(String catalog, String schema,
+      String table) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getVersionColumns(catalog, schema, table));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getPrimaryKeys(String catalog, String schema,
+      String table) {
+    try {
+      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
+          connection.getMetaData().getPrimaryKeys(catalog, schema, table));
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public MetaResultSet getImportedKeys(String catalog, String schema,
+      String table) {
+    return null;
+  }
+
+  public MetaResultSet getExportedKeys(String catalog, String schema,
+      String table) {
+    return null;
+  }
+
+  public MetaResultSet getCrossReference(String parentCatalog,
+      String parentSchema, String parentTable, String foreignCatalog,
+      String foreignSchema, String foreignTable) {
+    return null;
+  }
+
+  public MetaResultSet getTypeInfo() {
+    return null;
+  }
+
+  public MetaResultSet getIndexInfo(String catalog, String schema, String table,
+      boolean unique, boolean approximate) {
+    return null;
+  }
+
+  public MetaResultSet getUDTs(String catalog, Pat schemaPattern,
+      Pat typeNamePattern, int[] types) {
+    return null;
+  }
+
+  public MetaResultSet getSuperTypes(String catalog, Pat schemaPattern,
+      Pat typeNamePattern) {
+    return null;
+  }
+
+  public MetaResultSet getSuperTables(String catalog, Pat schemaPattern,
+      Pat tableNamePattern) {
+    return null;
+  }
+
+  public MetaResultSet getAttributes(String catalog, Pat schemaPattern,
+      Pat typeNamePattern, Pat attributeNamePattern) {
+    return null;
+  }
+
+  public MetaResultSet getClientInfoProperties() {
+    return null;
+  }
+
+  public MetaResultSet getFunctions(String catalog, Pat schemaPattern,
+      Pat functionNamePattern) {
+    return null;
+  }
+
+  public MetaResultSet getFunctionColumns(String catalog, Pat schemaPattern,
+      Pat functionNamePattern, Pat columnNamePattern) {
+    return null;
+  }
+
+  public MetaResultSet getPseudoColumns(String catalog, Pat schemaPattern,
+      Pat tableNamePattern, Pat columnNamePattern) {
+    return null;
+  }
+
+  public Iterable<Object> createIterable(StatementHandle handle,
+      Signature signature, List<Object> parameterValues, Frame firstFrame) {
+    return null;
+  }
+
+  protected Connection getConnection(String id) throws SQLException {
+    Connection conn = connectionCache.getIfPresent(id);
+    if (conn == null) {
+      conn = DriverManager.getConnection(url, info);
+      connectionCache.put(id, conn);
+    }
+    return conn;
+  }
+
+  public StatementHandle createStatement(ConnectionHandle ch) {
+    try {
+      final Connection conn = getConnection(ch.id);
+      final Statement statement = conn.createStatement();
+      final int id = System.identityHashCode(statement);
+      statementCache.put(id, new StatementInfo(statement));
+      StatementHandle h = new StatementHandle(ch.id, id, null);
+      if (LOG.isTraceEnabled()) {
+        LOG.trace("created statement " + h);
+      }
+      return h;
+    } catch (SQLException e) {
+      throw propagate(e);
+    }
+  }
+
+  @Override public void closeStatement(StatementHandle h) {
+    StatementInfo info = statementCache.getIfPresent(h.id);
+    if (info == null || info.statement == null) {
+      LOG.debug("client requested close unknown statement " + h);
+      return;
+    }
+    if (LOG.isTraceEnabled()) {
+      LOG.trace("closing statement " + h);
+    }
+    try {
+      if (info.resultSet != null) {
+        info.resultSet.close();
+      }
+      info.statement.close();
+    } catch (SQLException e) {
+      throw propagate(e);
+    } finally {
+      statementCache.invalidate(h.id);
+    }
+  }
+
+  @Override public void closeConnection(ConnectionHandle ch) {
+    Connection conn = connectionCache.getIfPresent(ch.id);
+    if (conn == null) {
+      LOG.debug("client requested close unknown connection " + ch);
+      return;
+    }
+    if (LOG.isTraceEnabled()) {
+      LOG.trace("closing connection " + ch);
+    }
+    try {
+      conn.close();
+    } catch (SQLException e) {
+      throw propagate(e);
+    } finally {
+      connectionCache.invalidate(ch.id);
+    }
+  }
+
+  private RuntimeException propagate(Throwable e) {
+    if (e instanceof RuntimeException) {
+      throw (RuntimeException) e;
+    } else if (e instanceof Error) {
+      throw (Error) e;
+    } else {
+      throw new RuntimeException(e);
+    }
+  }
+
+  public StatementHandle prepare(ConnectionHandle ch, String sql,
+      int maxRowCount) {
+    try {
+      final Connection conn = getConnection(ch.id);
+      final PreparedStatement statement = conn.prepareStatement(sql);
+      final int id = System.identityHashCode(statement);
+      statementCache.put(id, new StatementInfo(statement));
+      StatementHandle h = new StatementHandle(ch.id, id,
+          signature(statement.getMetaData(), statement.getParameterMetaData(),
+              sql));
+      if (LOG.isTraceEnabled()) {
+        LOG.trace("prepared statement " + h);
+      }
+      return h;
+    } catch (SQLException e) {
+      throw propagate(e);
+    }
+  }
+
+  public MetaResultSet prepareAndExecute(ConnectionHandle ch, String sql,
+      int maxRowCount, PrepareCallback callback) {
+    try {
+      final Connection connection = getConnection(ch.id);
+      final PreparedStatement statement = connection.prepareStatement(sql);
+      final int id = System.identityHashCode(statement);
+      final StatementInfo info = new StatementInfo(statement);
+      statementCache.put(id, info);
+      info.resultSet = statement.executeQuery();
+      MetaResultSet mrs = JdbcResultSet.create(ch.id, id, info.resultSet);
+      if (LOG.isTraceEnabled()) {
+        StatementHandle h = new StatementHandle(ch.id, id, null);
+        LOG.trace("prepAndExec statement " + h);
+      }
+      return mrs;
+    } catch (SQLException e) {
+      throw propagate(e);
+    }
+  }
+
+  public Frame fetch(StatementHandle h, List<Object> parameterValues,
+      int offset, int fetchMaxRowCount) {
+    if (LOG.isTraceEnabled()) {
+      LOG.trace("fetching " + h + " offset:" + offset + " fetchMaxRowCount:" + fetchMaxRowCount);
+    }
+    try {
+      final StatementInfo statementInfo = Objects.requireNonNull(
+          statementCache.getIfPresent(h.id),
+          "Statement not found, potentially expired. " + h);
+      if (statementInfo.resultSet == null || parameterValues != null) {
+        if (statementInfo.resultSet != null) {
+          statementInfo.resultSet.close();
+        }
+        final PreparedStatement preparedStatement =
+            (PreparedStatement) statementInfo.statement;
+        if (parameterValues != null) {
+          for (int i = 0; i < parameterValues.size(); i++) {
+            Object o = parameterValues.get(i);
+            preparedStatement.setObject(i + 1, o);
+          }
+        }
+        statementInfo.resultSet = preparedStatement.executeQuery();
+      }
+      return JdbcResultSet.frame(statementInfo.resultSet, offset,
+          fetchMaxRowCount);
+    } catch (SQLException e) {
+      throw propagate(e);
+    }
+  }
+
+  /** All we know about a statement. */
+  private static class StatementInfo {
+    final Statement statement; // sometimes a PreparedStatement
+    ResultSet resultSet;
+
+    private StatementInfo(Statement statement) {
+      this.statement = Objects.requireNonNull(statement);
+    }
+  }
+}
+
+// End JdbcMeta.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java
----------------------------------------------------------------------
diff --git a/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java b/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java
new file mode 100644
index 0000000..827f31d
--- /dev/null
+++ b/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcResultSet.java
@@ -0,0 +1,106 @@
+/*
+ * 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.avatica.jdbc;
+
+import org.apache.calcite.avatica.Meta;
+
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Implementation of {@link org.apache.calcite.avatica.Meta.MetaResultSet}
+ *  upon a JDBC {@link java.sql.ResultSet}.
+ *
+ *  @see org.apache.calcite.avatica.jdbc.JdbcMeta */
+class JdbcResultSet extends Meta.MetaResultSet {
+  protected JdbcResultSet(String connectionId, int statementId,
+      boolean ownStatement, Meta.Signature signature, Meta.Frame firstFrame) {
+    super(connectionId, statementId, ownStatement, signature, firstFrame);
+  }
+
+  /** Creates a result set. */
+  public static JdbcResultSet create(String connectionId, int statementId,
+      ResultSet resultSet) {
+    try {
+      Meta.Signature sig = JdbcMeta.signature(resultSet.getMetaData());
+      final Meta.Frame firstFrame = frame(resultSet, 0, -1);
+      resultSet.close();
+      return new JdbcResultSet(connectionId, statementId, true, sig,
+          firstFrame);
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /** Creates a frame containing a given number or unlimited number of rows
+   * from a result set. */
+  static Meta.Frame frame(ResultSet resultSet, int offset,
+      int fetchMaxRowCount) throws SQLException {
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    final int columnCount = metaData.getColumnCount();
+    final int[] types = new int[columnCount];
+    for (int i = 0; i < types.length; i++) {
+      types[i] = metaData.getColumnType(i + 1);
+    }
+    final List<Object> rows = new ArrayList<>();
+    boolean done = false;
+    for (int i = 0; fetchMaxRowCount < 0 || i < fetchMaxRowCount; i++) {
+      if (!resultSet.next()) {
+        done = true;
+        break;
+      }
+      Object[] columns = new Object[columnCount];
+      for (int j = 0; j < columnCount; j++) {
+        columns[j] = getValue(resultSet, types[j], j);
+      }
+      rows.add(columns);
+    }
+    return new Meta.Frame(offset, done, rows);
+  }
+
+  private static Object getValue(ResultSet resultSet, int type, int j)
+      throws SQLException {
+    switch (type) {
+    case Types.BIGINT:
+      final long aLong = resultSet.getLong(j + 1);
+      return aLong == 0 && resultSet.wasNull() ? null : aLong;
+    case Types.INTEGER:
+      final int anInt = resultSet.getInt(j + 1);
+      return anInt == 0 && resultSet.wasNull() ? null : anInt;
+    case Types.SMALLINT:
+      final short aShort = resultSet.getShort(j + 1);
+      return aShort == 0 && resultSet.wasNull() ? null : aShort;
+    case Types.TINYINT:
+      final byte aByte = resultSet.getByte(j + 1);
+      return aByte == 0 && resultSet.wasNull() ? null : aByte;
+    case Types.DOUBLE:
+    case Types.FLOAT:
+      final double aDouble = resultSet.getDouble(j + 1);
+      return aDouble == 0D && resultSet.wasNull() ? null : aDouble;
+    case Types.REAL:
+      final float aFloat = resultSet.getFloat(j + 1);
+      return aFloat == 0D && resultSet.wasNull() ? null : aFloat;
+    default:
+      return resultSet.getObject(j + 1);
+    }
+  }
+}
+
+// End JdbcResultSet.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/package-info.java
----------------------------------------------------------------------
diff --git a/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/package-info.java b/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/package-info.java
new file mode 100644
index 0000000..8b8fb76
--- /dev/null
+++ b/avatica-server/src/main/java/org/apache/calcite/avatica/jdbc/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * 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.
+ */
+
+/** Implements an Avatica provider on top of an existing JDBC data source. */
+package org.apache.calcite.avatica.jdbc;
+
+
+// End package-info.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica-server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
----------------------------------------------------------------------
diff --git a/avatica-server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java b/avatica-server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
new file mode 100644
index 0000000..a2ab87a
--- /dev/null
+++ b/avatica-server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
@@ -0,0 +1,431 @@
+/*
+ * 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.avatica;
+
+import org.apache.calcite.avatica.jdbc.JdbcMeta;
+import org.apache.calcite.avatica.remote.LocalJsonService;
+import org.apache.calcite.avatica.remote.LocalService;
+import org.apache.calcite.avatica.remote.MockJsonService;
+import org.apache.calcite.avatica.remote.Service;
+
+import com.google.common.cache.Cache;
+
+import net.hydromatic.scott.data.hsqldb.ScottHsqldb;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ParameterMetaData;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit test for Avatica Remote JDBC driver.
+ */
+public class RemoteDriverTest {
+  public static final String MJS =
+      MockJsonService.Factory.class.getName();
+
+  public static final String LJS =
+      LocalJdbcServiceFactory.class.getName();
+
+  public static final String QRJS =
+      QuasiRemoteJdbcServiceFactory.class.getName();
+
+  private static final ConnectionSpec CONNECTION_SPEC = ConnectionSpec.HSQLDB;
+
+  private Connection mjs() throws SQLException {
+    return DriverManager.getConnection("jdbc:avatica:remote:factory=" + MJS);
+  }
+
+  private Connection ljs() throws SQLException {
+    return DriverManager.getConnection("jdbc:avatica:remote:factory=" + QRJS);
+  }
+
+  @Before
+  public void before() throws Exception {
+    QuasiRemoteJdbcServiceFactory.initService();
+  }
+
+  @Test public void testRegister() throws Exception {
+    final Connection connection =
+        DriverManager.getConnection("jdbc:avatica:remote:");
+    assertThat(connection.isClosed(), is(false));
+    connection.close();
+    assertThat(connection.isClosed(), is(true));
+  }
+
+  @Test public void testSchemas() throws Exception {
+    final Connection connection = mjs();
+    final ResultSet resultSet =
+        connection.getMetaData().getSchemas(null, null);
+    assertFalse(resultSet.next());
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    assertTrue(metaData.getColumnCount() >= 2);
+    assertEquals("TABLE_CATALOG", metaData.getColumnName(1));
+    assertEquals("TABLE_SCHEM", metaData.getColumnName(2));
+    resultSet.close();
+    connection.close();
+  }
+
+  @Test public void testTables() throws Exception {
+    final Connection connection = mjs();
+    final ResultSet resultSet =
+        connection.getMetaData().getTables(null, null, null, new String[0]);
+    assertFalse(resultSet.next());
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    assertTrue(metaData.getColumnCount() >= 3);
+    assertEquals("TABLE_CAT", metaData.getColumnName(1));
+    assertEquals("TABLE_SCHEM", metaData.getColumnName(2));
+    assertEquals("TABLE_NAME", metaData.getColumnName(3));
+    resultSet.close();
+    connection.close();
+  }
+
+  @Ignore
+  @Test public void testNoFactory() throws Exception {
+    final Connection connection =
+        DriverManager.getConnection("jdbc:avatica:remote:");
+    assertThat(connection.isClosed(), is(false));
+    final ResultSet resultSet = connection.getMetaData().getSchemas();
+    assertFalse(resultSet.next());
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    assertEquals(2, metaData.getColumnCount());
+    assertEquals("TABLE_SCHEM", metaData.getColumnName(1));
+    assertEquals("TABLE_CATALOG", metaData.getColumnName(2));
+    resultSet.close();
+    connection.close();
+    assertThat(connection.isClosed(), is(true));
+  }
+
+  @Ignore
+  @Test public void testCatalogsMock() throws Exception {
+    final Connection connection = mjs();
+    assertThat(connection.isClosed(), is(false));
+    final ResultSet resultSet = connection.getMetaData().getSchemas();
+    assertFalse(resultSet.next());
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    assertEquals(2, metaData.getColumnCount());
+    assertEquals("TABLE_SCHEM", metaData.getColumnName(1));
+    assertEquals("TABLE_CATALOG", metaData.getColumnName(2));
+    resultSet.close();
+    connection.close();
+    assertThat(connection.isClosed(), is(true));
+  }
+
+  @Test public void testStatementExecuteQueryLocal() throws Exception {
+    checkStatementExecuteQuery(ljs(), false);
+  }
+
+  @Ignore
+  @Test public void testStatementExecuteQueryMock() throws Exception {
+    checkStatementExecuteQuery(mjs(), false);
+  }
+
+  @Ignore
+  @Test public void testPrepareExecuteQueryLocal() throws Exception {
+    checkStatementExecuteQuery(ljs(), true);
+  }
+
+  @Ignore
+  @Test public void testPrepareExecuteQueryMock() throws Exception {
+    checkStatementExecuteQuery(mjs(), true);
+  }
+
+  private void checkStatementExecuteQuery(Connection connection,
+      boolean prepare) throws SQLException {
+    final String sql = "select * from (\n"
+        + "  values (1, 'a'), (null, 'b'), (3, 'c')) as t (c1, c2)";
+    final Statement statement;
+    final ResultSet resultSet;
+    final ParameterMetaData parameterMetaData;
+    if (prepare) {
+      final PreparedStatement ps = connection.prepareStatement(sql);
+      statement = ps;
+      parameterMetaData = ps.getParameterMetaData();
+      resultSet = ps.executeQuery();
+    } else {
+      statement = connection.createStatement();
+      parameterMetaData = null;
+      resultSet = statement.executeQuery(sql);
+    }
+    if (parameterMetaData != null) {
+      assertThat(parameterMetaData.getParameterCount(), equalTo(2));
+    }
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    assertEquals(2, metaData.getColumnCount());
+    assertEquals("C1", metaData.getColumnName(1));
+    assertEquals("C2", metaData.getColumnName(2));
+    assertTrue(resultSet.next());
+    assertTrue(resultSet.next());
+    assertTrue(resultSet.next());
+    assertFalse(resultSet.next());
+    resultSet.close();
+    statement.close();
+    connection.close();
+  }
+
+  @Test public void testStatementLifecycle() throws Exception {
+    try (AvaticaConnection connection = (AvaticaConnection) ljs()) {
+      Map<Integer, AvaticaStatement> clientMap = connection.statementMap;
+      Cache<Integer, Object> serverMap =
+          QuasiRemoteJdbcServiceFactory.getRemoteStatementMap(connection);
+      assertEquals(0, clientMap.size());
+      assertEquals(0, serverMap.size());
+      Statement stmt = connection.createStatement();
+      assertEquals(1, clientMap.size());
+      assertEquals(1, serverMap.size());
+      stmt.close();
+      assertEquals(0, clientMap.size());
+      assertEquals(0, serverMap.size());
+    }
+  }
+
+  @Test public void testConnectionIsolation() throws Exception {
+    final String sql = "select * from (values (1, 'a'))";
+    Connection conn1 = ljs();
+    Connection conn2 = ljs();
+    Cache<String, Connection> connectionMap =
+        QuasiRemoteJdbcServiceFactory.getRemoteConnectionMap(
+            (AvaticaConnection) conn1);
+    assertEquals("connection cache should start empty",
+        0, connectionMap.size());
+    PreparedStatement conn1stmt1 = conn1.prepareStatement(sql);
+    assertEquals(
+        "statement creation implicitly creates a connection server-side",
+        1, connectionMap.size());
+    PreparedStatement conn2stmt1 = conn2.prepareStatement(sql);
+    assertEquals(
+        "statement creation implicitly creates a connection server-side",
+        2, connectionMap.size());
+    AvaticaPreparedStatement s1 = (AvaticaPreparedStatement) conn1stmt1;
+    AvaticaPreparedStatement s2 = (AvaticaPreparedStatement) conn2stmt1;
+    assertFalse("connection id's should be unique",
+        s1.handle.connectionId.equalsIgnoreCase(s2.handle.connectionId));
+    conn2.close();
+    assertEquals("closing a connection closes the server-side connection",
+        1, connectionMap.size());
+    conn1.close();
+    assertEquals("closing a connection closes the server-side connection",
+        0, connectionMap.size());
+  }
+
+  private void checkStatementExecuteQuery(Connection connection)
+      throws SQLException {
+    final Statement statement = connection.createStatement();
+    final ResultSet resultSet =
+        statement.executeQuery("select * from (\n"
+            + "  values (1, 'a'), (null, 'b'), (3, 'c')) as t (c1, c2)");
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    assertEquals(2, metaData.getColumnCount());
+    assertEquals("C1", metaData.getColumnName(1));
+    assertEquals("C2", metaData.getColumnName(2));
+    assertTrue(resultSet.next());
+    assertTrue(resultSet.next());
+    assertTrue(resultSet.next());
+    assertFalse(resultSet.next());
+    resultSet.close();
+    statement.close();
+    connection.close();
+  }
+
+  @Test public void testPrepareBindExecuteFetch() throws Exception {
+    checkPrepareBindExecuteFetch(ljs());
+  }
+
+  private void checkPrepareBindExecuteFetch(Connection connection)
+      throws SQLException {
+    final String sql = "select cast(? as integer) * 3 as c, 'x' as x\n"
+        + "from (values (1, 'a'))";
+    final PreparedStatement ps =
+        connection.prepareStatement(sql);
+    final ResultSetMetaData metaData = ps.getMetaData();
+    assertEquals(2, metaData.getColumnCount());
+    assertEquals("C", metaData.getColumnName(1));
+    assertEquals("X", metaData.getColumnName(2));
+    try {
+      final ResultSet resultSet = ps.executeQuery();
+      fail("expected error, got " + resultSet);
+    } catch (SQLException e) {
+      assertThat(e.getMessage(),
+          equalTo("exception while executing query: unbound parameter"));
+    }
+
+    final ParameterMetaData parameterMetaData = ps.getParameterMetaData();
+    assertThat(parameterMetaData.getParameterCount(), equalTo(1));
+
+    ps.setInt(1, 10);
+    final ResultSet resultSet = ps.executeQuery();
+    assertTrue(resultSet.next());
+    assertThat(resultSet.getInt(1), equalTo(30));
+    assertFalse(resultSet.next());
+    resultSet.close();
+
+    ps.setInt(1, 20);
+    final ResultSet resultSet2 = ps.executeQuery();
+    assertFalse(resultSet2.isClosed());
+    assertTrue(resultSet2.next());
+    assertThat(resultSet2.getInt(1), equalTo(60));
+    assertThat(resultSet2.wasNull(), is(false));
+    assertFalse(resultSet2.next());
+    resultSet2.close();
+
+    ps.setObject(1, null);
+    final ResultSet resultSet3 = ps.executeQuery();
+    assertTrue(resultSet3.next());
+    assertThat(resultSet3.getInt(1), equalTo(0));
+    assertThat(resultSet3.wasNull(), is(true));
+    assertFalse(resultSet3.next());
+    resultSet3.close();
+
+    ps.close();
+    connection.close();
+  }
+
+  /**
+   * Factory that creates a service based on a local JDBC connection.
+   */
+  public static class LocalJdbcServiceFactory implements Service.Factory {
+    @Override public Service create(AvaticaConnection connection) {
+      try {
+        return new LocalService(
+            new JdbcMeta(CONNECTION_SPEC.url, CONNECTION_SPEC.username,
+                CONNECTION_SPEC.password));
+      } catch (SQLException e) {
+        throw new RuntimeException(e);
+      }
+    }
+  }
+
+  /**
+   * Factory that creates a service based on a local JDBC connection.
+   */
+  public static class QuasiRemoteJdbcServiceFactory implements Service.Factory {
+
+    /** a singleton instance that is recreated for each test */
+    private static Service service;
+
+    static void initService() {
+      try {
+        final JdbcMeta jdbcMeta = new JdbcMeta(CONNECTION_SPEC.url,
+            CONNECTION_SPEC.username, CONNECTION_SPEC.password);
+        final LocalService localService = new LocalService(jdbcMeta);
+        service = new LocalJsonService(localService);
+      } catch (SQLException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Override public Service create(AvaticaConnection connection) {
+      assert service != null;
+      return service;
+    }
+
+    /**
+     * Reach into the guts of a quasi-remote connection and pull out the
+     * statement map from the other side.
+     * TODO: refactor tests to replace reflection with package-local access
+     */
+    static Cache<Integer, Object>
+    getRemoteStatementMap(AvaticaConnection connection) throws Exception {
+      Field metaF = AvaticaConnection.class.getDeclaredField("meta");
+      metaF.setAccessible(true);
+      Meta clientMeta = (Meta) metaF.get(connection);
+      Field remoteMetaServiceF = clientMeta.getClass().getDeclaredField("service");
+      remoteMetaServiceF.setAccessible(true);
+      LocalJsonService remoteMetaService = (LocalJsonService) remoteMetaServiceF.get(clientMeta);
+      Field remoteMetaServiceServiceF = remoteMetaService.getClass().getDeclaredField("service");
+      remoteMetaServiceServiceF.setAccessible(true);
+      LocalService remoteMetaServiceService =
+          (LocalService) remoteMetaServiceServiceF.get(remoteMetaService);
+      Field remoteMetaServiceServiceMetaF =
+          remoteMetaServiceService.getClass().getDeclaredField("meta");
+      remoteMetaServiceServiceMetaF.setAccessible(true);
+      JdbcMeta serverMeta = (JdbcMeta) remoteMetaServiceServiceMetaF.get(remoteMetaServiceService);
+      Field jdbcMetaStatementMapF = JdbcMeta.class.getDeclaredField("statementCache");
+      jdbcMetaStatementMapF.setAccessible(true);
+      //noinspection unchecked
+      return (Cache<Integer, Object>) jdbcMetaStatementMapF.get(serverMeta);
+    }
+
+    /**
+     * Reach into the guts of a quasi-remote connection and pull out the
+     * connection map from the other side.
+     * TODO: refactor tests to replace reflection with package-local access
+     */
+    static Cache<String, Connection>
+    getRemoteConnectionMap(AvaticaConnection connection) throws Exception {
+      Field metaF = AvaticaConnection.class.getDeclaredField("meta");
+      metaF.setAccessible(true);
+      Meta clientMeta = (Meta) metaF.get(connection);
+      Field remoteMetaServiceF = clientMeta.getClass().getDeclaredField("service");
+      remoteMetaServiceF.setAccessible(true);
+      LocalJsonService remoteMetaService = (LocalJsonService) remoteMetaServiceF.get(clientMeta);
+      Field remoteMetaServiceServiceF = remoteMetaService.getClass().getDeclaredField("service");
+      remoteMetaServiceServiceF.setAccessible(true);
+      LocalService remoteMetaServiceService =
+          (LocalService) remoteMetaServiceServiceF.get(remoteMetaService);
+      Field remoteMetaServiceServiceMetaF =
+          remoteMetaServiceService.getClass().getDeclaredField("meta");
+      remoteMetaServiceServiceMetaF.setAccessible(true);
+      JdbcMeta serverMeta = (JdbcMeta) remoteMetaServiceServiceMetaF.get(remoteMetaServiceService);
+      Field jdbcMetaConnectionCacheF = JdbcMeta.class.getDeclaredField("connectionCache");
+      jdbcMetaConnectionCacheF.setAccessible(true);
+      //noinspection unchecked
+      return (Cache<String, Connection>) jdbcMetaConnectionCacheF.get(serverMeta);
+    }
+  }
+
+  /** Information necessary to create a JDBC connection. Specify one to run
+   * tests against a different database. (hsqldb is the default.) */
+  public static class ConnectionSpec {
+    public final String url;
+    public final String username;
+    public final String password;
+    public final String driver;
+
+    public ConnectionSpec(String url, String username, String password,
+        String driver) {
+      this.url = url;
+      this.username = username;
+      this.password = password;
+      this.driver = driver;
+    }
+
+    public static final ConnectionSpec HSQLDB =
+        new ConnectionSpec(ScottHsqldb.URI, ScottHsqldb.USER,
+            ScottHsqldb.PASSWORD, "org.hsqldb.jdbcDriver");
+  }
+}
+
+// End RemoteDriverTest.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica-server/src/test/java/org/apache/calcite/avatica/test/AvaticaSuite.java
----------------------------------------------------------------------
diff --git a/avatica-server/src/test/java/org/apache/calcite/avatica/test/AvaticaSuite.java b/avatica-server/src/test/java/org/apache/calcite/avatica/test/AvaticaSuite.java
new file mode 100644
index 0000000..5afba20
--- /dev/null
+++ b/avatica-server/src/test/java/org/apache/calcite/avatica/test/AvaticaSuite.java
@@ -0,0 +1,36 @@
+/*
+ * 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.avatica.test;
+
+import org.apache.calcite.avatica.RemoteDriverTest;
+
+import org.junit.runner.RunWith;
+
+import org.junit.runners.Suite;
+
+/**
+ * Avatica test suite.
+ */
+@RunWith(Suite.class)
+@Suite.SuiteClasses({
+    ConnectStringParserTest.class,
+    RemoteDriverTest.class
+})
+public class AvaticaSuite {
+}
+
+// End AvaticaSuite.java

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica/pom.xml
----------------------------------------------------------------------
diff --git a/avatica/pom.xml b/avatica/pom.xml
index 0bacc4a..01cd4e9 100644
--- a/avatica/pom.xml
+++ b/avatica/pom.xml
@@ -38,32 +38,21 @@ limitations under the License.
          or on libraries other than Jackson. -->
     <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
-      <artifactId>jackson-databind</artifactId>
+      <artifactId>jackson-core</artifactId>
     </dependency>
     <dependency>
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-annotations</artifactId>
     </dependency>
     <dependency>
-      <groupId>commons-logging</groupId>
-      <artifactId>commons-logging</artifactId>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
     </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>net.hydromatic</groupId>
-      <artifactId>scott-data-hsqldb</artifactId>
-      <version>0.1</version>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>org.hsqldb</groupId>
-      <artifactId>hsqldb</artifactId>
-      <scope>test</scope>
-    </dependency>
   </dependencies>
 
   <build>
@@ -94,6 +83,20 @@ limitations under the License.
           </execution>
         </executions>
       </plugin>
+
+      <!-- Produce a tests jar so that avatica-server/pom.xml can reference for suite.
+           TODO: remove after moving over to annotation-based TestSuite definitions. -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>test-jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
 </project>

http://git-wip-us.apache.org/repos/asf/incubator-calcite/blob/fa9bdc4a/avatica/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
----------------------------------------------------------------------
diff --git a/avatica/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java b/avatica/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
deleted file mode 100644
index 90e8c2a..0000000
--- a/avatica/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
+++ /dev/null
@@ -1,765 +0,0 @@
-/*
- * 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.avatica.jdbc;
-
-import org.apache.calcite.avatica.AvaticaParameter;
-import org.apache.calcite.avatica.ColumnMetaData;
-import org.apache.calcite.avatica.Meta;
-
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
-
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.RemovalListener;
-import com.google.common.cache.RemovalNotification;
-
-import java.lang.reflect.Array;
-import java.lang.reflect.Type;
-import java.math.BigDecimal;
-import java.sql.Connection;
-import java.sql.DriverManager;
-import java.sql.ParameterMetaData;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.sql.Types;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.UUID;
-import java.util.concurrent.TimeUnit;
-
-/** Implementation of {@link Meta} upon an existing JDBC data source. */
-public class JdbcMeta implements Meta {
-
-  private static final Log LOG = LogFactory.getLog(JdbcMeta.class);
-
-  /**
-   * JDBC Types Mapped to Java Types
-   *
-   * @see <a href="https://docs.oracle.com/javase/1.5.0/docs/guide/jdbc/getstart/mapping.html#1051555">JDBC Types Mapped to Java Types</a>
-   */
-  protected static final Map<Integer, Type> SQL_TYPE_TO_JAVA_TYPE =
-      new HashMap<>();
-  static {
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.CHAR, String.class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.VARCHAR, String.class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.LONGNVARCHAR, String.class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.NUMERIC, BigDecimal.class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.DECIMAL, BigDecimal.class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.BIT, Boolean.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.TINYINT, Byte.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.SMALLINT, Short.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.INTEGER, Integer.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.BIGINT, Long.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.REAL, Float.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.FLOAT, Double.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.DOUBLE, Double.TYPE);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.BINARY, byte[].class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.VARBINARY, byte[].class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.LONGVARBINARY, byte[].class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.DATE, java.sql.Date.class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.TIME, java.sql.Time.class);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.TIMESTAMP, java.sql.Timestamp.class);
-    //put(Types.CLOB, Clob);
-    //put(Types.BLOB, Blob);
-    SQL_TYPE_TO_JAVA_TYPE.put(Types.ARRAY, Array.class);
-  }
-
-  /** A "magic column" descriptor used to return statement execute update count via a
-   * {@link Meta.MetaResultSet}. */
-  private static final ColumnMetaData UPDATE_COL = new ColumnMetaData(1, false, false, false,
-      false, ResultSetMetaData.columnNoNulls, true, 10, "u", "u", null, 15, 15, "update_result",
-      "avatica_internal", ColumnMetaData.scalar(Types.INTEGER, "INTEGER",
-        ColumnMetaData.Rep.INTEGER), true, false, false, "java.lang.Integer");
-
-  private static final String CONN_CACHE_KEY_BASE = "avatica.connectioncache";
-
-  /** Configurable connection cache settings. */
-  public enum ConnectionCacheSettings {
-    /** JDBC connection property for setting connection cache concurrency level. */
-    CONCURRENCY_LEVEL(CONN_CACHE_KEY_BASE + ".concurrency", "10"),
-
-    /** JDBC connection property for setting connection cache initial capacity. */
-    INITIAL_CAPACITY(CONN_CACHE_KEY_BASE + ".initialcapacity", "100"),
-
-    /** JDBC connection property for setting connection cache maximum capacity. */
-    MAX_CAPACITY(CONN_CACHE_KEY_BASE + ".maxcapacity", "1000"),
-
-    /** JDBC connection property for setting connection cache expiration duration. */
-    EXPIRY_DURATION(CONN_CACHE_KEY_BASE + ".expirydiration", "10"),
-
-    /** JDBC connection property for setting connection cache expiration unit. */
-    EXPIRY_UNIT(CONN_CACHE_KEY_BASE + ".expiryunit", TimeUnit.MINUTES.name());
-
-    private final String key;
-    private final String defaultValue;
-
-    private ConnectionCacheSettings(String key, String defaultValue) {
-      this.key = key;
-      this.defaultValue = defaultValue;
-    }
-
-    /** The configuration key for specifying this setting. */
-    public String key() {
-      return key;
-    }
-
-    /** The default value for this setting. */
-    public String defaultValue() {
-      return defaultValue;
-    }
-  }
-
-  private static final String STMT_CACHE_KEY_BASE = "avatica.statementcache";
-
-  /** Configurable statement cache settings. */
-  public enum StatementCacheSettings {
-    /** JDBC connection property for setting connection cache concurrency level. */
-    CONCURRENCY_LEVEL(STMT_CACHE_KEY_BASE + ".concurrency", "100"),
-
-    /** JDBC connection property for setting connection cache initial capacity. */
-    INITIAL_CAPACITY(STMT_CACHE_KEY_BASE + ".initialcapacity", "1000"),
-
-    /** JDBC connection property for setting connection cache maximum capacity. */
-    MAX_CAPACITY(STMT_CACHE_KEY_BASE + ".maxcapacity", "10000"),
-
-    /** JDBC connection property for setting connection cache expiration duration.
-     *
-     * <p>Used in conjunction with {@link #EXPIRY_UNIT}.</p>
-     */
-    EXPIRY_DURATION(STMT_CACHE_KEY_BASE + ".expirydiration", "5"),
-
-    /** JDBC connection property for setting connection cache expiration unit.
-     *
-     * <p>Used in conjunction with {@link #EXPIRY_DURATION}.</p>
-     */
-    EXPIRY_UNIT(STMT_CACHE_KEY_BASE + ".expiryunit", TimeUnit.MINUTES.name());
-
-    private final String key;
-    private final String defaultValue;
-
-    private StatementCacheSettings(String key, String defaultValue) {
-      this.key = key;
-      this.defaultValue = defaultValue;
-    }
-
-    /** The configuration key for specifying this setting. */
-    public String key() {
-      return key;
-    }
-
-    /** The default value for this setting. */
-    public String defaultValue() {
-      return defaultValue;
-    }
-  }
-
-  private static final String DEFAULT_CONN_ID =
-      UUID.fromString("00000000-0000-0000-0000-000000000000").toString();
-
-  private final String url;
-  private final Properties info;
-  private final Connection connection; // TODO: remove default connection
-  private final Cache<String, Connection> connectionCache;
-  private final Cache<Integer, StatementInfo> statementCache;
-
-  /**
-   * Convert from JDBC metadata to Avatica columns.
-   */
-  protected static List<ColumnMetaData>
-  columns(ResultSetMetaData metaData) throws SQLException {
-    final List<ColumnMetaData> columns = new ArrayList<>();
-    for (int i = 1; i <= metaData.getColumnCount(); i++) {
-      final Type javaType =
-          SQL_TYPE_TO_JAVA_TYPE.get(metaData.getColumnType(i));
-      ColumnMetaData.AvaticaType t =
-          ColumnMetaData.scalar(metaData.getColumnType(i),
-              metaData.getColumnTypeName(i), ColumnMetaData.Rep.of(javaType));
-      ColumnMetaData md =
-          new ColumnMetaData(i - 1, metaData.isAutoIncrement(i),
-              metaData.isCaseSensitive(i), metaData.isSearchable(i),
-              metaData.isCurrency(i), metaData.isNullable(i),
-              metaData.isSigned(i), metaData.getColumnDisplaySize(i),
-              metaData.getColumnLabel(i), metaData.getColumnName(i),
-              metaData.getSchemaName(i), metaData.getPrecision(i),
-              metaData.getScale(i), metaData.getTableName(i),
-              metaData.getCatalogName(i), t, metaData.isReadOnly(i),
-              metaData.isWritable(i), metaData.isDefinitelyWritable(i),
-              metaData.getColumnClassName(i));
-      columns.add(md);
-    }
-    return columns;
-  }
-
-  /**
-   * Converts from JDBC metadata to AvaticaParameters
-   */
-  protected static List<AvaticaParameter> parameters(ParameterMetaData metaData)
-      throws SQLException {
-    if (metaData == null) {
-      return Collections.emptyList();
-    }
-    final List<AvaticaParameter> params = new ArrayList<>();
-    for (int i = 1; i <= metaData.getParameterCount(); i++) {
-      params.add(
-          new AvaticaParameter(metaData.isSigned(i), metaData.getPrecision(i),
-              metaData.getScale(i), metaData.getParameterType(i),
-              metaData.getParameterTypeName(i),
-              metaData.getParameterClassName(i), "?" + i));
-    }
-    return params;
-  }
-
-  protected static Signature signature(ResultSetMetaData metaData,
-      ParameterMetaData parameterMetaData, String sql) throws  SQLException {
-    return new Signature(columns(metaData), sql, parameters(parameterMetaData),
-        null, CursorFactory.LIST /* LIST because JdbcResultSet#frame */);
-  }
-
-  protected static Signature signature(ResultSetMetaData metaData)
-      throws SQLException {
-    return signature(metaData, null, null);
-  }
-
-  /** Callback for {@link #connectionCache} member expiration. */
-  private class ConnectionExpiryHandler
-      implements RemovalListener<String, Connection> {
-
-    public void onRemoval(RemovalNotification<String, Connection> notification) {
-      String connectionId = notification.getKey();
-      Connection doomed = notification.getValue();
-      // is String.equals() more efficient?
-      if (notification.getValue() == connection) {
-        return;
-      }
-      if (LOG.isDebugEnabled()) {
-        LOG.debug("Expiring connection " + connectionId + " because "
-            + notification.getCause());
-      }
-      try {
-        if (doomed != null) {
-          doomed.close();
-        }
-      } catch (Throwable t) {
-        LOG.info("Exception thrown while expiring connection " + connectionId, t);
-      }
-    }
-  }
-
-  /** Callback for {@link #statementCache} member expiration. */
-  private class StatementExpiryHandler
-      implements RemovalListener<Integer, StatementInfo> {
-    public void onRemoval(RemovalNotification<Integer, StatementInfo> notification) {
-      Integer stmtId = notification.getKey();
-      StatementInfo doomed = notification.getValue();
-      if (doomed == null) {
-        // log/throw?
-        return;
-      }
-      if (LOG.isDebugEnabled()) {
-        LOG.debug("Expiring statement " + stmtId + " because "
-            + notification.getCause());
-      }
-      try {
-        if (doomed.resultSet != null) {
-          doomed.resultSet.close();
-        }
-        if (doomed.statement != null) {
-          doomed.statement.close();
-        }
-      } catch (Throwable t) {
-        LOG.info("Exception thrown while expiring statement " + stmtId);
-      }
-    }
-  }
-
-  /**
-   * @param url a database url of the form
-   *  <code> jdbc:<em>subprotocol</em>:<em>subname</em></code>
-   */
-  public JdbcMeta(String url) throws SQLException {
-    this(url, new Properties());
-  }
-
-  /**
-   * @param url a database url of the form
-   * <code>jdbc:<em>subprotocol</em>:<em>subname</em></code>
-   * @param user the database user on whose behalf the connection is being
-   *   made
-   * @param password the user's password
-   */
-  public JdbcMeta(final String url, final String user, final String password)
-      throws SQLException {
-    this(url, new Properties() {
-      {
-        put("user", user);
-        put("password", password);
-      }
-    });
-  }
-
-  /**
-   * @param url a database url of the form
-   * <code> jdbc:<em>subprotocol</em>:<em>subname</em></code>
-   * @param info a list of arbitrary string tag/value pairs as
-   * connection arguments; normally at least a "user" and
-   * "password" property should be included
-   */
-  public JdbcMeta(String url, Properties info) throws SQLException {
-    this.url = url;
-    this.info = info;
-    this.connection = DriverManager.getConnection(url, info);
-
-    int concurrencyLevel = Integer.parseInt(
-        info.getProperty(ConnectionCacheSettings.CONCURRENCY_LEVEL.key(),
-            ConnectionCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
-    int initialCapacity = Integer.parseInt(
-        info.getProperty(ConnectionCacheSettings.INITIAL_CAPACITY.key(),
-            ConnectionCacheSettings.INITIAL_CAPACITY.defaultValue()));
-    long maxCapacity = Long.parseLong(
-        info.getProperty(ConnectionCacheSettings.MAX_CAPACITY.key(),
-            ConnectionCacheSettings.MAX_CAPACITY.defaultValue()));
-    long connectionExpiryDuration = Long.parseLong(
-        info.getProperty(ConnectionCacheSettings.EXPIRY_DURATION.key(),
-            ConnectionCacheSettings.EXPIRY_DURATION.defaultValue()));
-    TimeUnit connectionExpiryUnit = TimeUnit.valueOf(
-        info.getProperty(ConnectionCacheSettings.EXPIRY_UNIT.key(),
-            ConnectionCacheSettings.EXPIRY_UNIT.defaultValue()));
-    this.connectionCache = CacheBuilder.newBuilder()
-        .concurrencyLevel(concurrencyLevel)
-        .initialCapacity(initialCapacity)
-        .maximumSize(maxCapacity)
-        .expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
-        .removalListener(new ConnectionExpiryHandler())
-        .build();
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("instantiated connection cache: " + connectionCache.stats());
-    }
-
-    concurrencyLevel = Integer.parseInt(
-        info.getProperty(StatementCacheSettings.CONCURRENCY_LEVEL.key(),
-            StatementCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
-    initialCapacity = Integer.parseInt(
-        info.getProperty(StatementCacheSettings.INITIAL_CAPACITY.key(),
-            StatementCacheSettings.INITIAL_CAPACITY.defaultValue()));
-    maxCapacity = Long.parseLong(
-        info.getProperty(StatementCacheSettings.MAX_CAPACITY.key(),
-            StatementCacheSettings.MAX_CAPACITY.defaultValue()));
-    connectionExpiryDuration = Long.parseLong(
-        info.getProperty(StatementCacheSettings.EXPIRY_DURATION.key(),
-            StatementCacheSettings.EXPIRY_DURATION.defaultValue()));
-    connectionExpiryUnit = TimeUnit.valueOf(
-        info.getProperty(StatementCacheSettings.EXPIRY_UNIT.key(),
-            StatementCacheSettings.EXPIRY_UNIT.defaultValue()));
-    this.statementCache = CacheBuilder.newBuilder()
-        .concurrencyLevel(concurrencyLevel)
-        .initialCapacity(initialCapacity)
-        .maximumSize(maxCapacity)
-        .expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
-        .removalListener(new StatementExpiryHandler())
-        .build();
-    if (LOG.isDebugEnabled()) {
-      LOG.debug("instantiated statement cache: " + statementCache.stats());
-    }
-  }
-
-  public String getSqlKeywords() {
-    try {
-      return connection.getMetaData().getSQLKeywords();
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public String getNumericFunctions() {
-    try {
-      return connection.getMetaData().getNumericFunctions();
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public String getStringFunctions() {
-    return null;
-  }
-
-  public String getSystemFunctions() {
-    return null;
-  }
-
-  public String getTimeDateFunctions() {
-    return null;
-  }
-
-  public MetaResultSet getTables(String catalog, Pat schemaPattern,
-      Pat tableNamePattern, List<String> typeList) {
-    try {
-      String[] types = new String[typeList == null ? 0 : typeList.size()];
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getTables(catalog, schemaPattern.s,
-              tableNamePattern.s,
-              typeList == null ? types : typeList.toArray(types)));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getColumns(String catalog, Pat schemaPattern,
-      Pat tableNamePattern, Pat columnNamePattern) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getColumns(catalog, schemaPattern.s,
-              tableNamePattern.s, columnNamePattern.s));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getSchemas(String catalog, Pat schemaPattern) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getSchemas(catalog, schemaPattern.s));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getCatalogs() {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getCatalogs());
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getTableTypes() {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getTableTypes());
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getProcedures(String catalog, Pat schemaPattern,
-      Pat procedureNamePattern) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getProcedures(catalog, schemaPattern.s,
-              procedureNamePattern.s));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getProcedureColumns(String catalog, Pat schemaPattern,
-      Pat procedureNamePattern, Pat columnNamePattern) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getProcedureColumns(catalog,
-              schemaPattern.s, procedureNamePattern.s, columnNamePattern.s));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getColumnPrivileges(String catalog, String schema,
-      String table, Pat columnNamePattern) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getColumnPrivileges(catalog, schema,
-              table, columnNamePattern.s));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getTablePrivileges(String catalog, Pat schemaPattern,
-      Pat tableNamePattern) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getTablePrivileges(catalog,
-              schemaPattern.s, tableNamePattern.s));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getBestRowIdentifier(String catalog, String schema,
-      String table, int scope, boolean nullable) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getBestRowIdentifier(catalog, schema,
-              table, scope, nullable));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getVersionColumns(String catalog, String schema,
-      String table) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getVersionColumns(catalog, schema, table));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getPrimaryKeys(String catalog, String schema,
-      String table) {
-    try {
-      return JdbcResultSet.create(DEFAULT_CONN_ID, -1,
-          connection.getMetaData().getPrimaryKeys(catalog, schema, table));
-    } catch (SQLException e) {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public MetaResultSet getImportedKeys(String catalog, String schema,
-      String table) {
-    return null;
-  }
-
-  public MetaResultSet getExportedKeys(String catalog, String schema,
-      String table) {
-    return null;
-  }
-
-  public MetaResultSet getCrossReference(String parentCatalog,
-      String parentSchema, String parentTable, String foreignCatalog,
-      String foreignSchema, String foreignTable) {
-    return null;
-  }
-
-  public MetaResultSet getTypeInfo() {
-    return null;
-  }
-
-  public MetaResultSet getIndexInfo(String catalog, String schema, String table,
-      boolean unique, boolean approximate) {
-    return null;
-  }
-
-  public MetaResultSet getUDTs(String catalog, Pat schemaPattern,
-      Pat typeNamePattern, int[] types) {
-    return null;
-  }
-
-  public MetaResultSet getSuperTypes(String catalog, Pat schemaPattern,
-      Pat typeNamePattern) {
-    return null;
-  }
-
-  public MetaResultSet getSuperTables(String catalog, Pat schemaPattern,
-      Pat tableNamePattern) {
-    return null;
-  }
-
-  public MetaResultSet getAttributes(String catalog, Pat schemaPattern,
-      Pat typeNamePattern, Pat attributeNamePattern) {
-    return null;
-  }
-
-  public MetaResultSet getClientInfoProperties() {
-    return null;
-  }
-
-  public MetaResultSet getFunctions(String catalog, Pat schemaPattern,
-      Pat functionNamePattern) {
-    return null;
-  }
-
-  public MetaResultSet getFunctionColumns(String catalog, Pat schemaPattern,
-      Pat functionNamePattern, Pat columnNamePattern) {
-    return null;
-  }
-
-  public MetaResultSet getPseudoColumns(String catalog, Pat schemaPattern,
-      Pat tableNamePattern, Pat columnNamePattern) {
-    return null;
-  }
-
-  public Iterable<Object> createIterable(StatementHandle handle,
-      Signature signature, List<Object> parameterValues, Frame firstFrame) {
-    return null;
-  }
-
-  protected Connection getConnection(String id) throws SQLException {
-    Connection conn = connectionCache.getIfPresent(id);
-    if (conn == null) {
-      conn = DriverManager.getConnection(url, info);
-      connectionCache.put(id, conn);
-    }
-    return conn;
-  }
-
-  public StatementHandle createStatement(ConnectionHandle ch) {
-    try {
-      final Connection conn = getConnection(ch.id);
-      final Statement statement = conn.createStatement();
-      final int id = System.identityHashCode(statement);
-      statementCache.put(id, new StatementInfo(statement));
-      StatementHandle h = new StatementHandle(ch.id, id, null);
-      if (LOG.isTraceEnabled()) {
-        LOG.trace("created statement " + h);
-      }
-      return h;
-    } catch (SQLException e) {
-      throw propagate(e);
-    }
-  }
-
-  @Override public void closeStatement(StatementHandle h) {
-    StatementInfo info = statementCache.getIfPresent(h.id);
-    if (info == null || info.statement == null) {
-      LOG.debug("client requested close unknown statement " + h);
-      return;
-    }
-    if (LOG.isTraceEnabled()) {
-      LOG.trace("closing statement " + h);
-    }
-    try {
-      if (info.resultSet != null) {
-        info.resultSet.close();
-      }
-      info.statement.close();
-    } catch (SQLException e) {
-      throw propagate(e);
-    } finally {
-      statementCache.invalidate(h.id);
-    }
-  }
-
-  @Override public void closeConnection(ConnectionHandle ch) {
-    Connection conn = connectionCache.getIfPresent(ch.id);
-    if (conn == null) {
-      LOG.debug("client requested close unknown connection " + ch);
-      return;
-    }
-    if (LOG.isTraceEnabled()) {
-      LOG.trace("closing connection " + ch);
-    }
-    try {
-      conn.close();
-    } catch (SQLException e) {
-      throw propagate(e);
-    } finally {
-      connectionCache.invalidate(ch.id);
-    }
-  }
-
-  private RuntimeException propagate(Throwable e) {
-    if (e instanceof RuntimeException) {
-      throw (RuntimeException) e;
-    } else if (e instanceof Error) {
-      throw (Error) e;
-    } else {
-      throw new RuntimeException(e);
-    }
-  }
-
-  public StatementHandle prepare(ConnectionHandle ch, String sql,
-      int maxRowCount) {
-    try {
-      final Connection conn = getConnection(ch.id);
-      final PreparedStatement statement = conn.prepareStatement(sql);
-      final int id = System.identityHashCode(statement);
-      statementCache.put(id, new StatementInfo(statement));
-      StatementHandle h = new StatementHandle(ch.id, id,
-          signature(statement.getMetaData(), statement.getParameterMetaData(),
-              sql));
-      if (LOG.isTraceEnabled()) {
-        LOG.trace("prepared statement " + h);
-      }
-      return h;
-    } catch (SQLException e) {
-      throw propagate(e);
-    }
-  }
-
-  public MetaResultSet prepareAndExecute(ConnectionHandle ch, String sql,
-      int maxRowCount, PrepareCallback callback) {
-    try {
-      final Connection connection = getConnection(ch.id);
-      final PreparedStatement statement = connection.prepareStatement(sql);
-      final int id = System.identityHashCode(statement);
-      final StatementInfo info = new StatementInfo(statement);
-      statementCache.put(id, info);
-      info.resultSet = statement.executeQuery();
-      MetaResultSet mrs = JdbcResultSet.create(ch.id, id, info.resultSet);
-      if (LOG.isTraceEnabled()) {
-        StatementHandle h = new StatementHandle(ch.id, id, null);
-        LOG.trace("prepAndExec statement " + h);
-      }
-      return mrs;
-    } catch (SQLException e) {
-      throw propagate(e);
-    }
-  }
-
-  public Frame fetch(StatementHandle h, List<Object> parameterValues,
-      int offset, int fetchMaxRowCount) {
-    if (LOG.isTraceEnabled()) {
-      LOG.trace("fetching " + h + " offset:" + offset + " fetchMaxRowCount:" + fetchMaxRowCount);
-    }
-    try {
-      final StatementInfo statementInfo = Objects.requireNonNull(
-          statementCache.getIfPresent(h.id),
-          "Statement not found, potentially expired. " + h);
-      if (statementInfo.resultSet == null || parameterValues != null) {
-        if (statementInfo.resultSet != null) {
-          statementInfo.resultSet.close();
-        }
-        final PreparedStatement preparedStatement =
-            (PreparedStatement) statementInfo.statement;
-        if (parameterValues != null) {
-          for (int i = 0; i < parameterValues.size(); i++) {
-            Object o = parameterValues.get(i);
-            preparedStatement.setObject(i + 1, o);
-          }
-        }
-        statementInfo.resultSet = preparedStatement.executeQuery();
-      }
-      return JdbcResultSet.frame(statementInfo.resultSet, offset,
-          fetchMaxRowCount);
-    } catch (SQLException e) {
-      throw propagate(e);
-    }
-  }
-
-  /** All we know about a statement. */
-  private static class StatementInfo {
-    final Statement statement; // sometimes a PreparedStatement
-    ResultSet resultSet;
-
-    private StatementInfo(Statement statement) {
-      this.statement = Objects.requireNonNull(statement);
-    }
-  }
-}
-
-// End JdbcMeta.java