/* * 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.remote.TypedValue; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** * Implementation of {@link java.sql.Statement} * for the Avatica engine. */ public abstract class AvaticaStatement implements Statement { /** The default value for {@link Statement#getFetchSize()}. */ public static final int DEFAULT_FETCH_SIZE = 100; public final AvaticaConnection connection; /** Statement id; unique within connection. */ public Meta.StatementHandle handle; protected boolean closed; /** Support for {@link #cancel()} method. */ protected final AtomicBoolean cancelFlag; /** * Support for {@link #closeOnCompletion()} method. */ protected boolean closeOnCompletion; /** * Current result set, or null if the statement is not executing anything. * Any method which modifies this member must synchronize * on the AvaticaStatement. */ protected AvaticaResultSet openResultSet; /** Current update count. Same lifecycle as {@link #openResultSet}. */ protected long updateCount; private int queryTimeoutMillis; final int resultSetType; final int resultSetConcurrency; final int resultSetHoldability; private int fetchSize = DEFAULT_FETCH_SIZE; private int fetchDirection; protected long maxRowCount = 0; private Meta.Signature signature; private final List<String> batchedSql; protected void setSignature(Meta.Signature signature) { this.signature = signature; } protected Meta.Signature getSignature() { return signature; } public Meta.StatementType getStatementType() { return signature.statementType; } /** * Creates an AvaticaStatement. * * @param connection Connection * @param h Statement handle * @param resultSetType Result set type * @param resultSetConcurrency Result set concurrency * @param resultSetHoldability Result set holdability */ protected AvaticaStatement(AvaticaConnection connection, Meta.StatementHandle h, int resultSetType, int resultSetConcurrency, int resultSetHoldability) { this(connection, h, resultSetType, resultSetConcurrency, resultSetHoldability, null); } protected AvaticaStatement(AvaticaConnection connection, Meta.StatementHandle h, int resultSetType, int resultSetConcurrency, int resultSetHoldability, Meta.Signature signature) { this.connection = Objects.requireNonNull(connection); this.resultSetType = resultSetType; this.resultSetConcurrency = resultSetConcurrency; this.resultSetHoldability = resultSetHoldability; this.signature = signature; this.closed = false; if (h == null) { final Meta.ConnectionHandle ch = connection.handle; h = connection.meta.createStatement(ch); } connection.statementMap.put(h.id, this); this.handle = h; this.batchedSql = new ArrayList<>(); try { this.cancelFlag = connection.getCancelFlag(h); } catch (NoSuchStatementException e) { throw new AssertionError("no statement", e); } } /** Returns the identifier of the statement, unique within its connection. */ public int getId() { return handle.id; } protected void checkOpen() throws SQLException { if (isClosed()) { throw AvaticaConnection.HELPER.createException("Statement closed"); } } private void checkNotPreparedOrCallable(String s) throws SQLException { if (this instanceof PreparedStatement || this instanceof CallableStatement) { throw AvaticaConnection.HELPER.createException("Cannot call " + s + " on prepared or callable statement"); } } protected void executeInternal(String sql) throws SQLException { // reset previous state before moving forward. this.updateCount = -1; try { // In JDBC, maxRowCount = 0 means no limit; in prepare it means LIMIT 0 final long maxRowCount1 = maxRowCount <= 0 ? -1 : maxRowCount; for (int i = 0; i < connection.maxRetriesPerExecute; i++) { try { @SuppressWarnings("unused") Meta.ExecuteResult x = connection.prepareAndExecuteInternal(this, sql, maxRowCount1); return; } catch (NoSuchStatementException e) { resetStatement(); } } } catch (RuntimeException e) { throw AvaticaConnection.HELPER.createException("Error while executing SQL \"" + sql + "\": " + e.getMessage(), e); } throw new RuntimeException("Failed to successfully execute query after " + connection.maxRetriesPerExecute + " attempts."); } /** * Executes a collection of updates in a single batch RPC. * * @return an array of long mapping to the update count per SQL command. */ protected long[] executeBatchInternal() throws SQLException { for (int i = 0; i < connection.maxRetriesPerExecute; i++) { try { return connection.prepareAndUpdateBatch(this, batchedSql).updateCounts; } catch (NoSuchStatementException e) { resetStatement(); } } throw new RuntimeException("Failed to successfully execute batch update after " + connection.maxRetriesPerExecute + " attempts"); } protected void resetStatement() { // Invalidate the old statement connection.statementMap.remove(handle.id); connection.flagMap.remove(handle.id); // Get a new one final Meta.ConnectionHandle ch = new Meta.ConnectionHandle(connection.id); Meta.StatementHandle h = connection.meta.createStatement(ch); // Cache it in the connection connection.statementMap.put(h.id, this); // Update the local state and try again this.handle = h; } /** * Re-initialize the ResultSet on the server with the given state. * @param state The ResultSet's state. * @param offset Offset into the desired ResultSet * @return True if the ResultSet has more results, false if there are no more results. */ protected boolean syncResults(QueryState state, long offset) throws NoSuchStatementException { return connection.meta.syncResults(handle, state, offset); } // implement Statement public boolean execute(String sql) throws SQLException { checkOpen(); checkNotPreparedOrCallable("execute(String)"); executeInternal(sql); // Result set is null for DML or DDL. // Result set is closed if user cancelled the query. return openResultSet != null && !openResultSet.isClosed(); } public ResultSet executeQuery(String sql) throws SQLException { checkOpen(); checkNotPreparedOrCallable("executeQuery(String)"); try { executeInternal(sql); if (openResultSet == null) { throw AvaticaConnection.HELPER.createException( "Statement did not return a result set"); } return openResultSet; } catch (RuntimeException e) { throw AvaticaConnection.HELPER.createException("Error while executing SQL \"" + sql + "\": " + e.getMessage(), e); } } public final int executeUpdate(String sql) throws SQLException { return AvaticaUtils.toSaturatedInt(executeLargeUpdate(sql)); } public long executeLargeUpdate(String sql) throws SQLException { checkOpen(); checkNotPreparedOrCallable("executeUpdate(String)"); executeInternal(sql); return updateCount; } public synchronized void close() throws SQLException { try { close_(); } catch (RuntimeException e) { throw AvaticaConnection.HELPER.createException("While closing statement", e); } } protected void close_() { if (!closed) { closed = true; if (openResultSet != null) { AvaticaResultSet c = openResultSet; openResultSet = null; c.close(); } try { // inform the server to close the resource connection.meta.closeStatement(handle); } finally { // make sure we don't leak on our side connection.statementMap.remove(handle.id); connection.flagMap.remove(handle.id); } // If onStatementClose throws, this method will throw an exception (later // converted to SQLException), but this statement still gets closed. connection.driver.handler.onStatementClose(this); } } public int getMaxFieldSize() throws SQLException { checkOpen(); return 0; } public void setMaxFieldSize(int max) throws SQLException { checkOpen(); if (max != 0) { throw AvaticaConnection.HELPER.createException( "illegal maxField value: " + max); } } public final int getMaxRows() throws SQLException { return AvaticaUtils.toSaturatedInt(getLargeMaxRows()); } public long getLargeMaxRows() throws SQLException { checkOpen(); return maxRowCount; } public final void setMaxRows(int maxRowCount) throws SQLException { setLargeMaxRows(maxRowCount); } public void setLargeMaxRows(long maxRowCount) throws SQLException { checkOpen(); if (maxRowCount < 0) { throw AvaticaConnection.HELPER.createException( "illegal maxRows value: " + maxRowCount); } this.maxRowCount = maxRowCount; } public void setEscapeProcessing(boolean enable) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public int getQueryTimeout() throws SQLException { checkOpen(); long timeoutSeconds = getQueryTimeoutMillis() / 1000; if (timeoutSeconds > Integer.MAX_VALUE) { return Integer.MAX_VALUE; } if (timeoutSeconds == 0 && getQueryTimeoutMillis() > 0) { // Don't return timeout=0 if e.g. timeoutMillis=500. 0 is special. return 1; } return (int) timeoutSeconds; } int getQueryTimeoutMillis() { return queryTimeoutMillis; } public void setQueryTimeout(int seconds) throws SQLException { checkOpen(); if (seconds < 0) { throw AvaticaConnection.HELPER.createException( "illegal timeout value " + seconds); } setQueryTimeoutMillis(seconds * 1000); } void setQueryTimeoutMillis(int millis) { this.queryTimeoutMillis = millis; } public synchronized void cancel() throws SQLException { checkOpen(); if (openResultSet != null) { openResultSet.cancel(); } // If there is an open result set, it probably just set the same flag. cancelFlag.compareAndSet(false, true); } public SQLWarning getWarnings() throws SQLException { checkOpen(); return null; // no warnings, since warnings are not supported } public void clearWarnings() throws SQLException { checkOpen(); // no-op since warnings are not supported } public void setCursorName(String name) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public ResultSet getResultSet() throws SQLException { checkOpen(); // NOTE: result set becomes visible in this member while // executeQueryInternal is still in progress, and before it has // finished executing. Its internal state may not be ready for API // calls. JDBC never claims to be thread-safe! (Except for calls to the // cancel method.) It is not possible to synchronize, because it would // block 'cancel'. return openResultSet; } public int getUpdateCount() throws SQLException { checkOpen(); return AvaticaUtils.toSaturatedInt(updateCount); } public long getLargeUpdateCount() throws SQLException { checkOpen(); return updateCount; } public boolean getMoreResults() throws SQLException { checkOpen(); return getMoreResults(CLOSE_CURRENT_RESULT); } public void setFetchDirection(int direction) throws SQLException { checkOpen(); this.fetchDirection = direction; } public int getFetchDirection() throws SQLException { checkOpen(); return fetchDirection; } public void setFetchSize(int rows) throws SQLException { checkOpen(); this.fetchSize = rows; } public int getFetchSize() throws SQLException { checkOpen(); return fetchSize; } public int getResultSetConcurrency() throws SQLException { checkOpen(); return resultSetConcurrency; } public int getResultSetType() throws SQLException { checkOpen(); return resultSetType; } public void addBatch(String sql) throws SQLException { checkOpen(); checkNotPreparedOrCallable("addBatch(String)"); this.batchedSql.add(Objects.requireNonNull(sql)); } public void clearBatch() throws SQLException { checkOpen(); checkNotPreparedOrCallable("clearBatch()"); this.batchedSql.clear(); } public int[] executeBatch() throws SQLException { return AvaticaUtils.toSaturatedInts(executeLargeBatch()); } public long[] executeLargeBatch() throws SQLException { checkOpen(); try { return executeBatchInternal(); } finally { // If we failed to send this batch, that's a problem for the user to handle, not us. // Make sure we always clear the statements we collected to submit in one RPC. clearBatch(); } } public AvaticaConnection getConnection() throws SQLException { checkOpen(); return connection; } public boolean getMoreResults(int current) throws SQLException { checkOpen(); switch (current) { case KEEP_CURRENT_RESULT: case CLOSE_ALL_RESULTS: throw AvaticaConnection.HELPER.unsupported(); case CLOSE_CURRENT_RESULT: break; default: throw AvaticaConnection.HELPER.createException("value " + current + " is not one of CLOSE_CURRENT_RESULT, KEEP_CURRENT_RESULT or CLOSE_ALL_RESULTS"); } if (openResultSet != null) { openResultSet.close(); } return false; } public ResultSet getGeneratedKeys() throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public int executeUpdate( String sql, int autoGeneratedKeys) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public int executeUpdate( String sql, int[] columnIndexes) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public int executeUpdate( String sql, String[] columnNames) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public boolean execute( String sql, int autoGeneratedKeys) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public boolean execute( String sql, int[] columnIndexes) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public boolean execute( String sql, String[] columnNames) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public int getResultSetHoldability() throws SQLException { checkOpen(); return resultSetHoldability; } public boolean isClosed() throws SQLException { return closed; } public void setPoolable(boolean poolable) throws SQLException { throw AvaticaConnection.HELPER.unsupported(); } public boolean isPoolable() throws SQLException { checkOpen(); return false; } // implements java.sql.Statement.closeOnCompletion (added in JDK 1.7) public void closeOnCompletion() throws SQLException { checkOpen(); closeOnCompletion = true; } // implements java.sql.Statement.isCloseOnCompletion (added in JDK 1.7) public boolean isCloseOnCompletion() throws SQLException { checkOpen(); return closeOnCompletion; } // implement Wrapper public <T> T unwrap(Class<T> iface) throws SQLException { if (iface.isInstance(this)) { return iface.cast(this); } throw AvaticaConnection.HELPER.createException( "does not implement '" + iface + "'"); } public boolean isWrapperFor(Class<?> iface) throws SQLException { return iface.isInstance(this); } /** * Executes a prepared statement. * * @param signature Parsed statement * @param isUpdate if the execute is for an update * * @return as specified by {@link java.sql.Statement#execute(String)} * @throws java.sql.SQLException if a database error occurs */ protected boolean executeInternal(Meta.Signature signature, boolean isUpdate) throws SQLException { ResultSet resultSet = executeQueryInternal(signature, isUpdate); // user may have cancelled the query if (resultSet.isClosed()) { return false; } return true; } /** * Executes a prepared query, closing any previously open result set. * * @param signature Parsed query * @param isUpdate If the execute is for an update * @return Result set * @throws java.sql.SQLException if a database error occurs */ protected ResultSet executeQueryInternal(Meta.Signature signature, boolean isUpdate) throws SQLException { return connection.executeQueryInternal(this, signature, null, null, isUpdate); } /** * Called by each child result set when it is closed. * * @param resultSet Result set or cell set */ void onResultSetClose(ResultSet resultSet) { if (closeOnCompletion) { close_(); } } /** Returns the list of values of this statement's parameters. * * <p>Called at execute time. Not a public API.</p> * * <p>The default implementation returns the empty list, because non-prepared * statements have no parameters.</p> * * @see org.apache.calcite.avatica.AvaticaConnection.Trojan#getParameterValues(AvaticaStatement) */ protected List<TypedValue> getParameterValues() { return Collections.emptyList(); } /** Returns a list of bound parameter values. * * <p>If any of the parameters have not been bound, throws. * If parameters have been bound to null, the value in the list is null. */ protected List<TypedValue> getBoundParameterValues() throws SQLException { final List<TypedValue> parameterValues = getParameterValues(); for (Object parameterValue : parameterValues) { if (parameterValue == null) { throw new SQLException("unbound parameter"); } } return parameterValues; } } // End AvaticaStatement.java