org.apache.calcite.avatica.jdbc.JdbcMeta.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.calcite.avatica.jdbc.JdbcMeta.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to you under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.calcite.avatica.jdbc;

import org.apache.calcite.avatica.AvaticaParameter;
import org.apache.calcite.avatica.AvaticaUtils;
import org.apache.calcite.avatica.ColumnMetaData;
import org.apache.calcite.avatica.ConnectionPropertiesImpl;
import org.apache.calcite.avatica.Meta;
import org.apache.calcite.avatica.SqlType;
import org.apache.calcite.avatica.remote.TypedValue;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;

import java.lang.reflect.InvocationTargetException;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/** Implementation of {@link Meta} upon an existing JDBC data source. */
public class JdbcMeta implements Meta {
    private static final Log LOG = LogFactory.getLog(JdbcMeta.class);

    private static final String CONN_CACHE_KEY_BASE = "avatica.connectioncache";

    final Calendar calendar = Calendar.getInstance();

    /** Configurable connection cache settings. */
    public enum ConnectionCacheSettings {
        /** JDBC connection property for setting connection cache concurrency level. */
        CONCURRENCY_LEVEL(CONN_CACHE_KEY_BASE + ".concurrency", "10"),

        /** JDBC connection property for setting connection cache initial capacity. */
        INITIAL_CAPACITY(CONN_CACHE_KEY_BASE + ".initialcapacity", "100"),

        /** JDBC connection property for setting connection cache maximum capacity. */
        MAX_CAPACITY(CONN_CACHE_KEY_BASE + ".maxcapacity", "1000"),

        /** JDBC connection property for setting connection cache expiration duration. */
        EXPIRY_DURATION(CONN_CACHE_KEY_BASE + ".expiryduration", "10"),

        /** JDBC connection property for setting connection cache expiration unit. */
        EXPIRY_UNIT(CONN_CACHE_KEY_BASE + ".expiryunit", TimeUnit.MINUTES.name());

        private final String key;
        private final String defaultValue;

        ConnectionCacheSettings(String key, String defaultValue) {
            this.key = key;
            this.defaultValue = defaultValue;
        }

        /** The configuration key for specifying this setting. */
        public String key() {
            return key;
        }

        /** The default value for this setting. */
        public String defaultValue() {
            return defaultValue;
        }
    }

    private static final String STMT_CACHE_KEY_BASE = "avatica.statementcache";

    /** Configurable statement cache settings. */
    public enum StatementCacheSettings {
        /** JDBC connection property for setting connection cache concurrency level. */
        CONCURRENCY_LEVEL(STMT_CACHE_KEY_BASE + ".concurrency", "100"),

        /** JDBC connection property for setting connection cache initial capacity. */
        INITIAL_CAPACITY(STMT_CACHE_KEY_BASE + ".initialcapacity", "1000"),

        /** JDBC connection property for setting connection cache maximum capacity. */
        MAX_CAPACITY(STMT_CACHE_KEY_BASE + ".maxcapacity", "10000"),

        /** JDBC connection property for setting connection cache expiration duration.
         *
         * <p>Used in conjunction with {@link #EXPIRY_UNIT}.</p>
         */
        EXPIRY_DURATION(STMT_CACHE_KEY_BASE + ".expirydiration", "5"),

        /** JDBC connection property for setting connection cache expiration unit.
         *
         * <p>Used in conjunction with {@link #EXPIRY_DURATION}.</p>
         */
        EXPIRY_UNIT(STMT_CACHE_KEY_BASE + ".expiryunit", TimeUnit.MINUTES.name());

        private final String key;
        private final String defaultValue;

        StatementCacheSettings(String key, String defaultValue) {
            this.key = key;
            this.defaultValue = defaultValue;
        }

        /** The configuration key for specifying this setting. */
        public String key() {
            return key;
        }

        /** The default value for this setting. */
        public String defaultValue() {
            return defaultValue;
        }
    }

    private static final String DEFAULT_CONN_ID = UUID.fromString("00000000-0000-0000-0000-000000000000")
            .toString();

    private final String url;
    private final Properties info;
    private final Connection connection; // TODO: remove default connection
    private final Cache<String, Connection> connectionCache;
    private final Cache<Integer, StatementInfo> statementCache;

    /**
     * Convert from JDBC metadata to Avatica columns.
     */
    protected static List<ColumnMetaData> columns(ResultSetMetaData metaData) throws SQLException {
        if (metaData == null) {
            return Collections.emptyList();
        }
        final List<ColumnMetaData> columns = new ArrayList<>();
        for (int i = 1; i <= metaData.getColumnCount(); i++) {
            final SqlType sqlType = SqlType.valueOf(metaData.getColumnType(i));
            final ColumnMetaData.Rep rep = ColumnMetaData.Rep.of(sqlType.internal);
            ColumnMetaData.AvaticaType t = ColumnMetaData.scalar(metaData.getColumnType(i),
                    metaData.getColumnTypeName(i), rep);
            ColumnMetaData md = new ColumnMetaData(i - 1, metaData.isAutoIncrement(i), metaData.isCaseSensitive(i),
                    metaData.isSearchable(i), metaData.isCurrency(i), metaData.isNullable(i), metaData.isSigned(i),
                    metaData.getColumnDisplaySize(i), metaData.getColumnLabel(i), metaData.getColumnName(i),
                    metaData.getSchemaName(i), metaData.getPrecision(i), metaData.getScale(i),
                    metaData.getTableName(i), metaData.getCatalogName(i), t, metaData.isReadOnly(i),
                    metaData.isWritable(i), metaData.isDefinitelyWritable(i), metaData.getColumnClassName(i));
            columns.add(md);
        }
        return columns;
    }

    /**
     * Converts from JDBC metadata to AvaticaParameters
     */
    protected static List<AvaticaParameter> parameters(ParameterMetaData metaData) throws SQLException {
        if (metaData == null) {
            return Collections.emptyList();
        }
        final List<AvaticaParameter> params = new ArrayList<>();
        for (int i = 1; i <= metaData.getParameterCount(); i++) {
            params.add(new AvaticaParameter(metaData.isSigned(i), metaData.getPrecision(i), metaData.getScale(i),
                    metaData.getParameterType(i), metaData.getParameterTypeName(i),
                    metaData.getParameterClassName(i), "?" + i));
        }
        return params;
    }

    protected static Signature signature(ResultSetMetaData metaData, ParameterMetaData parameterMetaData,
            String sql) throws SQLException {
        return new Signature(columns(metaData), sql, parameters(parameterMetaData), null,
                CursorFactory.LIST /* LIST because JdbcResultSet#frame */);
    }

    protected static Signature signature(ResultSetMetaData metaData) throws SQLException {
        return signature(metaData, null, null);
    }

    /** Callback for {@link #connectionCache} member expiration. */
    private class ConnectionExpiryHandler implements RemovalListener<String, Connection> {

        public void onRemoval(RemovalNotification<String, Connection> notification) {
            String connectionId = notification.getKey();
            Connection doomed = notification.getValue();
            // is String.equals() more efficient?
            if (notification.getValue() == connection) {
                return;
            }
            if (LOG.isDebugEnabled()) {
                LOG.debug("Expiring connection " + connectionId + " because " + notification.getCause());
            }
            try {
                if (doomed != null) {
                    doomed.close();
                }
            } catch (Throwable t) {
                LOG.info("Exception thrown while expiring connection " + connectionId, t);
            }
        }
    }

    /** Callback for {@link #statementCache} member expiration. */
    private class StatementExpiryHandler implements RemovalListener<Integer, StatementInfo> {
        public void onRemoval(RemovalNotification<Integer, StatementInfo> notification) {
            Integer stmtId = notification.getKey();
            StatementInfo doomed = notification.getValue();
            if (doomed == null) {
                // log/throw?
                return;
            }
            if (LOG.isDebugEnabled()) {
                LOG.debug("Expiring statement " + stmtId + " because " + notification.getCause());
            }
            try {
                if (doomed.resultSet != null) {
                    doomed.resultSet.close();
                }
                if (doomed.statement != null) {
                    doomed.statement.close();
                }
            } catch (Throwable t) {
                LOG.info("Exception thrown while expiring statement " + stmtId);
            }
        }
    }

    /**
     * @param url a database url of the form
     *  <code>jdbc:<em>subprotocol</em>:<em>subname</em></code>
     */
    public JdbcMeta(String url) throws SQLException {
        this(url, new Properties());
    }

    /**
     * @param url a database url of the form
     * <code>jdbc:<em>subprotocol</em>:<em>subname</em></code>
     * @param user the database user on whose behalf the connection is being
     *   made
     * @param password the user's password
     */
    public JdbcMeta(final String url, final String user, final String password) throws SQLException {
        this(url, new Properties() {
            {
                put("user", user);
                put("password", password);
            }
        });
    }

    /**
     * @param url a database url of the form
     * <code> jdbc:<em>subprotocol</em>:<em>subname</em></code>
     * @param info a list of arbitrary string tag/value pairs as
     * connection arguments; normally at least a "user" and
     * "password" property should be included
     */
    public JdbcMeta(String url, Properties info) throws SQLException {
        this.url = url;
        this.info = info;
        this.connection = DriverManager.getConnection(url, info);

        int concurrencyLevel = Integer.parseInt(info.getProperty(ConnectionCacheSettings.CONCURRENCY_LEVEL.key(),
                ConnectionCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
        int initialCapacity = Integer.parseInt(info.getProperty(ConnectionCacheSettings.INITIAL_CAPACITY.key(),
                ConnectionCacheSettings.INITIAL_CAPACITY.defaultValue()));
        long maxCapacity = Long.parseLong(info.getProperty(ConnectionCacheSettings.MAX_CAPACITY.key(),
                ConnectionCacheSettings.MAX_CAPACITY.defaultValue()));
        long connectionExpiryDuration = Long
                .parseLong(info.getProperty(ConnectionCacheSettings.EXPIRY_DURATION.key(),
                        ConnectionCacheSettings.EXPIRY_DURATION.defaultValue()));
        TimeUnit connectionExpiryUnit = TimeUnit.valueOf(info.getProperty(ConnectionCacheSettings.EXPIRY_UNIT.key(),
                ConnectionCacheSettings.EXPIRY_UNIT.defaultValue()));
        this.connectionCache = CacheBuilder.newBuilder().concurrencyLevel(concurrencyLevel)
                .initialCapacity(initialCapacity).maximumSize(maxCapacity)
                .expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
                .removalListener(new ConnectionExpiryHandler()).build();
        if (LOG.isDebugEnabled()) {
            LOG.debug("instantiated connection cache: " + connectionCache.stats());
        }

        concurrencyLevel = Integer.parseInt(info.getProperty(StatementCacheSettings.CONCURRENCY_LEVEL.key(),
                StatementCacheSettings.CONCURRENCY_LEVEL.defaultValue()));
        initialCapacity = Integer.parseInt(info.getProperty(StatementCacheSettings.INITIAL_CAPACITY.key(),
                StatementCacheSettings.INITIAL_CAPACITY.defaultValue()));
        maxCapacity = Long.parseLong(info.getProperty(StatementCacheSettings.MAX_CAPACITY.key(),
                StatementCacheSettings.MAX_CAPACITY.defaultValue()));
        connectionExpiryDuration = Long.parseLong(info.getProperty(StatementCacheSettings.EXPIRY_DURATION.key(),
                StatementCacheSettings.EXPIRY_DURATION.defaultValue()));
        connectionExpiryUnit = TimeUnit.valueOf(info.getProperty(StatementCacheSettings.EXPIRY_UNIT.key(),
                StatementCacheSettings.EXPIRY_UNIT.defaultValue()));
        this.statementCache = CacheBuilder.newBuilder().concurrencyLevel(concurrencyLevel)
                .initialCapacity(initialCapacity).maximumSize(maxCapacity)
                .expireAfterAccess(connectionExpiryDuration, connectionExpiryUnit)
                .removalListener(new StatementExpiryHandler()).build();
        if (LOG.isDebugEnabled()) {
            LOG.debug("instantiated statement cache: " + statementCache.stats());
        }
    }

    public Map<DatabaseProperty, Object> getDatabaseProperties() {
        try {
            final Map<DatabaseProperty, Object> map = new HashMap<>();
            final DatabaseMetaData metaData = connection.getMetaData();
            for (DatabaseProperty p : DatabaseProperty.values()) {
                addProperty(map, metaData, p);
            }
            return map;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private static Object addProperty(Map<DatabaseProperty, Object> map, DatabaseMetaData metaData,
            DatabaseProperty p) throws SQLException {
        try {
            return map.put(p, p.method.invoke(metaData));
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getTables(String catalog, Pat schemaPattern, Pat tableNamePattern, List<String> typeList) {
        try {
            final ResultSet rs = connection.getMetaData().getTables(catalog, schemaPattern.s, tableNamePattern.s,
                    toArray(typeList));
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getColumns(String catalog, Pat schemaPattern, Pat tableNamePattern,
            Pat columnNamePattern) {
        try {
            final ResultSet rs = connection.getMetaData().getColumns(catalog, schemaPattern.s, tableNamePattern.s,
                    columnNamePattern.s);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getSchemas(String catalog, Pat schemaPattern) {
        try {
            final ResultSet rs = connection.getMetaData().getSchemas(catalog, schemaPattern.s);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getCatalogs() {
        try {
            final ResultSet rs = connection.getMetaData().getCatalogs();
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getTableTypes() {
        try {
            final ResultSet rs = connection.getMetaData().getTableTypes();
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getProcedures(String catalog, Pat schemaPattern, Pat procedureNamePattern) {
        try {
            final ResultSet rs = connection.getMetaData().getProcedures(catalog, schemaPattern.s,
                    procedureNamePattern.s);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getProcedureColumns(String catalog, Pat schemaPattern, Pat procedureNamePattern,
            Pat columnNamePattern) {
        try {
            final ResultSet rs = connection.getMetaData().getProcedureColumns(catalog, schemaPattern.s,
                    procedureNamePattern.s, columnNamePattern.s);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getColumnPrivileges(String catalog, String schema, String table, Pat columnNamePattern) {
        try {
            final ResultSet rs = connection.getMetaData().getColumnPrivileges(catalog, schema, table,
                    columnNamePattern.s);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getTablePrivileges(String catalog, Pat schemaPattern, Pat tableNamePattern) {
        try {
            final ResultSet rs = connection.getMetaData().getTablePrivileges(catalog, schemaPattern.s,
                    tableNamePattern.s);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getBestRowIdentifier(String catalog, String schema, String table, int scope,
            boolean nullable) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("getBestRowIdentifier catalog:" + catalog + " schema:" + schema + " table:" + table
                    + " scope:" + scope + " nullable:" + nullable);
        }
        try {
            final ResultSet rs = connection.getMetaData().getBestRowIdentifier(catalog, schema, table, scope,
                    nullable);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getVersionColumns(String catalog, String schema, String table) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("getVersionColumns catalog:" + catalog + " schema:" + schema + " table:" + table);
        }
        try {
            final ResultSet rs = connection.getMetaData().getVersionColumns(catalog, schema, table);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getPrimaryKeys(String catalog, String schema, String table) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("getPrimaryKeys catalog:" + catalog + " schema:" + schema + " table:" + table);
        }
        try {
            final ResultSet rs = connection.getMetaData().getPrimaryKeys(catalog, schema, table);
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getImportedKeys(String catalog, String schema, String table) {
        return null;
    }

    public MetaResultSet getExportedKeys(String catalog, String schema, String table) {
        return null;
    }

    public MetaResultSet getCrossReference(String parentCatalog, String parentSchema, String parentTable,
            String foreignCatalog, String foreignSchema, String foreignTable) {
        return null;
    }

    public MetaResultSet getTypeInfo() {
        try {
            final ResultSet rs = connection.getMetaData().getTypeInfo();
            return JdbcResultSet.create(DEFAULT_CONN_ID, -1, rs);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public MetaResultSet getIndexInfo(String catalog, String schema, String table, boolean unique,
            boolean approximate) {
        return null;
    }

    public MetaResultSet getUDTs(String catalog, Pat schemaPattern, Pat typeNamePattern, int[] types) {
        return null;
    }

    public MetaResultSet getSuperTypes(String catalog, Pat schemaPattern, Pat typeNamePattern) {
        return null;
    }

    public MetaResultSet getSuperTables(String catalog, Pat schemaPattern, Pat tableNamePattern) {
        return null;
    }

    public MetaResultSet getAttributes(String catalog, Pat schemaPattern, Pat typeNamePattern,
            Pat attributeNamePattern) {
        return null;
    }

    public MetaResultSet getClientInfoProperties() {
        return null;
    }

    public MetaResultSet getFunctions(String catalog, Pat schemaPattern, Pat functionNamePattern) {
        return null;
    }

    public MetaResultSet getFunctionColumns(String catalog, Pat schemaPattern, Pat functionNamePattern,
            Pat columnNamePattern) {
        return null;
    }

    public MetaResultSet getPseudoColumns(String catalog, Pat schemaPattern, Pat tableNamePattern,
            Pat columnNamePattern) {
        return null;
    }

    public Iterable<Object> createIterable(StatementHandle handle, Signature signature,
            List<TypedValue> parameterValues, Frame firstFrame) {
        return null;
    }

    protected Connection getConnection(String id) throws SQLException {
        Connection conn = connectionCache.getIfPresent(id);
        if (conn == null) {
            conn = DriverManager.getConnection(url, info);
            connectionCache.put(id, conn);
        }
        return conn;
    }

    public StatementHandle createStatement(ConnectionHandle ch) {
        try {
            final Connection conn = getConnection(ch.id);
            final Statement statement = conn.createStatement();
            final int id = System.identityHashCode(statement);
            statementCache.put(id, new StatementInfo(statement));
            StatementHandle h = new StatementHandle(ch.id, id, null);
            if (LOG.isTraceEnabled()) {
                LOG.trace("created statement " + h);
            }
            return h;
        } catch (SQLException e) {
            throw propagate(e);
        }
    }

    @Override
    public void closeStatement(StatementHandle h) {
        StatementInfo info = statementCache.getIfPresent(h.id);
        if (info == null || info.statement == null) {
            LOG.debug("client requested close unknown statement " + h);
            return;
        }
        if (LOG.isTraceEnabled()) {
            LOG.trace("closing statement " + h);
        }
        try {
            if (info.resultSet != null) {
                info.resultSet.close();
            }
            info.statement.close();
        } catch (SQLException e) {
            throw propagate(e);
        } finally {
            statementCache.invalidate(h.id);
        }
    }

    @Override
    public void closeConnection(ConnectionHandle ch) {
        Connection conn = connectionCache.getIfPresent(ch.id);
        if (conn == null) {
            LOG.debug("client requested close unknown connection " + ch);
            return;
        }
        if (LOG.isTraceEnabled()) {
            LOG.trace("closing connection " + ch);
        }
        try {
            conn.close();
        } catch (SQLException e) {
            throw propagate(e);
        } finally {
            connectionCache.invalidate(ch.id);
        }
    }

    protected void apply(Connection conn, ConnectionProperties connProps) throws SQLException {
        if (connProps.isAutoCommit() != null) {
            conn.setAutoCommit(connProps.isAutoCommit());
        }
        if (connProps.isReadOnly() != null) {
            conn.setReadOnly(connProps.isReadOnly());
        }
        if (connProps.getTransactionIsolation() != null) {
            conn.setTransactionIsolation(connProps.getTransactionIsolation());
        }
        if (connProps.getCatalog() != null) {
            conn.setCatalog(connProps.getCatalog());
        }
        if (connProps.getSchema() != null) {
            conn.setSchema(connProps.getSchema());
        }
    }

    @Override
    public ConnectionProperties connectionSync(ConnectionHandle ch, ConnectionProperties connProps) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("syncing properties for connection " + ch);
        }
        try {
            Connection conn = getConnection(ch.id);
            ConnectionPropertiesImpl props = new ConnectionPropertiesImpl(conn).merge(connProps);
            if (props.isDirty()) {
                apply(conn, props);
                props.setDirty(false);
            }
            return props;
        } catch (SQLException e) {
            throw propagate(e);
        }
    }

    private RuntimeException propagate(Throwable e) {
        if (e instanceof RuntimeException) {
            throw (RuntimeException) e;
        } else if (e instanceof Error) {
            throw (Error) e;
        } else {
            throw new RuntimeException(e);
        }
    }

    public StatementHandle prepare(ConnectionHandle ch, String sql, long maxRowCount) {
        try {
            final Connection conn = getConnection(ch.id);
            final PreparedStatement statement = conn.prepareStatement(sql);
            final int id = System.identityHashCode(statement);
            statementCache.put(id, new StatementInfo(statement));
            StatementHandle h = new StatementHandle(ch.id, id,
                    signature(statement.getMetaData(), statement.getParameterMetaData(), sql));
            if (LOG.isTraceEnabled()) {
                LOG.trace("prepared statement " + h);
            }
            return h;
        } catch (SQLException e) {
            throw propagate(e);
        }
    }

    public ExecuteResult prepareAndExecute(StatementHandle h, String sql, long maxRowCount,
            PrepareCallback callback) {
        try {
            final StatementInfo info = statementCache.getIfPresent(h.id);
            if (info == null) {
                throw new RuntimeException("Statement not found, potentially expired. " + h);
            }
            final Statement statement = info.statement;
            // Special handling of maxRowCount as JDBC 0 is unlimited, our meta 0 row
            if (maxRowCount > 0) {
                AvaticaUtils.setLargeMaxRows(statement, maxRowCount);
            } else if (maxRowCount < 0) {
                statement.setMaxRows(0);
            }
            boolean ret = statement.execute(sql);
            info.resultSet = statement.getResultSet();
            assert ret || info.resultSet == null;
            final List<MetaResultSet> resultSets = new ArrayList<>();
            if (info.resultSet == null) {
                // Create a special result set that just carries update count
                resultSets.add(
                        MetaResultSet.count(h.connectionId, h.id, AvaticaUtils.getLargeUpdateCount(statement)));
            } else {
                resultSets.add(JdbcResultSet.create(h.connectionId, h.id, info.resultSet, maxRowCount));
            }
            if (LOG.isTraceEnabled()) {
                LOG.trace("prepAndExec statement " + h);
            }
            // TODO: review client to ensure statementId is updated when appropriate
            return new ExecuteResult(resultSets);
        } catch (SQLException e) {
            throw propagate(e);
        }
    }

    public Frame fetch(StatementHandle h, List<TypedValue> parameterValues, long offset, int fetchMaxRowCount) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("fetching " + h + " offset:" + offset + " fetchMaxRowCount:" + fetchMaxRowCount);
        }
        try {
            final StatementInfo statementInfo = Objects.requireNonNull(statementCache.getIfPresent(h.id),
                    "Statement not found, potentially expired. " + h);
            if (statementInfo.resultSet == null || parameterValues != null) {
                if (statementInfo.statement instanceof PreparedStatement) {
                    final PreparedStatement preparedStatement = (PreparedStatement) statementInfo.statement;
                    if (parameterValues != null) {
                        for (int i = 0; i < parameterValues.size(); i++) {
                            TypedValue o = parameterValues.get(i);
                            preparedStatement.setObject(i + 1, o.toJdbc(calendar));
                        }
                    }
                    if (preparedStatement.execute()) {
                        statementInfo.resultSet = preparedStatement.getResultSet();
                    }
                }
            }
            if (statementInfo.resultSet == null) {
                return Frame.EMPTY;
            } else {
                return JdbcResultSet.frame(statementInfo.resultSet, offset, fetchMaxRowCount, calendar);
            }
        } catch (SQLException e) {
            throw propagate(e);
        }
    }

    private static String[] toArray(List<String> typeList) {
        if (typeList == null) {
            return new String[0];
        }
        return typeList.toArray(new String[typeList.size()]);
    }

    /** All we know about a statement. */
    private static class StatementInfo {
        final Statement statement; // sometimes a PreparedStatement
        ResultSet resultSet;

        private StatementInfo(Statement statement) {
            this.statement = Objects.requireNonNull(statement);
        }
    }
}

// End JdbcMeta.java