[CALCITE-2845] Avoid duplication of exception messages
diff --git a/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java b/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
index b3552f8..e28fe46 100644
--- a/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
+++ b/core/src/main/java/org/apache/calcite/avatica/AvaticaConnection.java
@@ -552,9 +552,11 @@
           statement.updateCount = metaResultSet.updateCount;
           signature2 = executeResult.resultSets.get(0).signature;
         }
+      } catch (SQLException e) {
+        // We don't add meaningful info yet, so just rethrow the original exception
+        throw e;
       } catch (Exception e) {
-        e.printStackTrace();
-        throw HELPER.createException(e.getMessage(), e);
+        throw HELPER.createException("Error while executing a prepared statement", e);
       }
 
       final TimeZone timeZone = getTimeZone();
@@ -573,9 +575,12 @@
         statement.openResultSet.execute();
         isUpdateCapable(statement);
       }
+    } catch (SQLException e) {
+      // We don't add meaningful info yet, so just rethrow the original exception
+      throw e;
     } catch (Exception e) {
       throw HELPER.createException(
-          "exception while executing query: " + e.getMessage(), e);
+          "Error while executing a resultset", e);
     }
     return statement.openResultSet;
   }
diff --git a/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java b/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java
index bb02f1f..af71381 100644
--- a/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java
+++ b/core/src/main/java/org/apache/calcite/avatica/AvaticaSqlException.java
@@ -82,7 +82,10 @@
   }
 
   void printServerStackTrace(PrintStreamOrWriter streamOrWriter) {
-    for (String serverStackTrace : this.stackTraces) {
+    List<String> traces = this.stackTraces;
+    for (int i = 0; i < traces.size(); i++) {
+      String serverStackTrace = traces.get(i);
+      streamOrWriter.println("Remote driver error #" + i + ":");
       streamOrWriter.println(serverStackTrace);
     }
   }
diff --git a/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java b/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java
index 2d3c756..ac76ce3 100644
--- a/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java
+++ b/core/src/main/java/org/apache/calcite/avatica/AvaticaStatement.java
@@ -160,8 +160,8 @@
         }
       }
     } catch (RuntimeException e) {
-      throw AvaticaConnection.HELPER.createException("Error while executing SQL \"" + sql + "\": "
-          + e.getMessage(), e);
+      throw AvaticaConnection.HELPER.createException(
+              "Error while executing SQL \"" + sql + "\"", e);
     }
 
     throw new RuntimeException("Failed to successfully execute query after "
@@ -231,8 +231,8 @@
       }
       return openResultSet;
     } catch (RuntimeException e) {
-      throw AvaticaConnection.HELPER.createException("Error while executing SQL \"" + sql + "\": "
-          + e.getMessage(), e);
+      throw AvaticaConnection.HELPER.createException(
+              "Error while executing SQL \"" + sql + "\"", e);
     }
   }
 
diff --git a/core/src/main/java/org/apache/calcite/avatica/Helper.java b/core/src/main/java/org/apache/calcite/avatica/Helper.java
index fe716db..215e2b9 100644
--- a/core/src/main/java/org/apache/calcite/avatica/Helper.java
+++ b/core/src/main/java/org/apache/calcite/avatica/Helper.java
@@ -50,7 +50,10 @@
       if (null != rte.getRpcMetadata()) {
         serverAddress = rte.getRpcMetadata().serverAddress;
       }
-      return new AvaticaSqlException(message, rte.getSqlState(), rte.getErrorCode(),
+      // Note: we don't add "e" as a cause, so we add its message to the message of
+      // the newly created exception
+      return new AvaticaSqlException(message + ". " + e.getMessage(),
+          rte.getSqlState(), rte.getErrorCode(),
           rte.getServerExceptions(), serverAddress);
     }
     return new SQLException(message, e);
diff --git a/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java b/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
index b291d4c..ab9c7c6 100644
--- a/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
+++ b/core/src/main/java/org/apache/calcite/avatica/remote/AbstractHandler.java
@@ -145,7 +145,7 @@
    * @param e The Exception to summarize.
    * @return A summary message for the Exception.
    */
-  private String getCausalChain(Exception e) {
+  private String getCausalChain(Throwable e) {
     StringBuilder sb = new StringBuilder(16);
     Throwable curr = e;
     // Could use Guava, but that would increase dependency set unnecessarily.
@@ -156,6 +156,17 @@
       String message = curr.getMessage();
       sb.append(curr.getClass().getSimpleName()).append(": ");
       sb.append(null == message ? NULL_EXCEPTION_MESSAGE : message);
+      Throwable[] suppressed = curr.getSuppressed();
+      if (suppressed.length > 0) {
+        sb.append(", suppressed: [");
+        String sep = "";
+        for (Throwable throwable : suppressed) {
+          sb.append(sep);
+          sb.append(getCausalChain(throwable));
+          sep = ", ";
+        }
+        sb.append("]");
+      }
       curr = curr.getCause();
     }
     if (sb.length() == 0) {
diff --git a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
index b8f4ea4..2a8843c 100644
--- a/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
+++ b/server/src/main/java/org/apache/calcite/avatica/jdbc/JdbcMeta.java
@@ -304,7 +304,7 @@
       }
       return map;
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -315,7 +315,7 @@
       try {
         propertyValue = p.method.invoke(metaData);
       } catch (IllegalAccessException | InvocationTargetException e) {
-        throw new RuntimeException(e);
+        throw propagate(e);
       }
     } else {
       propertyValue = p.defaultValue;
@@ -333,7 +333,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -359,7 +359,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -370,7 +370,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -380,7 +380,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -390,7 +390,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -403,7 +403,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -416,7 +416,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -429,7 +429,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -442,7 +442,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -457,7 +457,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -470,7 +470,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -483,7 +483,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -509,7 +509,7 @@
       int stmtId = registerMetaStatement(rs);
       return JdbcResultSet.create(ch.id, stmtId, rs);
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -630,7 +630,7 @@
         throw new RuntimeException("Connection already exists: " + ch.id);
       }
     } catch (SQLException e) {
-      throw new RuntimeException(e);
+      throw propagate(e);
     }
   }
 
@@ -691,14 +691,9 @@
     }
   }
 
-  RuntimeException propagate(Throwable e) {
-    if (e instanceof RuntimeException) {
-      throw (RuntimeException) e;
-    } else if (e instanceof Error) {
-      throw (Error) e;
-    } else {
-      throw new RuntimeException(e);
-    }
+  static <E extends Throwable> RuntimeException propagate(Throwable e) throws E {
+    // We have nothing to add, so just throw the original exception
+    throw (E) e;
   }
 
   public StatementHandle prepare(ConnectionHandle ch, String sql,
diff --git a/server/src/test/java/org/apache/calcite/avatica/ExceptionUtils.java b/server/src/test/java/org/apache/calcite/avatica/ExceptionUtils.java
new file mode 100644
index 0000000..eff382f
--- /dev/null
+++ b/server/src/test/java/org/apache/calcite/avatica/ExceptionUtils.java
@@ -0,0 +1,44 @@
+/*
+ * 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 java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Objects;
+
+/** Prints full stacktrace of {@link java.lang.Throwable} as a {@code String}. */
+public class ExceptionUtils {
+  private ExceptionUtils() {
+    // The constructor is extremely secret
+  }
+
+  /** Prints full stacktrace of {@link java.lang.Throwable} as a {@code String}.
+   * @param e exception
+   * @return string representation of a given exception
+   */
+  public static String toString(Exception e) {
+    //noinspection ThrowableResultOfMethodCallIgnored
+    Objects.requireNonNull(e);
+    StringWriter sw = new StringWriter();
+    PrintWriter pw = new PrintWriter(sw);
+    e.printStackTrace(pw);
+    pw.flush();
+    return sw.toString();
+  }
+}
+
+// End ExceptionUtils.java
diff --git a/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java b/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
index 66fcf8c..4d473ef 100644
--- a/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/RemoteDriverTest.java
@@ -67,11 +67,12 @@
 import java.util.UUID;
 import java.util.concurrent.Callable;
 
+import static org.apache.calcite.avatica.test.StringContainsOnce.containsStringOnce;
+
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.core.StringContains.containsString;
 import static org.hamcrest.core.StringStartsWith.startsWith;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -914,8 +915,9 @@
       fail("expected error, got " + resultSet);
     } catch (SQLException e) {
       LOG.info("Caught expected error", e);
-      assertThat(e.getMessage(),
-          containsString("exception while executing query: unbound parameter"));
+      String message = ExceptionUtils.toString(e);
+      assertThat(message,
+          containsStringOnce("unbound parameter"));
     }
 
     final ParameterMetaData parameterMetaData = ps.getParameterMetaData();
@@ -1842,8 +1844,9 @@
 
     @Override public Response _apply(Request request) {
       final RequestLogger logger = THREAD_LOG.get();
+      String jsonRequest = null;
       try {
-        String jsonRequest = JsonService.MAPPER.writeValueAsString(request);
+        jsonRequest = JsonService.MAPPER.writeValueAsString(request);
         logger.requestStart(jsonRequest);
 
         Response response = super._apply(request);
@@ -1853,7 +1856,7 @@
 
         return response;
       } catch (Exception e) {
-        throw new RuntimeException(e);
+        throw new RuntimeException("Request " + jsonRequest + " failed", e);
       }
     }
   }
diff --git a/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java b/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
index f3ebc8a..0c80f44 100644
--- a/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/jdbc/JdbcMetaTest.java
@@ -50,13 +50,11 @@
   @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));
+    } catch (Throwable caughtException) {
+      assertThat(caughtException, is(e));
     }
   }
 
diff --git a/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java b/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java
index 2a1fe80..452309c 100644
--- a/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/remote/RemoteMetaTest.java
@@ -329,10 +329,8 @@
       DriverManager.getConnection(url, "john", "doe");
       fail("expected exception");
     } catch (RuntimeException e) {
-      assertEquals("Remote driver error: RuntimeException: "
-          + "java.sql.SQLInvalidAuthorizationSpecException: invalid authorization specification"
-          + " - not found: john"
-          + " -> SQLInvalidAuthorizationSpecException: invalid authorization specification - "
+      assertEquals("Remote driver error: "
+          + "SQLInvalidAuthorizationSpecException: invalid authorization specification - "
           + "not found: john"
           + " -> HsqlException: invalid authorization specification - not found: john",
           e.getMessage());
@@ -357,9 +355,8 @@
         stmt2.executeQuery("select * from buffer");
         fail("expected exception");
       } catch (Exception e) {
-        assertEquals("Error -1 (00000) : Error while executing SQL \"select * from buffer\": "
-            + "Remote driver error: RuntimeException: java.sql.SQLSyntaxErrorException: "
-            + "user lacks privilege or object not found: BUFFER -> "
+        assertEquals("Error -1 (00000) : Error while executing SQL \"select * from buffer\". "
+            + "Remote driver error: "
             + "SQLSyntaxErrorException: user lacks privilege or object not found: BUFFER -> "
             + "HsqlException: user lacks privilege or object not found: BUFFER",
             e.getMessage());
diff --git a/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java b/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java
index 9f9914a..e11f1cb 100644
--- a/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/server/BasicAuthHttpServerTest.java
@@ -149,10 +149,8 @@
       readWriteData(url, "DISALLOWED_DB_USER", props);
       fail("Expected an exception");
     } catch (RuntimeException e) {
-      assertEquals("Remote driver error: RuntimeException: "
-          + "java.sql.SQLInvalidAuthorizationSpecException: invalid authorization specification"
-          + " - not found: USER1"
-          + " -> SQLInvalidAuthorizationSpecException: invalid authorization specification - "
+      assertEquals("Remote driver error: "
+          + "SQLInvalidAuthorizationSpecException: invalid authorization specification - "
           + "not found: USER1"
           + " -> HsqlException: invalid authorization specification - not found: USER1",
           e.getMessage());
diff --git a/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java b/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java
index a5d8e86..73b5bb5 100644
--- a/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java
+++ b/server/src/test/java/org/apache/calcite/avatica/server/DigestAuthHttpServerTest.java
@@ -163,10 +163,8 @@
       readWriteData(url, "DISALLOWED_HSQLDB_USER", props);
       fail("Expected a failure");
     } catch (RuntimeException e) {
-      assertEquals("Remote driver error: RuntimeException: "
-          + "java.sql.SQLInvalidAuthorizationSpecException: invalid authorization specification"
-          + " - not found: USER1"
-          + " -> SQLInvalidAuthorizationSpecException: invalid authorization specification - "
+      assertEquals("Remote driver error: "
+          + "SQLInvalidAuthorizationSpecException: invalid authorization specification - "
           + "not found: USER1"
           + " -> HsqlException: invalid authorization specification - not found: USER1",
           e.getMessage());
diff --git a/server/src/test/java/org/apache/calcite/avatica/test/StringContainsOnce.java b/server/src/test/java/org/apache/calcite/avatica/test/StringContainsOnce.java
new file mode 100644
index 0000000..3a55c1e
--- /dev/null
+++ b/server/src/test/java/org/apache/calcite/avatica/test/StringContainsOnce.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.calcite.avatica.test;
+
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+import org.hamcrest.core.SubstringMatcher;
+
+/**
+ * Tests if the argument is a string that contains a substring exactly once.
+ */
+public class StringContainsOnce extends SubstringMatcher {
+  public StringContainsOnce(String substring) {
+    super(substring);
+  }
+
+  @Override public void describeMismatchSafely(String item, Description mismatchDescription) {
+    int cnt = countMatches(item);
+    if (cnt == 0) {
+      mismatchDescription.appendText("pattern is not found in \"");
+    } else if (cnt == 2) {
+      mismatchDescription.appendText("pattern is present more than once in \"");
+    }
+    mismatchDescription.appendText(item);
+    mismatchDescription.appendText("\"");
+  }
+
+  @Override protected boolean evalSubstringOf(String s) {
+    return countMatches(s) == 1;
+  }
+
+  private int countMatches(String s) {
+    int indexOf = s.indexOf(substring);
+    if (indexOf < 0) {
+      return 0;
+    }
+    // There should be just a single match
+    return s.indexOf(substring, indexOf + 1) == -1 ? 1 : 2;
+  }
+
+  @Override protected String relationship() {
+    return "containing exactly once";
+  }
+
+  /**
+   * Creates a matcher that matches if the examined {@link String} contains the specified
+   * {@link String} anywhere exactly once.
+   *
+   * <p>For example:
+   * <pre>assertThat("myStringOfNote", containsStringOnce("ring"))</pre>
+   * @param substring substring
+   * @return matcher
+   */
+  @Factory
+  public static Matcher<String> containsStringOnce(String substring) {
+    return new StringContainsOnce(substring);
+  }
+}
+
+// End StringContainsOnce.java