/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed 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 com.alibaba.druid.pool;

import com.alibaba.druid.pool.PreparedStatementPool.MethodType;
import com.alibaba.druid.support.logging.Log;
import com.alibaba.druid.support.logging.LogFactory;
import com.alibaba.druid.util.OracleUtils;

import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.*;
import java.util.Arrays;
import java.util.Calendar;

/**
 * @author wenshao [szujobs@hotmail.com]
 */
public class DruidPooledPreparedStatement extends DruidPooledStatement implements PreparedStatement {
    private static final Log LOG = LogFactory.getLog(DruidPooledPreparedStatement.class);

    protected final PreparedStatementHolder holder;
    protected final PreparedStatement stmt;
    private final String sql;

    private int defaultMaxFieldSize;
    private int defaultMaxRows;
    private int defaultQueryTimeout;
    private int defaultFetchDirection;
    private int defaultFetchSize;
    private int currentMaxFieldSize;
    private int currentMaxRows;
    private int currentQueryTimeout;
    private int currentFetchDirection;
    private int currentFetchSize;

    protected boolean pooled;

    public DruidPooledPreparedStatement(DruidPooledConnection conn,
                                        PreparedStatementHolder holder) throws SQLException {
        super(conn, holder.statement);
        this.stmt = holder.statement;
        this.holder = holder;
        this.sql = holder.key.sql;

        pooled = conn.getConnectionHolder().isPoolPreparedStatements();
        // Remember the defaults

        if (pooled) {
            try {
                defaultMaxFieldSize = stmt.getMaxFieldSize();
            } catch (SQLException e) {
                LOG.error("getMaxFieldSize error", e);
            }

            try {
                defaultMaxRows = stmt.getMaxRows();
            } catch (SQLException e) {
                LOG.error("getMaxRows error", e);
            }

            try {
                defaultQueryTimeout = stmt.getQueryTimeout();
            } catch (SQLException e) {
                LOG.error("getMaxRows error", e);
            }

            try {
                defaultFetchDirection = stmt.getFetchDirection();
            } catch (SQLException e) {
                LOG.error("getFetchDirection error", e);
            }

            try {
                defaultFetchSize = stmt.getFetchSize();
            } catch (SQLException e) {
                LOG.error("getFetchSize error", e);
            }
        }

        currentMaxFieldSize = defaultMaxFieldSize;
        currentMaxRows = defaultMaxRows;
        currentQueryTimeout = defaultQueryTimeout;
        currentFetchDirection = defaultFetchDirection;
        currentFetchSize = defaultFetchSize;
    }

    public PreparedStatementHolder getPreparedStatementHolder() {
        return holder;
    }

    public int getHitCount() {
        return holder.getHitCount();
    }

    public void setFetchSize(int rows) throws SQLException {
        currentFetchSize = rows;
        super.setFetchSize(rows);
    }

    public void setFetchDirection(int direction) throws SQLException {
        currentFetchDirection = direction;
        super.setFetchDirection(direction);
    }

    public void setMaxFieldSize(int max) throws SQLException {
        currentMaxFieldSize = max;
        super.setMaxFieldSize(max);
    }

    public void setMaxRows(int max) throws SQLException {
        currentMaxRows = max;
        super.setMaxRows(max);
    }

    public void setQueryTimeout(int seconds) throws SQLException {
        currentQueryTimeout = seconds;
        super.setQueryTimeout(seconds);
    }

    public String getSql() {
        return sql;
    }

    public PreparedStatementKey getKey() {
        return holder.key;
    }

    public PreparedStatement getRawPreparedStatement() {
        return stmt;
    }

    public PreparedStatement getRawStatement() {
        return stmt;
    }

    @Override
    public void close() throws SQLException {
        if (closed) {
            return;
        }

        boolean connectionClosed = conn.holder == null || conn.closed || conn.disable;
        // Reset the defaults
        if (pooled && !connectionClosed) {
            try {
                if (defaultMaxFieldSize != currentMaxFieldSize) {
                    stmt.setMaxFieldSize(defaultMaxFieldSize);
                    currentMaxFieldSize = defaultMaxFieldSize;
                }
                if (defaultMaxRows != currentMaxRows) {
                    stmt.setMaxRows(defaultMaxRows);
                    currentMaxRows = defaultMaxRows;
                }
                if (defaultQueryTimeout != currentQueryTimeout) {
                    stmt.setQueryTimeout(defaultQueryTimeout);
                    currentQueryTimeout = defaultQueryTimeout;
                }
                if (defaultFetchDirection != currentFetchDirection) {
                    stmt.setFetchDirection(defaultFetchDirection);
                    currentFetchDirection = defaultFetchDirection;
                }
                if (defaultFetchSize != currentFetchSize) {
                    stmt.setFetchSize(defaultFetchSize);
                    currentFetchSize = defaultFetchSize;
                }
            } catch (Exception e) {
                this.conn.handleException(e, null);
            }
        }

        conn.closePoolableStatement(this);
    }

    public boolean isPooled() {
        return pooled;
    }

    void closeInternal() throws SQLException {
        super.close();
    }

    void setClosed(boolean value) {
        this.closed = value;
    }

    @Override
    public ResultSet executeQuery() throws SQLException {
        checkOpen();

        incrementExecuteQueryCount();
        transactionRecord(sql);

        oracleSetRowPrefetch();

        conn.beforeExecute();
        try {
            ResultSet rs = stmt.executeQuery();

            if (rs == null) {
                return null;
            }

            DruidPooledResultSet poolableResultSet = new DruidPooledResultSet(this, rs);
            addResultSetTrace(poolableResultSet);

            return poolableResultSet;
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public int executeUpdate() throws SQLException {
        checkOpen();

        incrementExecuteUpdateCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.executeUpdate();
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public void setNull(int parameterIndex, int sqlType) throws SQLException {
        checkOpen();

        try {
            stmt.setNull(parameterIndex, sqlType);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBoolean(int parameterIndex, boolean x) throws SQLException {
        checkOpen();

        try {
            stmt.setBoolean(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setByte(int parameterIndex, byte x) throws SQLException {
        checkOpen();

        try {
            stmt.setByte(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setShort(int parameterIndex, short x) throws SQLException {
        checkOpen();

        try {
            stmt.setShort(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setInt(int parameterIndex, int x) throws SQLException {
        checkOpen();

        try {
            stmt.setInt(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setLong(int parameterIndex, long x) throws SQLException {
        checkOpen();

        try {
            stmt.setLong(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setFloat(int parameterIndex, float x) throws SQLException {
        checkOpen();

        try {
            stmt.setFloat(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setDouble(int parameterIndex, double x) throws SQLException {
        checkOpen();

        try {
            stmt.setDouble(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException {
        checkOpen();

        try {
            stmt.setBigDecimal(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setString(int parameterIndex, String x) throws SQLException {
        checkOpen();

        try {
            stmt.setString(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBytes(int parameterIndex, byte[] x) throws SQLException {
        checkOpen();

        try {
            stmt.setBytes(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setDate(int parameterIndex, Date x) throws SQLException {
        checkOpen();

        try {
            stmt.setDate(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setTime(int parameterIndex, Time x) throws SQLException {
        checkOpen();

        try {
            stmt.setTime(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
        checkOpen();

        try {
            stmt.setTimestamp(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
        checkOpen();

        try {
            stmt.setAsciiStream(parameterIndex, x, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    @Deprecated
    public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException {
        checkOpen();

        try {
            stmt.setUnicodeStream(parameterIndex, x, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
        checkOpen();

        try {
            stmt.setBinaryStream(parameterIndex, x, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void clearParameters() throws SQLException {
        checkOpen();

        try {
            stmt.clearParameters();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException {
        checkOpen();

        try {
            stmt.setObject(parameterIndex, x, targetSqlType);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setObject(int parameterIndex, Object x) throws SQLException {
        checkOpen();

        try {
            stmt.setObject(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public boolean execute() throws SQLException {
        checkOpen();

        incrementExecuteCount();
        transactionRecord(sql);

        oracleSetRowPrefetch();

        conn.beforeExecute();
        try {
            return stmt.execute();
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t);
        } finally {
            conn.afterExecute();
        }
    }

    protected void oracleSetRowPrefetch() throws SQLException {
        if (!conn.holder.dataSource.isOracle) {
            return;
        }

        if (holder.hitCount == 0) {
            return;
        }

        int fetchRowPeak = holder.fetchRowPeak;

        if (fetchRowPeak < 0) {
            return;
        }

        if (holder.getDefaultRowPrefetch() == -1) {
            int defaultRowPretch = OracleUtils.getRowPrefetch(this);
            if (defaultRowPretch != holder.getDefaultRowPrefetch()) {
                holder.setDefaultRowPrefetch(defaultRowPretch);
                holder.setRowPrefetch(defaultRowPretch);
            }
        }

        int rowPrefetch;

        if (fetchRowPeak <= 1) {
            rowPrefetch = 2;
        } else if (fetchRowPeak > holder.getDefaultRowPrefetch()) {
            rowPrefetch = holder.getDefaultRowPrefetch();
        } else {
            rowPrefetch = fetchRowPeak + 1;
        }

        if (rowPrefetch != holder.getRowPrefetch()) {
            OracleUtils.setRowPrefetch(this, rowPrefetch);
            holder.setRowPrefetch(rowPrefetch);
        }
    }

    @Override
    public void addBatch() throws SQLException {
        checkOpen();

        try {
            stmt.addBatch();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    public int[] executeBatch() throws SQLException {
        checkOpen();

        incrementExecuteBatchCount();
        transactionRecord(sql);

        conn.beforeExecute();
        try {
            return stmt.executeBatch();
        } catch (Throwable t) {
            errorCheck(t);

            throw checkException(t);
        } finally {
            conn.afterExecute();
        }
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {
        checkOpen();

        try {
            stmt.setCharacterStream(parameterIndex, reader, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setRef(int parameterIndex, Ref x) throws SQLException {
        checkOpen();

        try {
            stmt.setRef(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBlob(int parameterIndex, Blob x) throws SQLException {
        checkOpen();

        try {
            stmt.setBlob(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setClob(int parameterIndex, Clob x) throws SQLException {
        checkOpen();

        try {
            stmt.setClob(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setArray(int parameterIndex, Array x) throws SQLException {
        checkOpen();

        try {
            stmt.setArray(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public ResultSetMetaData getMetaData() throws SQLException {
        checkOpen();

        if (!conn.holder.isUnderlyingAutoCommit()) {
            conn.createTransactionInfo();
        }

        try {
            return stmt.getMetaData();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException {
        checkOpen();

        try {
            stmt.setDate(parameterIndex, x, cal);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException {
        checkOpen();

        try {
            stmt.setTime(parameterIndex, x, cal);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException {
        checkOpen();

        try {
            stmt.setTimestamp(parameterIndex, x, cal);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException {
        checkOpen();

        try {
            stmt.setNull(parameterIndex, sqlType, typeName);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setURL(int parameterIndex, URL x) throws SQLException {
        checkOpen();

        try {
            stmt.setURL(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public ParameterMetaData getParameterMetaData() throws SQLException {
        checkOpen();

        if (!conn.holder.isUnderlyingAutoCommit()) {
            conn.createTransactionInfo();
        }

        try {
            return stmt.getParameterMetaData();
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setRowId(int parameterIndex, RowId x) throws SQLException {
        checkOpen();

        try {
            stmt.setRowId(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setNString(int parameterIndex, String value) throws SQLException {
        checkOpen();

        try {
            stmt.setNString(parameterIndex, value);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {
        checkOpen();

        try {
            stmt.setNCharacterStream(parameterIndex, value, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setNClob(int parameterIndex, NClob value) throws SQLException {
        checkOpen();

        try {
            stmt.setNClob(parameterIndex, value);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
        checkOpen();

        try {
            stmt.setClob(parameterIndex, reader, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException {
        checkOpen();

        try {
            stmt.setBlob(parameterIndex, inputStream, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
        checkOpen();

        try {
            stmt.setNClob(parameterIndex, reader, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException {
        checkOpen();

        try {
            stmt.setSQLXML(parameterIndex, xmlObject);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException {
        checkOpen();

        try {
            stmt.setObject(parameterIndex, x, targetSqlType, scaleOrLength);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException {
        checkOpen();

        try {
            stmt.setAsciiStream(parameterIndex, x, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException {
        checkOpen();

        try {
            stmt.setBinaryStream(parameterIndex, x, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException {
        checkOpen();

        try {
            stmt.setCharacterStream(parameterIndex, reader, length);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException {
        checkOpen();

        try {
            stmt.setAsciiStream(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException {
        checkOpen();

        try {
            stmt.setBinaryStream(parameterIndex, x);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException {
        checkOpen();

        try {
            stmt.setCharacterStream(parameterIndex, reader);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException {
        checkOpen();

        try {
            stmt.setNCharacterStream(parameterIndex, value);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setClob(int parameterIndex, Reader reader) throws SQLException {
        checkOpen();

        try {
            stmt.setClob(parameterIndex, reader);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException {
        checkOpen();

        try {
            stmt.setBlob(parameterIndex, inputStream);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    @Override
    public void setNClob(int parameterIndex, Reader reader) throws SQLException {
        checkOpen();

        try {
            stmt.setNClob(parameterIndex, reader);
        } catch (Throwable t) {
            throw checkException(t);
        }
    }

    public static class PreparedStatementKey {
        protected final String sql;
        protected final String catalog;

        protected final MethodType methodType;

        public final int resultSetType;
        public final int resultSetConcurrency;
        public final int resultSetHoldability;
        public final int autoGeneratedKeys;
        private final int[] columnIndexes;
        private final String[] columnNames;

        public PreparedStatementKey(String sql, String catalog, MethodType methodType) throws SQLException {
            this(sql, catalog, methodType, 0, 0, 0, 0, null, null);
        }

        public PreparedStatementKey(String sql, String catalog, MethodType methodType, int resultSetType,
                                    int resultSetConcurrency) throws SQLException {
            this(sql, catalog, methodType, resultSetType, resultSetConcurrency, 0, 0, null, null);
        }

        public PreparedStatementKey(String sql, String catalog, MethodType methodType, int resultSetType,
                                    int resultSetConcurrency, int resultSetHoldability) throws SQLException {
            this(sql, catalog, methodType, resultSetType, resultSetConcurrency, resultSetHoldability, 0, null, null);
        }

        public PreparedStatementKey(String sql, String catalog, MethodType methodType, int autoGeneratedKeys)
                throws SQLException {
            this(sql, catalog, methodType, 0, 0, 0, autoGeneratedKeys, null, null);
        }

        public PreparedStatementKey(String sql, String catalog, MethodType methodType, int[] columnIndexes)
                throws SQLException {
            this(sql, catalog, methodType, 0, 0, 0, 0, columnIndexes, null);
        }

        public PreparedStatementKey(String sql, String catalog, MethodType methodType, String[] columnNames)
                throws SQLException {
            this(sql, catalog, methodType, 0, 0, 0, 0, null, columnNames);
        }

        public PreparedStatementKey(String sql, String catalog, MethodType methodType, int resultSetType,
                                    int resultSetConcurrency, int resultSetHoldability, int autoGeneratedKeys,
                                    int[] columnIndexes, String[] columnNames) throws SQLException {
            if (sql == null) {
                throw new SQLException("sql is null");
            }

            this.sql = sql;
            this.catalog = catalog;
            this.methodType = methodType;
            this.resultSetType = resultSetType;
            this.resultSetConcurrency = resultSetConcurrency;
            this.resultSetHoldability = resultSetHoldability;
            this.autoGeneratedKeys = autoGeneratedKeys;
            this.columnIndexes = columnIndexes;
            this.columnNames = columnNames;
        }

        public int getResultSetType() {
            return resultSetType;
        }

        public int getResultSetConcurrency() {
            return resultSetConcurrency;
        }

        public int getResultSetHoldability() {
            return resultSetHoldability;
        }

        @Override
        public boolean equals(Object object) {
            if (!(object instanceof PreparedStatementKey)) {
                return false;
            }

            PreparedStatementKey that = (PreparedStatementKey) object;

            if (!this.sql.equals(that.sql)) {
                return false;
            }

            if (this.catalog == null) {
                if (that.catalog != null) {
                    return false;
                }
            } else {
                if (!this.catalog.equals(that.catalog)) {
                    return false;
                }
            }

            if (this.methodType != that.methodType) {
                return false;
            }

            if (this.resultSetType != that.resultSetType) {
                return false;
            }

            if (this.resultSetConcurrency != that.resultSetConcurrency) {
                return false;
            }

            if (this.resultSetHoldability != that.resultSetHoldability) {
                return false;
            }

            if (this.autoGeneratedKeys != that.autoGeneratedKeys) {
                return false;
            }

            if (!Arrays.equals(columnIndexes, that.columnIndexes)) {
                return false;
            }

            if (!Arrays.equals(columnNames, that.columnNames)) {
                return false;
            }

            return true;
        }

        public int hashCode() {
            final int prime = 31;
            int result = 1;

            result = prime * result + ((sql == null) ? 0 : sql.hashCode());
            result = prime * result + ((catalog == null) ? 0 : catalog.hashCode());
            result = prime * result + ((methodType == null) ? 0 : methodType.hashCode());

            result = prime * result + resultSetConcurrency;
            result = prime * result + resultSetHoldability;
            result = prime * result + resultSetType;

            result = prime * result + autoGeneratedKeys;

            result = prime * result + Arrays.hashCode(columnIndexes);
            result = prime * result + Arrays.hashCode(columnNames);

            return result;
        }

        public String getSql() {
            return sql;
        }

    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        if (iface == PreparedStatementHolder.class) {
            return true;
        }

        return super.isWrapperFor(iface);
    }

    @SuppressWarnings("unchecked")
    public <T> T unwrap(Class<T> iface) throws SQLException {
        if (iface == PreparedStatementHolder.class) {
            return (T) holder;
        }
        return super.unwrap(iface);
    }
}
