All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.calcite.avatica.AvaticaConnection Maven / Gradle / Ivy

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

import org.apache.calcite.avatica.Meta.MetaResultSet;
import org.apache.calcite.avatica.remote.Service.ErrorResponse;
import org.apache.calcite.avatica.remote.Service.OpenConnectionRequest;
import org.apache.calcite.avatica.remote.TypedValue;

import java.sql.Array;
import java.sql.Blob;
import java.sql.CallableStatement;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.NClob;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLClientInfoException;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.SQLXML;
import java.sql.Savepoint;
import java.sql.Statement;
import java.sql.Struct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;

/**
 * Implementation of JDBC connection
 * for the Avatica framework.
 *
 * 

Abstract to allow newer versions of JDBC to add methods. */ public abstract class AvaticaConnection implements Connection { /** The name of the sole column returned by DML statements, containing * the number of rows modified. */ public static final String ROWCOUNT_COLUMN_NAME = "ROWCOUNT"; public static final String NUM_EXECUTE_RETRIES_KEY = "avatica.statement.retries"; public static final String NUM_EXECUTE_RETRIES_DEFAULT = "5"; /** The name of the sole column returned by an EXPLAIN statement. * *

Actually Avatica does not care what this column is called, but here is * a useful place to define a suggested value. */ public static final String PLAN_COLUMN_NAME = "PLAN"; protected int statementCount; private boolean closed; private int holdability; private int networkTimeout; public final String id; public final Meta.ConnectionHandle handle; protected final UnregisteredDriver driver; protected final AvaticaFactory factory; final String url; protected final Properties info; protected final Meta meta; protected final AvaticaDatabaseMetaData metaData; public final Helper helper = Helper.INSTANCE; public final Map properties = new HashMap<>(); public final Map statementMap = new ConcurrentHashMap<>(); protected final long maxRetriesPerExecute; /** * Creates an AvaticaConnection. * *

Not public; method is called only from the driver or a derived * class.

* * @param driver Driver * @param factory Factory for JDBC objects * @param url Server URL * @param info Other connection properties */ protected AvaticaConnection(UnregisteredDriver driver, AvaticaFactory factory, String url, Properties info) { this.id = UUID.randomUUID().toString(); this.handle = new Meta.ConnectionHandle(this.id); this.driver = driver; this.factory = factory; this.url = url; this.info = info; this.meta = driver.createMeta(this); this.metaData = factory.newDatabaseMetaData(this); this.holdability = metaData.getResultSetHoldability(); this.maxRetriesPerExecute = getNumStatementRetries(info); } /** Computes the number of retries * {@link #executeQueryInternal(AvaticaStatement, Meta.Signature, Meta.Frame, QueryState)} * should retry before failing. */ long getNumStatementRetries(Properties props) { return Long.valueOf(Objects.requireNonNull(props) .getProperty(NUM_EXECUTE_RETRIES_KEY, NUM_EXECUTE_RETRIES_DEFAULT)); } /** Returns a view onto this connection's configuration properties. Code * in Avatica and derived projects should use this view rather than calling * {@link java.util.Properties#getProperty(String)}. Derived projects will * almost certainly subclass {@link ConnectionConfig} with their own * properties. */ public ConnectionConfig config() { return new ConnectionConfigImpl(info); } /** * Opens the connection on the server. */ public void openConnection() { // Open the connection on the server this.meta.openConnection(handle, OpenConnectionRequest.serializeProperties(info)); } // Connection methods public AvaticaStatement createStatement() throws SQLException { //noinspection MagicConstant return createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, holdability); } public PreparedStatement prepareStatement(String sql) throws SQLException { //noinspection MagicConstant return prepareStatement( sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, holdability); } public CallableStatement prepareCall(String sql) throws SQLException { throw helper.unsupported(); } public String nativeSQL(String sql) throws SQLException { throw helper.unsupported(); } public void setAutoCommit(boolean autoCommit) throws SQLException { meta.connectionSync(handle, new ConnectionPropertiesImpl().setAutoCommit(autoCommit)); } public boolean getAutoCommit() throws SQLException { return unbox(sync().isAutoCommit(), true); } public void commit() throws SQLException { meta.commit(handle); } public void rollback() throws SQLException { meta.rollback(handle); } public void close() throws SQLException { if (!closed) { closed = true; // Per specification, if onConnectionClose throws, this method will throw // a SQLException, but statement will still be closed. try { meta.closeConnection(handle); driver.handler.onConnectionClose(this); } catch (RuntimeException e) { throw helper.createException("While closing connection", e); } } } public boolean isClosed() throws SQLException { return closed; } public DatabaseMetaData getMetaData() throws SQLException { return metaData; } public void setReadOnly(boolean readOnly) throws SQLException { meta.connectionSync(handle, new ConnectionPropertiesImpl().setReadOnly(readOnly)); } public boolean isReadOnly() throws SQLException { return unbox(sync().isReadOnly(), true); } public void setCatalog(String catalog) throws SQLException { meta.connectionSync(handle, new ConnectionPropertiesImpl().setCatalog(catalog)); } public String getCatalog() { return sync().getCatalog(); } public void setTransactionIsolation(int level) throws SQLException { meta.connectionSync(handle, new ConnectionPropertiesImpl().setTransactionIsolation(level)); } public int getTransactionIsolation() throws SQLException { //noinspection MagicConstant return unbox(sync().getTransactionIsolation(), TRANSACTION_NONE); } public SQLWarning getWarnings() throws SQLException { return null; } public void clearWarnings() throws SQLException { // no-op since connection pooling often calls this. } public Statement createStatement( int resultSetType, int resultSetConcurrency) throws SQLException { //noinspection MagicConstant return createStatement(resultSetType, resultSetConcurrency, holdability); } public PreparedStatement prepareStatement( String sql, int resultSetType, int resultSetConcurrency) throws SQLException { //noinspection MagicConstant return prepareStatement( sql, resultSetType, resultSetConcurrency, holdability); } public CallableStatement prepareCall( String sql, int resultSetType, int resultSetConcurrency) throws SQLException { throw helper.unsupported(); } public Map> getTypeMap() throws SQLException { throw helper.unsupported(); } public void setTypeMap(Map> map) throws SQLException { throw helper.unsupported(); } public void setHoldability(int holdability) throws SQLException { if (!(holdability == ResultSet.CLOSE_CURSORS_AT_COMMIT || holdability == ResultSet.HOLD_CURSORS_OVER_COMMIT)) { throw new SQLException("invalid value"); } this.holdability = holdability; } public int getHoldability() throws SQLException { return holdability; } public Savepoint setSavepoint() throws SQLException { throw helper.unsupported(); } public Savepoint setSavepoint(String name) throws SQLException { throw helper.unsupported(); } public void rollback(Savepoint savepoint) throws SQLException { throw helper.unsupported(); } public void releaseSavepoint(Savepoint savepoint) throws SQLException { throw helper.unsupported(); } public AvaticaStatement createStatement( int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { return factory.newStatement(this, null, resultSetType, resultSetConcurrency, resultSetHoldability); } public PreparedStatement prepareStatement( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { try { final Meta.StatementHandle h = meta.prepare(handle, sql, -1); return factory.newPreparedStatement(this, h, h.signature, resultSetType, resultSetConcurrency, resultSetHoldability); } catch (RuntimeException e) { throw helper.createException("while preparing SQL: " + sql, e); } } public CallableStatement prepareCall( String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { throw helper.unsupported(); } public PreparedStatement prepareStatement( String sql, int autoGeneratedKeys) throws SQLException { throw helper.unsupported(); } public PreparedStatement prepareStatement( String sql, int[] columnIndexes) throws SQLException { throw helper.unsupported(); } public PreparedStatement prepareStatement( String sql, String[] columnNames) throws SQLException { throw helper.unsupported(); } public Clob createClob() throws SQLException { throw helper.unsupported(); } public Blob createBlob() throws SQLException { throw helper.unsupported(); } public NClob createNClob() throws SQLException { throw helper.unsupported(); } public SQLXML createSQLXML() throws SQLException { throw helper.unsupported(); } public boolean isValid(int timeout) throws SQLException { throw helper.unsupported(); } public void setClientInfo(String name, String value) throws SQLClientInfoException { throw helper.clientInfo(); } public void setClientInfo(Properties properties) throws SQLClientInfoException { throw helper.clientInfo(); } public String getClientInfo(String name) throws SQLException { throw helper.unsupported(); } public Properties getClientInfo() throws SQLException { throw helper.unsupported(); } public Array createArrayOf(String typeName, Object[] elements) throws SQLException { throw helper.unsupported(); } public Struct createStruct(String typeName, Object[] attributes) throws SQLException { throw helper.unsupported(); } public void setSchema(String schema) throws SQLException { meta.connectionSync(handle, new ConnectionPropertiesImpl().setSchema(schema)); } public String getSchema() { return sync().getSchema(); } public void abort(Executor executor) throws SQLException { throw helper.unsupported(); } public void setNetworkTimeout( Executor executor, int milliseconds) throws SQLException { this.networkTimeout = milliseconds; } public int getNetworkTimeout() throws SQLException { return networkTimeout; } public T unwrap(Class iface) throws SQLException { if (iface.isInstance(this)) { return iface.cast(this); } throw helper.createException( "does not implement '" + iface + "'"); } public boolean isWrapperFor(Class iface) throws SQLException { return iface.isInstance(this); } /** Returns the time zone of this connection. Determines the offset applied * when converting datetime values from the database into * {@link java.sql.Timestamp} values. */ public TimeZone getTimeZone() { final String timeZoneName = config().timeZone(); return timeZoneName == null ? TimeZone.getDefault() : TimeZone.getTimeZone(timeZoneName); } /** * Executes a prepared query, closing any previously open result set. * * @param statement Statement * @param signature Prepared query * @param firstFrame First frame of rows, or null if we need to execute * @param state The state used to create the given result * @return Result set * @throws java.sql.SQLException if a database error occurs */ protected ResultSet executeQueryInternal(AvaticaStatement statement, Meta.Signature signature, Meta.Frame firstFrame, QueryState state) throws SQLException { // Close the previous open result set, if there is one. Meta.Frame frame = firstFrame; Meta.Signature signature2 = signature; synchronized (statement) { if (statement.openResultSet != null) { final AvaticaResultSet rs = statement.openResultSet; statement.openResultSet = null; try { rs.close(); } catch (Exception e) { throw helper.createException( "Error while closing previous result set", e); } } try { if (statement.isWrapperFor(AvaticaPreparedStatement.class)) { final AvaticaPreparedStatement pstmt = (AvaticaPreparedStatement) statement; final Meta.ExecuteResult executeResult = meta.execute(pstmt.handle, pstmt.getParameterValues(), statement.getFetchSize()); final MetaResultSet metaResultSet = executeResult.resultSets.get(0); frame = metaResultSet.firstFrame; statement.updateCount = metaResultSet.updateCount; signature2 = executeResult.resultSets.get(0).signature; } } catch (Exception e) { e.printStackTrace(); throw helper.createException(e.getMessage(), e); } final TimeZone timeZone = getTimeZone(); if (frame == null && signature2 == null && statement.updateCount != -1) { statement.openResultSet = null; } else { // Duplicative SQL, for support non-prepared statements statement.openResultSet = factory.newResultSet(statement, state, signature2, timeZone, frame); } } // Release the monitor before executing, to give another thread the // opportunity to call cancel. try { if (statement.openResultSet != null) { statement.openResultSet.execute(); isUpdateCapable(statement); } } catch (Exception e) { throw helper.createException( "exception while executing query: " + e.getMessage(), e); } return statement.openResultSet; } /** Returns whether a a statement is capable of updates and if so, * and the statement's {@code updateCount} is still -1, proceeds to * get updateCount value from statement's resultSet. * *

Handles "ROWCOUNT" object as Number or List * * @param statement Statement * @throws SQLException on error */ private void isUpdateCapable(final AvaticaStatement statement) throws SQLException { Meta.Signature signature = statement.getSignature(); if (signature == null || signature.statementType == null) { return; } if (signature.statementType.canUpdate() && statement.updateCount == -1) { statement.openResultSet.next(); Object obj = statement.openResultSet.getObject(ROWCOUNT_COLUMN_NAME); if (obj instanceof Number) { statement.updateCount = ((Number) obj).intValue(); } else if (obj instanceof List) { statement.updateCount = ((Number) ((List) obj).get(0)).intValue(); } else { throw helper.createException("Not a valid return result."); } statement.openResultSet = null; } } protected Meta.ExecuteResult prepareAndExecuteInternal( final AvaticaStatement statement, final String sql, long maxRowCount) throws SQLException, NoSuchStatementException { final Meta.PrepareCallback callback = new Meta.PrepareCallback() { public Object getMonitor() { return statement; } public void clear() throws SQLException { if (statement.openResultSet != null) { final AvaticaResultSet rs = statement.openResultSet; statement.openResultSet = null; try { rs.close(); } catch (Exception e) { throw helper.createException( "Error while closing previous result set", e); } } } public void assign(Meta.Signature signature, Meta.Frame firstFrame, long updateCount) throws SQLException { statement.setSignature(signature); if (updateCount != -1) { statement.updateCount = updateCount; } else { final TimeZone timeZone = getTimeZone(); statement.openResultSet = factory.newResultSet(statement, new QueryState(sql), signature, timeZone, firstFrame); } } public void execute() throws SQLException { if (statement.openResultSet != null) { statement.openResultSet.execute(); isUpdateCapable(statement); } } }; return meta.prepareAndExecute(statement.handle, sql, maxRowCount, callback); } protected ResultSet createResultSet(Meta.MetaResultSet metaResultSet, QueryState state) throws SQLException { final Meta.StatementHandle h = new Meta.StatementHandle( metaResultSet.connectionId, metaResultSet.statementId, null); final AvaticaStatement statement = lookupStatement(h); ResultSet resultSet = executeQueryInternal(statement, metaResultSet.signature.sanitize(), metaResultSet.firstFrame, state); if (metaResultSet.ownStatement) { resultSet.getStatement().closeOnCompletion(); } return resultSet; } /** Creates a statement wrapper around an existing handle. */ protected AvaticaStatement lookupStatement(Meta.StatementHandle h) throws SQLException { final AvaticaStatement statement = statementMap.get(h.id); if (statement != null) { return statement; } //noinspection MagicConstant return factory.newStatement(this, Objects.requireNonNull(h), ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, holdability); } // do not make public protected static Trojan createTrojan() { return new Trojan(); } /** Converts a {@link Boolean} to a {@code boolean}, with a default value. */ private boolean unbox(Boolean b, boolean defaultValue) { return b == null ? defaultValue : b; } /** Converts an {@link Integer} to an {@code int}, with a default value. */ private int unbox(Integer i, int defaultValue) { return i == null ? defaultValue : i; } private Meta.ConnectionProperties sync() { return meta.connectionSync(handle, new ConnectionPropertiesImpl()); } /** A way to call package-protected methods. But only a sub-class of * connection can create one. */ public static class Trojan { // must be private private Trojan() { } /** A means for anyone who has a trojan to call the protected method * {@link org.apache.calcite.avatica.AvaticaResultSet#execute()}. * @throws SQLException if execute fails for some reason. */ public ResultSet execute(AvaticaResultSet resultSet) throws SQLException { return resultSet.execute(); } /** A means for anyone who has a trojan to call the protected method * {@link org.apache.calcite.avatica.AvaticaStatement#getParameterValues()}. */ public List getParameterValues(AvaticaStatement statement) { return statement.getParameterValues(); } /** A means for anyone who has a trojan to get the protected field * {@link org.apache.calcite.avatica.AvaticaConnection#meta}. */ public Meta getMeta(AvaticaConnection connection) { return connection.meta; } } /** * A Callable-like interface but without a "throws Exception". * * @param The return type from {@code call}. */ public interface CallableWithoutException { T call(); } /** * Invokes the given "callable", retrying the call when the server responds with an error * denoting that the connection is missing on the server. * * @param callable The function to invoke. * @return The value from the result of the callable. */ public T invokeWithRetries(CallableWithoutException callable) { RuntimeException lastException = null; for (int i = 0; i < maxRetriesPerExecute; i++) { try { return callable.call(); } catch (AvaticaClientRuntimeException e) { lastException = e; if (ErrorResponse.MISSING_CONNECTION_ERROR_CODE == e.getErrorCode()) { this.openConnection(); continue; } throw e; } } if (null != lastException) { throw lastException; } else { // Shouldn't ever happen. throw new IllegalStateException(); } } } // End AvaticaConnection.java