/** * 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.drill.jdbc.test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.slf4j.LoggerFactory.getLogger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.List; import org.apache.drill.common.util.TestTools; import org.apache.drill.jdbc.AlreadyClosedSqlException; import org.apache.drill.jdbc.Driver; import org.apache.drill.jdbc.JdbcTestBase; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.slf4j.Logger; /** * Test that non-SQLException exceptions used by Drill's current version of * Avatica to indicate unsupported features are wrapped in or mapped to * SQLException exceptions. * * <p> * As of 2015-08-24, Drill's version of Avatica used non-SQLException exception * class to report that methods/features were not implemented. * </p> * <pre> * 5 UnsupportedOperationException in ArrayImpl * 29 UnsupportedOperationException in AvaticaConnection * 10 Helper.todo() (RuntimeException) in AvaticaDatabaseMetaData * 21 UnsupportedOperationException in AvaticaStatement * 4 UnsupportedOperationException in AvaticaPreparedStatement * 103 UnsupportedOperationException in AvaticaResultSet * </pre> */ public class Drill2769UnsupportedReportsUseSqlExceptionTest extends JdbcTestBase { private static final Logger logger = getLogger(Drill2769UnsupportedReportsUseSqlExceptionTest.class); @Rule public TestRule TIMEOUT = TestTools.getTimeoutRule(180_000 /* ms */); private static Connection connection; private static Statement plainStatement; private static PreparedStatement preparedStatement; // No CallableStatement. private static ResultSet resultSet; private static ResultSetMetaData resultSetMetaData; private static DatabaseMetaData databaseMetaData; @BeforeClass public static void setUpObjects() throws Exception { // (Note: Can't use JdbcTest's connect(...) for this test class.) connection = new Driver().connect("jdbc:drill:zk=local", JdbcAssert.getDefaultProperties()); plainStatement = connection.createStatement(); preparedStatement = connection.prepareStatement("VALUES 'PreparedStatement query'"); try { connection.prepareCall("VALUES 'CallableStatement query'"); fail("Test seems to be out of date. Was prepareCall(...) implemented?"); } catch (SQLException | UnsupportedOperationException e) { // Expected. } try { connection.createArrayOf("INTEGER", new Object[0]); fail("Test seems to be out of date. Were arrays implemented?"); } catch (SQLException | UnsupportedOperationException e) { // Expected. } resultSet = plainStatement.executeQuery("VALUES 'plain Statement query'"); resultSet.next(); resultSetMetaData = resultSet.getMetaData(); databaseMetaData = connection.getMetaData(); // Self-check that member variables are set: assertFalse("Test setup error", connection.isClosed()); assertFalse("Test setup error", plainStatement.isClosed()); assertFalse("Test setup error", preparedStatement.isClosed()); assertFalse("Test setup error", resultSet.isClosed()); // (No ResultSetMetaData.isClosed() or DatabaseMetaData.isClosed():) assertNotNull("Test setup error", resultSetMetaData); assertNotNull("Test setup error", databaseMetaData); } @AfterClass public static void tearDownConnection() throws Exception { connection.close(); } /** * Reflection-based checker that exceptions thrown by JDBC interfaces' * implementation methods for unsupported-operation cases are SQLExceptions * (not UnsupportedOperationExceptions). * * @param <INTF> JDBC interface type */ private static class NoNonSqlExceptionsChecker<INTF> { private final Class<INTF> jdbcIntf; private final INTF jdbcObject; private final StringBuilder failureLinesBuf = new StringBuilder(); private final StringBuilder successLinesBuf = new StringBuilder(); NoNonSqlExceptionsChecker(final Class<INTF> jdbcIntf, final INTF jdbcObject) { this.jdbcIntf = jdbcIntf; this.jdbcObject = jdbcObject; } /** * Hook/factory method to allow context to provide fresh object for each * method. Needed for Statement and PrepareStatement, whose execute... * methods can close the statement (at least given our minimal dummy * argument values). */ protected INTF getJdbcObject() throws SQLException { return jdbcObject; } /** * Gets minimal value suitable for use as actual parameter value for given * formal parameter type. */ private static Object getDummyValueForType(Class<?> type) { final Object result; if (! type.isPrimitive()) { result = null; } else { if (type == boolean.class) { result = false; } else if (type == byte.class) { result = (byte) 0; } else if (type == short.class) { result = (short) 0; } else if (type == int.class) { result = 0; } else if (type == long.class) { result = (long) 0L; } else if (type == float.class) { result = 0F; } else if (type == double.class) { result = 0.0; } else { fail("Test needs to be updated to handle type " + type); result = null; // Not executed; for "final". } } return result; } /** * Assembles method signature text for given method. */ private String makeLabel(Method method) { String methodLabel; methodLabel = jdbcIntf.getSimpleName() + "." + method.getName() + "("; boolean first = true; for (Class<?> paramType : method.getParameterTypes()) { if (! first) { methodLabel += ", "; } first = false; methodLabel += paramType.getSimpleName(); } methodLabel += ")"; return methodLabel; } /** * Assembles (minimal) arguments array for given method. */ private Object[] makeArgs(Method method) { final List<Object> argsList = new ArrayList<>(); for (Class<?> paramType : method.getParameterTypes()) { argsList.add(getDummyValueForType(paramType)); } Object[] argsArray = argsList.toArray(); return argsArray; } /** * Tests one method. * (Disturbs members set by makeArgsAndLabel, but those shouldn't be used * except by this method.) */ private void testOneMethod(Method method) { final String methodLabel = makeLabel(method); try { final INTF jdbcObject; try { jdbcObject = getJdbcObject(); } catch (SQLException e) { fail("Unexpected exception: " + e + " from getJdbcObject()"); throw new RuntimeException("DUMMY; so compiler know block throws"); } // See if method throws exception: method.invoke(jdbcObject, makeArgs(method)); // If here, method didn't throw--check if it's an expected non-throwing // method (e.g., an isClosed). (If not, report error.) final String resultLine = "- " + methodLabel + " didn't throw\n"; successLinesBuf.append(resultLine); } catch (InvocationTargetException wrapperEx) { final Throwable cause = wrapperEx.getCause(); final String resultLine = "- " + methodLabel + " threw <" + cause + ">\n"; if (SQLException.class.isAssignableFrom(cause.getClass()) && ! AlreadyClosedSqlException.class.isAssignableFrom(cause.getClass()) ) { // Good case--almost any exception should be SQLException or subclass // (but make sure not accidentally closed). successLinesBuf.append(resultLine); } else if (NullPointerException.class == cause.getClass() && (method.getName().equals("isWrapperFor") || method.getName().equals("unwrap"))) { // Known good-enough case--these methods throw NullPointerException // because of the way we call them (with null) and the way Avatica // code implements them. successLinesBuf.append(resultLine); } else if (isOkaySpecialCaseException(method, cause)) { successLinesBuf.append(resultLine); } else { final String badResultLine = "- " + methodLabel + " threw <" + cause + "> instead" + " of a " + SQLException.class.getSimpleName() + "\n"; logger.trace("Failure: " + resultLine); failureLinesBuf.append(badResultLine); } } catch (IllegalAccessException | IllegalArgumentException e) { fail("Unexpected exception: " + e + ", cause = " + e.getCause() + " from " + method); } } public void testMethods() { for (Method method : jdbcIntf.getMethods()) { final String methodLabel = makeLabel(method); if ("close".equals(method.getName())) { logger.debug("Skipping (because closes): " + methodLabel); } /* Uncomment to suppress calling DatabaseMetaData.getColumns(...), which sometimes takes about 2 minutes, and other DatabaseMetaData methods that query, collectively taking a while too: else if (DatabaseMetaData.class == jdbcIntf && "getColumns".equals(method.getName())) { logger.debug("Skipping (because really slow): " + methodLabel); } else if (DatabaseMetaData.class == jdbcIntf && ResultSet.class == method.getReturnType()) { logger.debug("Skipping (because a bit slow): " + methodLabel); } */ else { logger.debug("Testing method " + methodLabel); testOneMethod(method); } } } /** * Reports whether it's okay if given method throw given exception (that is * not preferred AlreadyClosedException with regular message). */ protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { return false; } public boolean hadAnyFailures() { return 0 != failureLinesBuf.length(); } public String getFailureLines() { return failureLinesBuf.toString(); } public String getSuccessLines() { return successLinesBuf.toString(); } public String getReport() { final String report = "Failures:\n" + getFailureLines() + "(Successes:\n" + getSuccessLines() + ")"; return report; } } // class NoNonSqlExceptionsChecker<INTF> @Test public void testConnectionMethodsThrowRight() { NoNonSqlExceptionsChecker<Connection> checker = new NoNonSqlExceptionsChecker<Connection>(Connection.class, connection); checker.testMethods(); if (checker.hadAnyFailures()) { System.err.println(checker.getReport()); fail("Non-SQLException exception error(s): \n" + checker.getReport()); } } private static class PlainStatementChecker extends NoNonSqlExceptionsChecker<Statement> { private final Connection factoryConnection; PlainStatementChecker(Connection factoryConnection) { super(Statement.class, null); this.factoryConnection = factoryConnection; } @Override protected Statement getJdbcObject() throws SQLException { return factoryConnection.createStatement(); } @Override protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { // New Java 8 method not supported by Avatica return method.getName().equals( "executeLargeBatch" ); } } // class PlainStatementChecker @Test public void testPlainStatementMethodsThrowRight() { NoNonSqlExceptionsChecker<Statement> checker = new PlainStatementChecker(connection); checker.testMethods(); if (checker.hadAnyFailures()) { fail("Non-SQLException exception error(s): \n" + checker.getReport()); } } private static class PreparedStatementChecker extends NoNonSqlExceptionsChecker<PreparedStatement> { private final Connection factoryConnection; PreparedStatementChecker(Connection factoryConnection) { super(PreparedStatement.class, null); this.factoryConnection = factoryConnection; } @Override protected PreparedStatement getJdbcObject() throws SQLException { return factoryConnection.prepareStatement("VALUES 1"); } @Override protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { // New Java 8 method not supported by Avatica return method.getName().equals( "executeLargeBatch" ); } } // class PlainStatementChecker @Test public void testPreparedStatementMethodsThrowRight() { NoNonSqlExceptionsChecker<PreparedStatement> checker = new PreparedStatementChecker(connection); checker.testMethods(); if (checker.hadAnyFailures()) { fail("Non-SQLException exception error(s): \n" + checker.getReport()); } } @Test public void testResultSetMethodsThrowRight() { NoNonSqlExceptionsChecker<ResultSet> checker = new NoNonSqlExceptionsChecker<ResultSet>(ResultSet.class, resultSet); checker.testMethods(); if (checker.hadAnyFailures()) { fail("Non-SQLException exception error(s): \n" + checker.getReport()); } } @Test public void testResultSetMetaDataMethodsThrowRight() { NoNonSqlExceptionsChecker<ResultSetMetaData> checker = new NoNonSqlExceptionsChecker<ResultSetMetaData>(ResultSetMetaData.class, resultSetMetaData); checker.testMethods(); if (checker.hadAnyFailures()) { fail("Non-SQLException exception error(s): \n" + checker.getReport()); } } @Test public void testDatabaseMetaDataMethodsThrowRight() { NoNonSqlExceptionsChecker<DatabaseMetaData> checker = new NoNonSqlExceptionsChecker<DatabaseMetaData>(DatabaseMetaData.class, databaseMetaData); checker.testMethods(); if (checker.hadAnyFailures()) { fail("Non-SQLException exception error(s): \n" + checker.getReport()); } } }