You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by el...@apache.org on 2016/03/07 19:28:03 UTC

[25/59] [partial] calcite git commit: [CALCITE-1078] Detach avatica from the core calcite Maven project

http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/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..228ba8d
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
@@ -0,0 +1,1320 @@
+/*
+ * 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.JsonService;
+import org.apache.calcite.avatica.remote.LocalJsonService;
+import org.apache.calcite.avatica.remote.LocalProtobufService;
+import org.apache.calcite.avatica.remote.LocalService;
+import org.apache.calcite.avatica.remote.ProtobufTranslation;
+import org.apache.calcite.avatica.remote.ProtobufTranslationImpl;
+import org.apache.calcite.avatica.remote.Service;
+
+import com.google.common.cache.Cache;
+
+import net.jcip.annotations.NotThreadSafe;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.lang.reflect.Field;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.Date;
+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.Time;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+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.
+ */
+@RunWith(Parameterized.class)
+@NotThreadSafe // for testConnectionIsolation
+public class RemoteDriverTest {
+  public static final String LJS =
+      LocalJdbcServiceFactory.class.getName();
+
+  public static final String QRJS =
+      QuasiRemoteJdbcServiceFactory.class.getName();
+
+  public static final String QRPBS =
+      QuasiRemotePBJdbcServiceFactory.class.getName();
+
+  private static final ConnectionSpec CONNECTION_SPEC = ConnectionSpec.HSQLDB;
+
+  private static Connection ljs() throws SQLException {
+    return DriverManager.getConnection("jdbc:avatica:remote:factory=" + QRJS);
+  }
+
+  private static Connection lpbs() throws SQLException {
+    return DriverManager.getConnection("jdbc:avatica:remote:factory=" + QRPBS);
+  }
+
+  private Connection canon() throws SQLException {
+    return DriverManager.getConnection(CONNECTION_SPEC.url,
+        CONNECTION_SPEC.username, CONNECTION_SPEC.password);
+  }
+
+  /**
+   * Interface that allows for alternate ways to access internals to the Connection for testing
+   * purposes.
+   */
+  interface ConnectionInternals {
+    /**
+     * Reaches into the guts of a quasi-remote connection and pull out the
+     * statement map from the other side.
+     *
+     * <p>TODO: refactor tests to replace reflection with package-local access
+     */
+    Cache<Integer, Object> getRemoteStatementMap(AvaticaConnection connection) throws Exception;
+
+    /**
+     * Reaches into the guts of a quasi-remote connection and pull out the
+     * connection map from the other side.
+     *
+     * <p>TODO: refactor tests to replace reflection with package-local access
+     */
+    Cache<String, Connection> getRemoteConnectionMap(AvaticaConnection connection) throws Exception;
+  }
+
+  // Run each test with the LocalJsonService and LocalProtobufService
+  @Parameters
+  public static List<Object[]> parameters() {
+    List<Object[]> connections = new ArrayList<>();
+
+    // Json and Protobuf operations should be equivalent -- tests against one work on the other
+    // Each test needs to get a fresh Connection and also access some internals on that Connection.
+
+    connections.add(
+      new Object[] {
+        new Callable<Connection>() {
+          public Connection call() {
+            try {
+              return ljs();
+            } catch (SQLException e) {
+              throw new RuntimeException(e);
+            }
+          }
+        },
+        new QuasiRemoteJdbcServiceInternals(),
+        new Callable<RequestInspection>() {
+          public RequestInspection call() throws Exception {
+            assert null != QuasiRemoteJdbcServiceFactory.requestInspection;
+            return QuasiRemoteJdbcServiceFactory.requestInspection;
+          }
+        } });
+
+    // TODO write the ConnectionInternals implementation
+    connections.add(
+      new Object[] {
+        new Callable<Connection>() {
+          public Connection call() {
+            try {
+              return lpbs();
+            } catch (SQLException e) {
+              throw new RuntimeException(e);
+            }
+          }
+        },
+        new QuasiRemoteProtobufJdbcServiceInternals(),
+        new Callable<RequestInspection>() {
+          public RequestInspection call() throws Exception {
+            assert null != QuasiRemotePBJdbcServiceFactory.requestInspection;
+            return QuasiRemotePBJdbcServiceFactory.requestInspection;
+          }
+        } });
+
+    return connections;
+  }
+
+  private final Callable<Connection> localConnectionCallable;
+  private final ConnectionInternals localConnectionInternals;
+  private final Callable<RequestInspection> requestInspectionCallable;
+
+  public RemoteDriverTest(Callable<Connection> localConnectionCallable,
+      ConnectionInternals internals, Callable<RequestInspection> requestInspectionCallable) {
+    this.localConnectionCallable = localConnectionCallable;
+    this.localConnectionInternals = internals;
+    this.requestInspectionCallable = requestInspectionCallable;
+  }
+
+  private Connection getLocalConnection() {
+    try {
+      return localConnectionCallable.call();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private ConnectionInternals getLocalConnectionInternals() {
+    return localConnectionInternals;
+  }
+
+  private RequestInspection getRequestInspection() {
+    try {
+      return requestInspectionCallable.call();
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  /** Executes a lambda for the canonical connection and the local
+   * connection. */
+  public void eachConnection(ConnectionFunction f, Connection localConn) throws Exception {
+    for (int i = 0; i < 2; i++) {
+      try (Connection connection = i == 0 ? canon() : localConn) {
+        f.apply(connection);
+      }
+    }
+  }
+
+  @Before
+  public void before() throws Exception {
+    QuasiRemoteJdbcServiceFactory.initService();
+    QuasiRemotePBJdbcServiceFactory.initService();
+  }
+
+  @Test public void testRegister() throws Exception {
+    final Connection connection = getLocalConnection();
+    assertThat(connection.isClosed(), is(false));
+    connection.close();
+    assertThat(connection.isClosed(), is(true));
+  }
+
+  @Test public void testDatabaseProperties() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      final Connection connection = getLocalConnection();
+      for (Meta.DatabaseProperty p : Meta.DatabaseProperty.values()) {
+        switch (p) {
+        case GET_NUMERIC_FUNCTIONS:
+          assertThat(connection.getMetaData().getNumericFunctions(),
+              equalTo("ABS,ACOS,ASIN,ATAN,ATAN2,BITAND,BITOR,BITXOR,"
+                  + "CEILING,COS,COT,DEGREES,EXP,FLOOR,LOG,LOG10,MOD,"
+                  + "PI,POWER,RADIANS,RAND,ROUND,ROUNDMAGIC,SIGN,SIN,"
+                  + "SQRT,TAN,TRUNCATE"));
+          break;
+        case GET_SYSTEM_FUNCTIONS:
+          assertThat(connection.getMetaData().getSystemFunctions(),
+              equalTo("DATABASE,IFNULL,USER"));
+          break;
+        case GET_TIME_DATE_FUNCTIONS:
+          assertThat(connection.getMetaData().getTimeDateFunctions(),
+              equalTo("CURDATE,CURTIME,DATEDIFF,DAYNAME,DAYOFMONTH,DAYOFWEEK,"
+                  + "DAYOFYEAR,HOUR,MINUTE,MONTH,MONTHNAME,NOW,QUARTER,SECOND,"
+                  + "SECONDS_SINCE_MIDNIGHT,TIMESTAMPADD,TIMESTAMPDIFF,"
+                  + "TO_CHAR,WEEK,YEAR"));
+          break;
+        case GET_S_Q_L_KEYWORDS:
+          assertThat(connection.getMetaData().getSQLKeywords(),
+              equalTo("")); // No SQL keywords return for HSQLDB
+          break;
+        case GET_STRING_FUNCTIONS:
+          assertThat(connection.getMetaData().getStringFunctions(),
+              equalTo("ASCII,CHAR,CONCAT,DIFFERENCE,HEXTORAW,INSERT,LCASE,"
+                  + "LEFT,LENGTH,LOCATE,LTRIM,RAWTOHEX,REPEAT,REPLACE,"
+                  + "RIGHT,RTRIM,SOUNDEX,SPACE,SUBSTR,UCASE"));
+          break;
+        default:
+        }
+      }
+      connection.close();
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testTypeInfo() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      final Connection connection = getLocalConnection();
+      final ResultSet resultSet =
+          connection.getMetaData().getTypeInfo();
+      assertTrue(resultSet.next());
+      final ResultSetMetaData metaData = resultSet.getMetaData();
+      assertTrue(metaData.getColumnCount() >= 18);
+      assertEquals("TYPE_NAME", metaData.getColumnName(1));
+      assertEquals("DATA_TYPE", metaData.getColumnName(2));
+      assertEquals("PRECISION", metaData.getColumnName(3));
+      assertEquals("SQL_DATA_TYPE", metaData.getColumnName(16));
+      assertEquals("SQL_DATETIME_SUB", metaData.getColumnName(17));
+      assertEquals("NUM_PREC_RADIX", metaData.getColumnName(18));
+      resultSet.close();
+      connection.close();
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testGetTables() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      final Connection connection = getLocalConnection();
+      final ResultSet resultSet =
+              connection.getMetaData().getTables(null, "SCOTT", null, null);
+      assertEquals(13, resultSet.getMetaData().getColumnCount());
+      assertTrue(resultSet.next());
+      assertEquals("DEPT", resultSet.getString(3));
+      assertTrue(resultSet.next());
+      assertEquals("EMP", resultSet.getString(3));
+      assertTrue(resultSet.next());
+      assertEquals("BONUS", resultSet.getString(3));
+      assertTrue(resultSet.next());
+      assertEquals("SALGRADE", resultSet.getString(3));
+      resultSet.close();
+      connection.close();
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @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));
+  }
+
+  @Test public void testStatementExecuteQueryLocal() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      checkStatementExecuteQuery(getLocalConnection(), false);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testPrepareExecuteQueryLocal() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      checkStatementExecuteQuery(getLocalConnection(), true);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testInsertDrop() throws Exception {
+    final String t = AvaticaUtils.unique("TEST_TABLE2");
+    final String create =
+        String.format("create table if not exists %s ("
+            + "id int not null, "
+            + "msg varchar(3) not null)", t);
+    final String insert = String.format("insert into %s values(1, 'foo')", t);
+    Connection connection = ljs();
+    Statement statement = connection.createStatement();
+    statement.execute(create);
+
+    Statement stmt = connection.createStatement();
+    int count = stmt.executeUpdate(insert);
+    assertThat(count, is(1));
+    ResultSet resultSet = stmt.getResultSet();
+    assertThat(resultSet, nullValue());
+
+    PreparedStatement pstmt = connection.prepareStatement(insert);
+    boolean status = pstmt.execute();
+    assertThat(status, is(false));
+    int updateCount = pstmt.getUpdateCount();
+    assertThat(updateCount, is(1));
+  }
+
+  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(0));
+    }
+    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 testStatementExecuteLocal() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      checkStatementExecute(getLocalConnection(), false);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testStatementExecuteFetch() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      // Creating a > 100 rows queries to enable fetch request
+      String sql = "select * from emp cross join emp";
+      checkExecuteFetch(getLocalConnection(), sql, false, 1);
+      // PreparedStatement needed an extra fetch, as the execute will
+      // trigger the 1st fetch. Where statement execute will execute direct
+      // with results back.
+      // 1 fetch, because execute did the first fetch
+      checkExecuteFetch(getLocalConnection(), sql, true, 1);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  private void checkExecuteFetch(Connection conn, String sql, boolean isPrepare,
+    int fetchCountMatch) throws SQLException {
+    final Statement exeStatement;
+    final ResultSet results;
+    getRequestInspection().getRequestLogger().enableAndClear();
+    if (isPrepare) {
+      PreparedStatement statement = conn.prepareStatement(sql);
+      exeStatement = statement;
+      results = statement.executeQuery();
+    } else {
+      Statement statement = conn.createStatement();
+      exeStatement = statement;
+      results = statement.executeQuery(sql);
+    }
+    int count = 0;
+    int fetchCount = 0;
+    while (results.next()) {
+      count++;
+    }
+    results.close();
+    exeStatement.close();
+    List<String[]> x = getRequestInspection().getRequestLogger().getAndDisable();
+    for (String[] pair : x) {
+      if (pair[0].contains("\"request\":\"fetch")) {
+        fetchCount++;
+      }
+    }
+    assertEquals(count, 196);
+    assertEquals(fetchCountMatch, fetchCount);
+  }
+
+  @Test public void testStatementExecuteLocalMaxRow() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      checkStatementExecute(getLocalConnection(), false, 2);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testFetchSize() throws Exception {
+    Connection connection = ljs();
+
+    Statement statement = connection.createStatement();
+    statement.setFetchSize(101);
+    assertEquals(statement.getFetchSize(), 101);
+
+    PreparedStatement preparedStatement =
+        connection.prepareStatement("select * from (values (1, 'a')) as tbl1 (c1, c2)");
+    preparedStatement.setFetchSize(1);
+    assertEquals(preparedStatement.getFetchSize(), 1);
+  }
+
+  @Ignore("CALCITE-719: Refactor PreparedStatement to support setMaxRows")
+  @Test public void testStatementPrepareExecuteLocalMaxRow() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      checkStatementExecute(getLocalConnection(), true, 2);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testPrepareExecuteLocal() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      checkStatementExecute(getLocalConnection(), true);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  private void checkStatementExecute(Connection connection,
+      boolean prepare) throws SQLException {
+    checkStatementExecute(connection, prepare, 0);
+  }
+  private void checkStatementExecute(Connection connection,
+      boolean prepare, int maxRowCount) 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;
+      ps.setMaxRows(maxRowCount);
+      parameterMetaData = ps.getParameterMetaData();
+      assertTrue(ps.execute());
+      resultSet = ps.getResultSet();
+    } else {
+      statement = connection.createStatement();
+      statement.setMaxRows(maxRowCount);
+      parameterMetaData = null;
+      assertTrue(statement.execute(sql));
+      resultSet = statement.getResultSet();
+    }
+    if (parameterMetaData != null) {
+      assertThat(parameterMetaData.getParameterCount(), equalTo(0));
+    }
+    final ResultSetMetaData metaData = resultSet.getMetaData();
+    assertEquals(2, metaData.getColumnCount());
+    assertEquals("C1", metaData.getColumnName(1));
+    assertEquals("C2", metaData.getColumnName(2));
+    for (int i = 0; i < maxRowCount || (maxRowCount == 0 && i < 3); i++) {
+      assertTrue(resultSet.next());
+    }
+    assertFalse(resultSet.next());
+    resultSet.close();
+    statement.close();
+    connection.close();
+  }
+
+  @Test public void testCreateInsertUpdateDrop() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    final String t = AvaticaUtils.unique("TEST_TABLE");
+    final String drop = String.format("drop table %s if exists", t);
+    final String create = String.format("create table %s("
+        + "id int not null, "
+        + "msg varchar(3) not null)",
+        t);
+    final String insert = String.format("insert into %s values(1, 'foo')", t);
+    final String update =
+        String.format("update %s set msg='bar' where id=1", t);
+    try (Connection connection = getLocalConnection();
+        Statement statement = connection.createStatement();
+        PreparedStatement pstmt = connection.prepareStatement("values 1")) {
+      // drop
+      assertFalse(statement.execute(drop));
+      assertEquals(0, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+      try {
+        final ResultSet rs = statement.executeQuery(drop);
+        fail("expected error, got " + rs);
+      } catch (SQLException e) {
+        assertThat(e.getMessage(),
+            equalTo("Statement did not return a result set"));
+      }
+      assertEquals(0, statement.executeUpdate(drop));
+      assertEquals(0, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+
+      // create
+      assertFalse(statement.execute(create));
+      assertEquals(0, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+      assertFalse(statement.execute(drop)); // tidy up
+      try {
+        final ResultSet rs = statement.executeQuery(create);
+        fail("expected error, got " + rs);
+      } catch (SQLException e) {
+        assertThat(e.getMessage(),
+            equalTo("Statement did not return a result set"));
+      }
+      assertFalse(statement.execute(drop)); // tidy up
+      assertEquals(0, statement.executeUpdate(create));
+      assertEquals(0, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+
+      // insert
+      assertFalse(statement.execute(insert));
+      assertEquals(1, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+      try {
+        final ResultSet rs = statement.executeQuery(insert);
+        fail("expected error, got " + rs);
+      } catch (SQLException e) {
+        assertThat(e.getMessage(),
+            equalTo("Statement did not return a result set"));
+      }
+      assertEquals(1, statement.executeUpdate(insert));
+      assertEquals(1, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+
+      // update
+      assertFalse(statement.execute(update));
+      assertEquals(3, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+      try {
+        final ResultSet rs = statement.executeQuery(update);
+        fail("expected error, got " + rs);
+      } catch (SQLException e) {
+        assertThat(e.getMessage(),
+            equalTo("Statement did not return a result set"));
+      }
+      assertEquals(3, statement.executeUpdate(update));
+      assertEquals(3, statement.getUpdateCount());
+      assertNull(statement.getResultSet());
+
+      final String[] messages = {
+        "Cannot call executeQuery(String) on prepared or callable statement",
+        "Cannot call execute(String) on prepared or callable statement",
+        "Cannot call executeUpdate(String) on prepared or callable statement",
+      };
+      for (String sql : new String[]{drop, create, insert, update}) {
+        for (int i = 0; i <= 2; i++) {
+          try {
+            Object o;
+            switch (i) {
+            case 0:
+              o = pstmt.executeQuery(sql);
+              break;
+            case 1:
+              o = pstmt.execute(sql);
+              break;
+            default:
+              o = pstmt.executeUpdate(sql);
+            }
+            fail("expected error, got " + o);
+          } catch (SQLException e) {
+            assertThat(e.getMessage(), equalTo(messages[i]));
+          }
+        }
+      }
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testTypeHandling() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      final String query = "select * from EMP";
+      try (Connection cannon = canon();
+          Connection underTest = getLocalConnection();
+          Statement s1 = cannon.createStatement();
+          Statement s2 = underTest.createStatement()) {
+        assertTrue(s1.execute(query));
+        assertTrue(s2.execute(query));
+        assertResultSetsEqual(s1, s2);
+      }
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  private void assertResultSetsEqual(Statement s1, Statement s2)
+      throws SQLException {
+    final TimeZone moscowTz = TimeZone.getTimeZone("Europe/Moscow");
+    final Calendar moscowCalendar = Calendar.getInstance(moscowTz);
+    final TimeZone alaskaTz = TimeZone.getTimeZone("America/Anchorage");
+    final Calendar alaskaCalendar = Calendar.getInstance(alaskaTz);
+    try (ResultSet rs1 = s1.getResultSet();
+        ResultSet rs2 = s2.getResultSet()) {
+      assertEquals(rs1.getMetaData().getColumnCount(),
+          rs2.getMetaData().getColumnCount());
+      int colCount = rs1.getMetaData().getColumnCount();
+      while (rs1.next() && rs2.next()) {
+        for (int i = 0; i < colCount; i++) {
+          Object o1 = rs1.getObject(i + 1);
+          Object o2 = rs2.getObject(i + 1);
+          if (o1 instanceof Integer && o2 instanceof Short) {
+            // Hsqldb returns Integer for short columns; we prefer Short
+            o1 = ((Number) o1).shortValue();
+          }
+          if (o1 instanceof Integer && o2 instanceof Byte) {
+            // Hsqldb returns Integer for tinyint columns; we prefer Byte
+            o1 = ((Number) o1).byteValue();
+          }
+          if (o1 instanceof Date) {
+            Date d1 = rs1.getDate(i + 1, moscowCalendar);
+            Date d2 = rs2.getDate(i + 1, moscowCalendar);
+            assertEquals(d1, d2);
+            d1 = rs1.getDate(i + 1, alaskaCalendar);
+            d2 = rs2.getDate(i + 1, alaskaCalendar);
+            assertEquals(d1, d2);
+            d1 = rs1.getDate(i + 1, null);
+            d2 = rs2.getDate(i + 1, null);
+            assertEquals(d1, d2);
+            d1 = rs1.getDate(i + 1);
+            d2 = rs2.getDate(i + 1);
+            assertEquals(d1, d2);
+          }
+          if (o1 instanceof Timestamp) {
+            Timestamp d1 = rs1.getTimestamp(i + 1, moscowCalendar);
+            Timestamp d2 = rs2.getTimestamp(i + 1, moscowCalendar);
+            assertEquals(d1, d2);
+            d1 = rs1.getTimestamp(i + 1, alaskaCalendar);
+            d2 = rs2.getTimestamp(i + 1, alaskaCalendar);
+            assertEquals(d1, d2);
+            d1 = rs1.getTimestamp(i + 1, null);
+            d2 = rs2.getTimestamp(i + 1, null);
+            assertEquals(d1, d2);
+            d1 = rs1.getTimestamp(i + 1);
+            d2 = rs2.getTimestamp(i + 1);
+            assertEquals(d1, d2);
+          }
+          assertEquals(o1, o2);
+        }
+      }
+      assertEquals(rs1.next(), rs2.next());
+    }
+  }
+
+  /** Callback to set parameters on each prepared statement before
+   * each is executed and the result sets compared. */
+  interface PreparedStatementFunction {
+    void apply(PreparedStatement s1, PreparedStatement s2)
+        throws SQLException;
+  }
+
+  /** Callback to execute some code against a connection. */
+  interface ConnectionFunction {
+    void apply(Connection c1) throws Exception;
+  }
+
+  @Test public void testSetParameter() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      checkSetParameter("select ? from (values 1)",
+          new PreparedStatementFunction() {
+            public void apply(PreparedStatement s1, PreparedStatement s2)
+                throws SQLException {
+              final Date d = new Date(1234567890);
+              s1.setDate(1, d);
+              s2.setDate(1, d);
+            }
+          });
+      checkSetParameter("select ? from (values 1)",
+          new PreparedStatementFunction() {
+            public void apply(PreparedStatement s1, PreparedStatement s2)
+                throws SQLException {
+              final Timestamp ts = new Timestamp(123456789012L);
+              s1.setTimestamp(1, ts);
+              s2.setTimestamp(1, ts);
+            }
+          });
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  void checkSetParameter(String query, PreparedStatementFunction fn)
+      throws SQLException {
+    try (Connection cannon = canon();
+         Connection underTest = ljs();
+         PreparedStatement s1 = cannon.prepareStatement(query);
+         PreparedStatement s2 = underTest.prepareStatement(query)) {
+      fn.apply(s1, s2);
+      assertTrue(s1.execute());
+      assertTrue(s2.execute());
+      assertResultSetsEqual(s1, s2);
+    }
+  }
+
+  @Test public void testStatementLifecycle() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try (AvaticaConnection connection = (AvaticaConnection) getLocalConnection()) {
+      Map<Integer, AvaticaStatement> clientMap = connection.statementMap;
+      Cache<Integer, Object> serverMap = getLocalConnectionInternals()
+          .getRemoteStatementMap(connection);
+      // Other tests being run might leave statements in the cache.
+      // The lock guards against more statements being cached during the test.
+      serverMap.invalidateAll();
+      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());
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testConnectionIsolation() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      Cache<String, Connection> connectionMap = getLocalConnectionInternals()
+          .getRemoteConnectionMap((AvaticaConnection) getLocalConnection());
+      // Other tests being run might leave connections in the cache.
+      // The lock guards against more connections being cached during the test.
+      connectionMap.invalidateAll();
+
+      final String sql = "select * from (values (1, 'a'))";
+      assertEquals("connection cache should start empty",
+          0, connectionMap.size());
+      Connection conn1 = getLocalConnection();
+      Connection conn2 = getLocalConnection();
+      assertEquals("we now have two connections open",
+          2, connectionMap.size());
+      PreparedStatement conn1stmt1 = conn1.prepareStatement(sql);
+      assertEquals(
+          "creating a statement does not cause new connection",
+          2, connectionMap.size());
+      PreparedStatement conn2stmt1 = conn2.prepareStatement(sql);
+      assertEquals(
+          "creating a statement does not cause new connection",
+          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());
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testPrepareBindExecuteFetch() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      getRequestInspection().getRequestLogger().enableAndClear();
+      checkPrepareBindExecuteFetch(getLocalConnection());
+      List<String[]> x = getRequestInspection().getRequestLogger().getAndDisable();
+      for (String[] pair : x) {
+        System.out.println(pair[0] + "=" + pair[1]);
+      }
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  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(),
+          containsString("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();
+  }
+
+  @Test public void testPrepareBindExecuteFetchVarbinary() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      final Connection connection = getLocalConnection();
+      final String sql = "select x'de' || ? as c from (values (1, 'a'))";
+      final PreparedStatement ps =
+          connection.prepareStatement(sql);
+      final ParameterMetaData parameterMetaData = ps.getParameterMetaData();
+      assertThat(parameterMetaData.getParameterCount(), equalTo(1));
+
+      ps.setBytes(1, new byte[]{65, 0, 66});
+      final ResultSet resultSet = ps.executeQuery();
+      assertTrue(resultSet.next());
+      assertThat(resultSet.getBytes(1),
+          equalTo(new byte[]{(byte) 0xDE, 65, 0, 66}));
+      resultSet.close();
+      ps.close();
+      connection.close();
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testPrepareBindExecuteFetchDate() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      eachConnection(
+          new ConnectionFunction() {
+            public void apply(Connection c1) throws Exception {
+              checkPrepareBindExecuteFetchDate(c1);
+            }
+          }, getLocalConnection());
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  private void checkPrepareBindExecuteFetchDate(Connection connection) throws Exception {
+    final String sql0 =
+        "select cast(? as varchar(20)) as c\n"
+            + "from (values (1, 'a'))";
+    final String sql1 = "select ? + interval '2' day as c from (values (1, 'a'))";
+
+    final Date date = Date.valueOf("2015-04-08");
+    final long time = date.getTime();
+
+    PreparedStatement ps;
+    ParameterMetaData parameterMetaData;
+    ResultSet resultSet;
+
+    ps = connection.prepareStatement(sql0);
+    parameterMetaData = ps.getParameterMetaData();
+    assertThat(parameterMetaData.getParameterCount(), equalTo(1));
+    ps.setDate(1, date);
+    resultSet = ps.executeQuery();
+    assertThat(resultSet.next(), is(true));
+    assertThat(resultSet.getString(1), is("2015-04-08"));
+
+    ps.setTimestamp(1, new Timestamp(time));
+    resultSet = ps.executeQuery();
+    assertThat(resultSet.next(), is(true));
+    assertThat(resultSet.getString(1), is("2015-04-08 00:00:00.0"));
+
+    ps.setTime(1, new Time(time));
+    resultSet = ps.executeQuery();
+    assertThat(resultSet.next(), is(true));
+    assertThat(resultSet.getString(1), is("00:00:00"));
+    ps.close();
+
+    ps = connection.prepareStatement(sql1);
+    parameterMetaData = ps.getParameterMetaData();
+    assertThat(parameterMetaData.getParameterCount(), equalTo(1));
+
+    ps.setDate(1, date);
+    resultSet = ps.executeQuery();
+    assertTrue(resultSet.next());
+    assertThat(resultSet.getDate(1),
+        equalTo(new Date(time + TimeUnit.DAYS.toMillis(2))));
+    assertThat(resultSet.getTimestamp(1),
+        equalTo(new Timestamp(time + TimeUnit.DAYS.toMillis(2))));
+
+    ps.setTimestamp(1, new Timestamp(time));
+    resultSet = ps.executeQuery();
+    assertTrue(resultSet.next());
+    assertThat(resultSet.getTimestamp(1),
+        equalTo(new Timestamp(time + TimeUnit.DAYS.toMillis(2))));
+    assertThat(resultSet.getTimestamp(1),
+        equalTo(new Timestamp(time + TimeUnit.DAYS.toMillis(2))));
+
+    ps.setObject(1, new java.util.Date(time));
+    resultSet = ps.executeQuery();
+    assertTrue(resultSet.next());
+    assertThat(resultSet.getDate(1),
+        equalTo(new Date(time + TimeUnit.DAYS.toMillis(2))));
+    assertThat(resultSet.getTimestamp(1),
+        equalTo(new Timestamp(time + TimeUnit.DAYS.toMillis(2))));
+
+    resultSet.close();
+    ps.close();
+    connection.close();
+  }
+
+  @Test public void testDatabaseProperty() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      eachConnection(
+          new ConnectionFunction() {
+            public void apply(Connection c1) throws Exception {
+              checkDatabaseProperty(c1);
+            }
+          }, getLocalConnection());
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  private void checkDatabaseProperty(Connection connection)
+      throws SQLException {
+    final DatabaseMetaData metaData = connection.getMetaData();
+    assertThat(metaData.getSQLKeywords(), equalTo(""));
+    assertThat(metaData.getStringFunctions(),
+        equalTo("ASCII,CHAR,CONCAT,DIFFERENCE,HEXTORAW,INSERT,LCASE,LEFT,"
+            + "LENGTH,LOCATE,LTRIM,RAWTOHEX,REPEAT,REPLACE,RIGHT,RTRIM,SOUNDEX,"
+            + "SPACE,SUBSTR,UCASE"));
+    assertThat(metaData.getDefaultTransactionIsolation(),
+        equalTo(Connection.TRANSACTION_READ_COMMITTED));
+  }
+
+  /**
+   * 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 fully-local Protobuf service.
+   */
+  public static class QuasiRemotePBJdbcServiceFactory implements Service.Factory {
+    private static Service service;
+
+    private static RequestInspection requestInspection;
+
+    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 LoggingLocalProtobufService(localService, new ProtobufTranslationImpl());
+        requestInspection = (RequestInspection) service;
+      } catch (SQLException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Override public Service create(AvaticaConnection connection) {
+      assert null != service;
+      return service;
+    }
+  }
+
+  /**
+   * Proxy that logs all requests passed into the {@link LocalProtobufService}.
+   */
+  public static class LoggingLocalProtobufService extends LocalProtobufService
+      implements RequestInspection {
+    private static final ThreadLocal<RequestLogger> THREAD_LOG =
+        new ThreadLocal<RequestLogger>() {
+          @Override protected RequestLogger initialValue() {
+            return new RequestLogger();
+          }
+        };
+
+    public LoggingLocalProtobufService(Service service, ProtobufTranslation translation) {
+      super(service, translation);
+    }
+
+    @Override public RequestLogger getRequestLogger() {
+      return THREAD_LOG.get();
+    }
+
+    @Override public Response _apply(Request request) {
+      final RequestLogger logger = THREAD_LOG.get();
+      try {
+        String jsonRequest = JsonService.MAPPER.writeValueAsString(request);
+        logger.requestStart(jsonRequest);
+
+        Response response = super._apply(request);
+
+        String jsonResponse = JsonService.MAPPER.writeValueAsString(response);
+        logger.requestEnd(jsonRequest, jsonResponse);
+
+        return response;
+      } catch (Exception 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;
+
+    private static RequestInspection requestInspection;
+
+    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 LoggingLocalJsonService(localService);
+        requestInspection = (RequestInspection) service;
+      } catch (SQLException e) {
+        throw new RuntimeException(e);
+      }
+    }
+
+    @Override public Service create(AvaticaConnection connection) {
+      assert service != null;
+      return service;
+    }
+  }
+
+  /**
+   * Implementation that reaches into current connection state via reflection to extract certain
+   * internal information.
+   */
+  public static class QuasiRemoteJdbcServiceInternals implements ConnectionInternals {
+
+    @Override public 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);
+      // Use the explicitly class to avoid issues with LoggingLocalJsonService
+      Field remoteMetaServiceServiceF = LocalJsonService.class.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
+      @SuppressWarnings("unchecked")
+      Cache<Integer, Object> cache = (Cache<Integer, Object>) jdbcMetaStatementMapF.get(serverMeta);
+      return cache;
+    }
+
+    @Override public 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);
+      // Get the field explicitly off the correct class to avoid LocalLoggingJsonService.class
+      Field remoteMetaServiceServiceF = LocalJsonService.class.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
+      @SuppressWarnings("unchecked")
+      Cache<String, Connection> cache =
+          (Cache<String, Connection>) jdbcMetaConnectionCacheF.get(serverMeta);
+      return cache;
+    }
+  }
+
+  /**
+   * Implementation that reaches into current connection state via reflection to extract certain
+   * internal information.
+   */
+  public static class QuasiRemoteProtobufJdbcServiceInternals implements ConnectionInternals {
+
+    @Override public 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);
+      LocalProtobufService remoteMetaService =
+          (LocalProtobufService) remoteMetaServiceF.get(clientMeta);
+      // Use the explicitly class to avoid issues with LoggingLocalJsonService
+      Field remoteMetaServiceServiceF = LocalProtobufService.class.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
+      @SuppressWarnings("unchecked")
+      Cache<Integer, Object> cache = (Cache<Integer, Object>) jdbcMetaStatementMapF.get(serverMeta);
+      return cache;
+    }
+
+    @Override public 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);
+      LocalProtobufService remoteMetaService =
+          (LocalProtobufService) remoteMetaServiceF.get(clientMeta);
+      // Get the field explicitly off the correct class to avoid LocalLoggingJsonService.class
+      Field remoteMetaServiceServiceF = LocalProtobufService.class.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
+      @SuppressWarnings("unchecked")
+      Cache<String, Connection> cache =
+          (Cache<String, Connection>) jdbcMetaConnectionCacheF.get(serverMeta);
+      return cache;
+    }
+  }
+
+  /**
+   * Provides access to a log of requests.
+   */
+  interface RequestInspection {
+    RequestLogger getRequestLogger();
+  }
+
+  /** Extension to {@link LocalJsonService} that writes requests and responses
+   * into a thread-local. */
+  private static class LoggingLocalJsonService extends LocalJsonService
+      implements RequestInspection {
+    private static final ThreadLocal<RequestLogger> THREAD_LOG =
+        new ThreadLocal<RequestLogger>() {
+          @Override protected RequestLogger initialValue() {
+            return new RequestLogger();
+          }
+        };
+
+    public LoggingLocalJsonService(LocalService localService) {
+      super(localService);
+    }
+
+    @Override public String apply(String request) {
+      final RequestLogger logger = THREAD_LOG.get();
+      logger.requestStart(request);
+      final String response = super.apply(request);
+      logger.requestEnd(request, response);
+      return response;
+    }
+
+    @Override public RequestLogger getRequestLogger() {
+      return THREAD_LOG.get();
+    }
+  }
+
+  /** Logs request and response strings if enabled. */
+  private static class RequestLogger {
+    final List<String[]> requestResponses = new ArrayList<>();
+    boolean enabled;
+
+    void enableAndClear() {
+      enabled = true;
+      requestResponses.clear();
+    }
+
+    void requestStart(String request) {
+      if (enabled) {
+        requestResponses.add(new String[]{request, null});
+      }
+    }
+
+    void requestEnd(String request, String response) {
+      if (enabled) {
+        String[] last = requestResponses.get(requestResponses.size() - 1);
+        if (!request.equals(last[0])) {
+          throw new AssertionError();
+        }
+        last[1] = response;
+      }
+    }
+
+    List<String[]> getAndDisable() {
+      enabled = false;
+      return new ArrayList<>(requestResponses);
+    }
+  }
+}
+
+// End RemoteDriverTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java b/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
new file mode 100644
index 0000000..48887b4
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.junit.Test;
+
+import java.sql.SQLException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ * Unit tests for {@link JdbcMeta}.
+ */
+public class JdbcMetaTest {
+
+  @Test public void testExceptionPropagation() throws SQLException {
+    JdbcMeta meta = new JdbcMeta("url");
+    final Throwable e = new Exception();
+    final RuntimeException rte;
+    try {
+      meta.propagate(e);
+      fail("Expected an exception to be thrown");
+    } catch (RuntimeException caughtException) {
+      rte = caughtException;
+      assertThat(rte.getCause(), is(e));
+    }
+  }
+}
+
+// End JdbcMetaTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/StatementInfoTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/StatementInfoTest.java b/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/StatementInfoTest.java
new file mode 100644
index 0000000..2984692
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/jdbc/StatementInfoTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.sql.ResultSet;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.Statement;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Tests covering {@link StatementInfo}.
+ */
+public class StatementInfoTest {
+
+  @Test
+  public void testLargeOffsets() throws Exception {
+    Statement stmt = Mockito.mock(Statement.class);
+    ResultSet results = Mockito.mock(ResultSet.class);
+
+    StatementInfo info = new StatementInfo(stmt);
+
+    Mockito.when(results.relative(Integer.MAX_VALUE)).thenReturn(true, true);
+    Mockito.when(results.relative(1)).thenReturn(true);
+
+    long offset = 1L + Integer.MAX_VALUE + Integer.MAX_VALUE;
+    assertTrue(info.advanceResultSetToOffset(results, offset));
+
+    InOrder inOrder = Mockito.inOrder(results);
+
+    inOrder.verify(results, Mockito.times(2)).relative(Integer.MAX_VALUE);
+    inOrder.verify(results).relative(1);
+
+    assertEquals(offset, info.getPosition());
+  }
+
+  @Test
+  public void testNextUpdatesPosition() throws Exception {
+    Statement stmt = Mockito.mock(Statement.class);
+    ResultSet results = Mockito.mock(ResultSet.class);
+
+    StatementInfo info = new StatementInfo(stmt);
+    info.setResultSet(results);
+
+    Mockito.when(results.next()).thenReturn(true, true, true, false);
+
+    for (int i = 0; i < 3; i++) {
+      assertTrue(i + "th call of next() should return true", info.next());
+      assertEquals(info.getPosition(), i + 1);
+    }
+
+    assertFalse("Expected last next() to return false", info.next());
+    assertEquals(info.getPosition(), 4L);
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testNoMovement() throws Exception {
+    Statement stmt = Mockito.mock(Statement.class);
+    ResultSet results = Mockito.mock(ResultSet.class);
+
+    StatementInfo info = new StatementInfo(stmt);
+    info.setPosition(500);
+
+    info.advanceResultSetToOffset(results, 400);
+  }
+
+  @Test public void testResultSetGetter() throws Exception {
+    Statement stmt = Mockito.mock(Statement.class);
+    ResultSet results = Mockito.mock(ResultSet.class);
+
+    StatementInfo info = new StatementInfo(stmt);
+
+    assertFalse("ResultSet should not be initialized", info.isResultSetInitialized());
+    assertNull("ResultSet should be null", info.getResultSet());
+
+    info.setResultSet(results);
+
+    assertTrue("ResultSet should be initialized", info.isResultSetInitialized());
+    assertEquals(results, info.getResultSet());
+  }
+
+  @Test public void testCheckPositionAfterFailedRelative() throws Exception {
+    Statement stmt = Mockito.mock(Statement.class);
+    ResultSet results = Mockito.mock(ResultSet.class);
+    final long offset = 500;
+
+    StatementInfo info = new StatementInfo(stmt);
+    info.setResultSet(results);
+
+    // relative() doesn't work
+    Mockito.when(results.relative((int) offset)).thenThrow(new SQLFeatureNotSupportedException());
+    // Should fall back to next(), 500 calls to next, 1 false
+    Mockito.when(results.next()).then(new Answer<Boolean>() {
+      private long invocations = 0;
+
+      // Return true until 500, false after.
+      @Override public Boolean answer(InvocationOnMock invocation) throws Throwable {
+        invocations++;
+        if (invocations >= offset) {
+          return false;
+        }
+        return true;
+      }
+    });
+
+    info.advanceResultSetToOffset(results, offset);
+
+    // Verify correct position
+    assertEquals(offset, info.getPosition());
+    // Make sure that we actually advanced the result set
+    Mockito.verify(results, Mockito.times(500)).next();
+  }
+}
+
+// End StatementInfoTest.java

http://git-wip-us.apache.org/repos/asf/calcite/blob/5cee486f/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java
----------------------------------------------------------------------
diff --git a/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java b/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java
new file mode 100644
index 0000000..6f4c51e
--- /dev/null
+++ b/avatica/server/src/test/java/org/apache/calcite/avatica/remote/AlternatingRemoteMetaTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.remote;
+
+import org.apache.calcite.avatica.AvaticaConnection;
+import org.apache.calcite.avatica.AvaticaStatement;
+import org.apache.calcite.avatica.ConnectionConfig;
+import org.apache.calcite.avatica.ConnectionPropertiesImpl;
+import org.apache.calcite.avatica.ConnectionSpec;
+import org.apache.calcite.avatica.Meta;
+import org.apache.calcite.avatica.jdbc.JdbcMeta;
+import org.apache.calcite.avatica.server.AvaticaJsonHandler;
+import org.apache.calcite.avatica.server.HttpServer;
+import org.apache.calcite.avatica.server.Main;
+import org.apache.calcite.avatica.server.Main.HandlerFactory;
+
+import com.google.common.cache.Cache;
+
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+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;
+
+/**
+ * Tests that verify that the Driver still functions when requests are randomly bounced between
+ * more than one server.
+ */
+public class AlternatingRemoteMetaTest {
+  private static final ConnectionSpec CONNECTION_SPEC = ConnectionSpec.HSQLDB;
+
+  private static String url;
+
+  static {
+    try {
+      // Force DriverManager initialization before we hit AlternatingDriver->Driver.<clinit>
+      // Otherwise Driver.<clinit> -> DriverManager.registerDriver -> scan service provider files
+      // causes a deadlock; see [CALCITE-1060]
+      DriverManager.getDrivers();
+      DriverManager.registerDriver(new AlternatingDriver());
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  // Keep a reference to the servers we start to clean them up after
+  private static final List<HttpServer> ACTIVE_SERVERS = new ArrayList<>();
+
+  /** Factory that provides a {@link JdbcMeta}. */
+  public static class FullyRemoteJdbcMetaFactory implements Meta.Factory {
+
+    private static JdbcMeta instance = null;
+
+    private static JdbcMeta getInstance() {
+      if (instance == null) {
+        try {
+          instance = new JdbcMeta(CONNECTION_SPEC.url, CONNECTION_SPEC.username,
+              CONNECTION_SPEC.password);
+        } catch (SQLException e) {
+          throw new RuntimeException(e);
+        }
+      }
+      return instance;
+    }
+
+    @Override public Meta create(List<String> args) {
+      return getInstance();
+    }
+  }
+
+  /**
+   * AvaticaHttpClient implementation that randomly chooses among the provided URLs.
+   */
+  public static class AlternatingAvaticaHttpClient implements AvaticaHttpClient {
+    private final List<AvaticaHttpClientImpl> clients;
+    private final Random r = new Random();
+
+    public AlternatingAvaticaHttpClient(List<URL> urls) {
+      //System.out.println("Constructing clients for " + urls);
+      clients = new ArrayList<>(urls.size());
+      for (URL url : urls) {
+        clients.add(new AvaticaHttpClientImpl(url));
+      }
+    }
+
+    public byte[] send(byte[] request) {
+      AvaticaHttpClientImpl client = clients.get(r.nextInt(clients.size()));
+      //System.out.println("URL: " + client.url);
+      return client.send(request);
+    }
+
+  }
+
+  /**
+   * Driver implementation {@link AlternatingAvaticaHttpClient}.
+   */
+  public static class AlternatingDriver extends Driver {
+
+    public static final String PREFIX = "jdbc:avatica:remote-alternating:";
+
+    @Override protected String getConnectStringPrefix() {
+      return PREFIX;
+    }
+
+    @Override public Meta createMeta(AvaticaConnection connection) {
+      final ConnectionConfig config = connection.config();
+      return new RemoteMeta(connection, new RemoteService(getHttpClient(connection, config)));
+    }
+
+    @Override AvaticaHttpClient getHttpClient(AvaticaConnection connection,
+        ConnectionConfig config) {
+      return new AlternatingAvaticaHttpClient(parseUrls(config.url()));
+    }
+
+    List<URL> parseUrls(String urlStr) {
+      final List<URL> urls = new ArrayList<>();
+      final char comma = ',';
+
+      int prevIndex = 0;
+      int index = urlStr.indexOf(comma);
+      if (-1 == index) {
+        try {
+          return Collections.singletonList(new URL(urlStr));
+        } catch (MalformedURLException e) {
+          throw new RuntimeException(e);
+        }
+      }
+
+      // String split w/o regex
+      while (-1 != index) {
+        try {
+          urls.add(new URL(urlStr.substring(prevIndex, index)));
+        } catch (MalformedURLException e) {
+          throw new RuntimeException(e);
+        }
+        prevIndex = index + 1;
+        index = urlStr.indexOf(comma, prevIndex);
+      }
+
+      // Get the last one
+      try {
+        urls.add(new URL(urlStr.substring(prevIndex)));
+      } catch (MalformedURLException e) {
+        throw new RuntimeException(e);
+      }
+
+      return urls;
+    }
+
+  }
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    final String[] mainArgs = new String[] { FullyRemoteJdbcMetaFactory.class.getName() };
+
+    // Bind to '0' to pluck an ephemeral port instead of expecting a certain one to be free
+
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < 2; i++) {
+      if (sb.length() > 0) {
+        sb.append(",");
+      }
+      HttpServer jsonServer = Main.start(mainArgs, 0, new HandlerFactory() {
+        @Override public AbstractHandler createHandler(Service service) {
+          return new AvaticaJsonHandler(service);
+        }
+      });
+      ACTIVE_SERVERS.add(jsonServer);
+      sb.append("http://localhost:").append(jsonServer.getPort());
+    }
+
+    url = AlternatingDriver.PREFIX + "url=" + sb.toString();
+  }
+
+  @AfterClass public static void afterClass() throws Exception {
+    for (HttpServer server : ACTIVE_SERVERS) {
+      if (server != null) {
+        server.stop();
+      }
+    }
+  }
+
+  private static Meta getMeta(AvaticaConnection conn) throws Exception {
+    Field f = AvaticaConnection.class.getDeclaredField("meta");
+    f.setAccessible(true);
+    return (Meta) f.get(conn);
+  }
+
+  private static Meta.ExecuteResult prepareAndExecuteInternal(AvaticaConnection conn,
+    final AvaticaStatement statement, String sql, int maxRowCount) throws Exception {
+    Method m =
+        AvaticaConnection.class.getDeclaredMethod("prepareAndExecuteInternal",
+            AvaticaStatement.class, String.class, long.class);
+    m.setAccessible(true);
+    return (Meta.ExecuteResult) m.invoke(conn, statement, sql, maxRowCount);
+  }
+
+  private static Connection getConnection(JdbcMeta m, String id) throws Exception {
+    Field f = JdbcMeta.class.getDeclaredField("connectionCache");
+    f.setAccessible(true);
+    //noinspection unchecked
+    Cache<String, Connection> connectionCache = (Cache<String, Connection>) f.get(m);
+    return connectionCache.getIfPresent(id);
+  }
+
+  @Test public void testRemoteExecuteMaxRowCount() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try (AvaticaConnection conn = (AvaticaConnection) DriverManager.getConnection(url)) {
+      final AvaticaStatement statement = conn.createStatement();
+      prepareAndExecuteInternal(conn, statement,
+        "select * from (values ('a', 1), ('b', 2))", 0);
+      ResultSet rs = statement.getResultSet();
+      int count = 0;
+      while (rs.next()) {
+        count++;
+      }
+      assertEquals("Check maxRowCount=0 and ResultSets is 0 row", count, 0);
+      assertEquals("Check result set meta is still there",
+        rs.getMetaData().getColumnCount(), 2);
+      rs.close();
+      statement.close();
+      conn.close();
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  /** Test case for
+   * <a href="https://issues.apache.org/jira/browse/CALCITE-780">[CALCITE-780]
+   * HTTP error 413 when sending a long string to the Avatica server</a>. */
+  @Test public void testRemoteExecuteVeryLargeQuery() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try {
+      // Before the bug was fixed, a value over 7998 caused an HTTP 413.
+      // 16K bytes, I guess.
+      checkLargeQuery(8);
+      checkLargeQuery(240);
+      checkLargeQuery(8000);
+      checkLargeQuery(240000);
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  private void checkLargeQuery(int n) throws Exception {
+    try (AvaticaConnection conn = (AvaticaConnection) DriverManager.getConnection(url)) {
+      final AvaticaStatement statement = conn.createStatement();
+      final String frenchDisko = "It said human existence is pointless\n"
+          + "As acts of rebellious solidarity\n"
+          + "Can bring sense in this world\n"
+          + "La resistance!\n";
+      final String sql = "select '"
+          + longString(frenchDisko, n)
+          + "' as s from (values 'x')";
+      prepareAndExecuteInternal(conn, statement, sql, -1);
+      ResultSet rs = statement.getResultSet();
+      int count = 0;
+      while (rs.next()) {
+        count++;
+      }
+      assertThat(count, is(1));
+      rs.close();
+      statement.close();
+      conn.close();
+    }
+  }
+
+  /** Creates a string of exactly {@code length} characters by concatenating
+   * {@code fragment}. */
+  private static String longString(String fragment, int length) {
+    assert fragment.length() > 0;
+    final StringBuilder buf = new StringBuilder();
+    while (buf.length() < length) {
+      buf.append(fragment);
+    }
+    buf.setLength(length);
+    return buf.toString();
+  }
+
+  @Test public void testRemoteConnectionProperties() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try (AvaticaConnection conn = (AvaticaConnection) DriverManager.getConnection(url)) {
+      String id = conn.id;
+      final Map<String, ConnectionPropertiesImpl> m = ((RemoteMeta) getMeta(conn)).propsMap;
+      assertFalse("remote connection map should start ignorant", m.containsKey(id));
+      // force creating a connection object on the remote side.
+      try (final Statement stmt = conn.createStatement()) {
+        assertTrue("creating a statement starts a local object.", m.containsKey(id));
+        assertTrue(stmt.execute("select count(1) from EMP"));
+      }
+      Connection remoteConn = getConnection(FullyRemoteJdbcMetaFactory.getInstance(), id);
+      final boolean defaultRO = remoteConn.isReadOnly();
+      final boolean defaultAutoCommit = remoteConn.getAutoCommit();
+      final String defaultCatalog = remoteConn.getCatalog();
+      final String defaultSchema = remoteConn.getSchema();
+      conn.setReadOnly(!defaultRO);
+      assertTrue("local changes dirty local state", m.get(id).isDirty());
+      assertEquals("remote connection has not been touched", defaultRO, remoteConn.isReadOnly());
+      conn.setAutoCommit(!defaultAutoCommit);
+      assertEquals("remote connection has not been touched",
+          defaultAutoCommit, remoteConn.getAutoCommit());
+
+      // further interaction with the connection will force a sync
+      try (final Statement stmt = conn.createStatement()) {
+        assertEquals(!defaultAutoCommit, remoteConn.getAutoCommit());
+        assertFalse("local values should be clean", m.get(id).isDirty());
+      }
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testQuery() throws Exception {
+    ConnectionSpec.getDatabaseLock().lock();
+    try (AvaticaConnection conn = (AvaticaConnection) DriverManager.getConnection(url);
+        Statement statement = conn.createStatement()) {
+      assertFalse(statement.execute("SET SCHEMA \"SCOTT\""));
+      assertFalse(
+          statement.execute(
+              "CREATE TABLE \"FOO\"(\"KEY\" INTEGER NOT NULL, \"VALUE\" VARCHAR(10))"));
+      assertFalse(statement.execute("SET TABLE \"FOO\" READONLY FALSE"));
+
+      final int numRecords = 1000;
+      for (int i = 0; i < numRecords; i++) {
+        assertFalse(statement.execute("INSERT INTO \"FOO\" VALUES(" + i + ", '" + i + "')"));
+      }
+
+      // Make sure all the records are there that we expect
+      ResultSet results = statement.executeQuery("SELECT count(KEY) FROM FOO");
+      assertTrue(results.next());
+      assertEquals(1000, results.getInt(1));
+      assertFalse(results.next());
+
+      results = statement.executeQuery("SELECT KEY, VALUE FROM FOO ORDER BY KEY ASC");
+      for (int i = 0; i < numRecords; i++) {
+        assertTrue(results.next());
+        assertEquals(i, results.getInt(1));
+        assertEquals(Integer.toString(i), results.getString(2));
+      }
+    } finally {
+      ConnectionSpec.getDatabaseLock().unlock();
+    }
+  }
+
+  @Test public void testSingleUrlParsing() throws Exception {
+    AlternatingDriver d = new AlternatingDriver();
+    List<URL> urls = d.parseUrls("http://localhost:1234");
+    assertEquals(Arrays.asList(new URL("http://localhost:1234")), urls);
+  }
+
+  @Test public void testMultipleUrlParsing() throws Exception {
+    AlternatingDriver d = new AlternatingDriver();
+    List<URL> urls = d.parseUrls("http://localhost:1234,http://localhost:2345,"
+        + "http://localhost:3456");
+    List<URL> expectedUrls = Arrays.asList(new URL("http://localhost:1234"),
+        new URL("http://localhost:2345"), new URL("http://localhost:3456"));
+    assertEquals(expectedUrls, urls);
+  }
+}
+
+// End AlternatingRemoteMetaTest.java