/*
* Copyright © 2014 Cask Data, Inc.
*
* 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 co.cask.cdap.explore.jdbc;
import co.cask.cdap.explore.client.ExploreClient;
import co.cask.cdap.explore.client.ExploreExecutionResult;
import co.cask.cdap.explore.service.HandleNotFoundException;
import co.cask.cdap.explore.service.UnexpectedQueryStatusException;
import co.cask.cdap.proto.Id;
import co.cask.cdap.proto.QueryStatus;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.ListenableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
/**
* CDAP JDBC Statement. At most one {@link ExploreResultSet} object can be produced by instances
* of this class.
*/
public class ExploreStatement implements Statement {
private static final Logger LOG = LoggerFactory.getLogger(ExploreStatement.class);
private int fetchSize = 1000;
/**
* We need to keep a reference to the result set to support the following:
* <code>
* statement.execute(String sql);
* statement.getResultSet();
* </code>.
*/
private ResultSet resultSet = null;
/**
* Sets the limit for the maximum number of rows that any ResultSet object produced by this
* Statement can contain to the given number. If the limit is exceeded, the excess rows
* are silently dropped. The value must be >= 0, and 0 means there is not limit.
*/
private int maxRows = 0;
/**
* Add SQLWarnings to the warningChain if needed.
*/
private SQLWarning warningChain = null;
private volatile boolean isClosed = false;
private volatile ListenableFuture<ExploreExecutionResult> futureResults = null;
private Connection connection;
private ExploreClient exploreClient;
private final Id.Namespace namespace;
ExploreStatement(Connection connection, ExploreClient exploreClient, String namespace) {
this.connection = connection;
this.exploreClient = exploreClient;
this.namespace = Id.Namespace.from(namespace);
}
@Override
public ResultSet executeQuery(String sql) throws SQLException {
if (isClosed) {
throw new SQLException("Can't execute after statement has been closed");
}
if (!execute(sql)) {
throw new SQLException("The query did not generate a result set!");
}
return resultSet;
}
/**
* Executes a query and wait until it is finished, but does not close the session.
*/
@Override
public boolean execute(String sql) throws SQLException {
if (isClosed) {
throw new SQLException("Can't execute after statement has been closed");
}
if (resultSet != null) {
// As requested by the Statement interface javadoc, "All execution methods in the Statement interface
// implicitly close a statement's current ResultSet object if an open one exists"
resultSet.close();
resultSet = null;
}
futureResults = exploreClient.submit(namespace, sql);
try {
resultSet = new ExploreResultSet(futureResults.get(), this, maxRows);
// NOTE: Javadoc states: "returns false if the first result is an update count or there is no result"
// Here we have a result, it may contain rows or may be empty, but it exists.
return true;
} catch (InterruptedException e) {
LOG.error("Caught exception", e);
Thread.currentThread().interrupt();
return false;
} catch (ExecutionException e) {
Throwable t = Throwables.getRootCause(e);
if (t instanceof HandleNotFoundException) {
LOG.error("Error executing query", e);
throw new SQLException("Unknown state");
} else if (t instanceof UnexpectedQueryStatusException) {
UnexpectedQueryStatusException sE = (UnexpectedQueryStatusException) t;
if (QueryStatus.OpStatus.CANCELED.equals(sE.getStatus())) {
// The query execution may have been canceled without calling futureResults.cancel(), using the right
// REST endpoint with the handle for eg.
return false;
}
throw new SQLException(String.format("Statement '%s' execution did not finish successfully. " +
"Got final state - %s", sql, sE.getStatus().toString()));
}
LOG.error("Caught exception", e);
throw new SQLException(Throwables.getRootCause(e));
} catch (CancellationException e) {
// If futureResults has been cancelled
return false;
}
}
@Override
public ResultSet getResultSet() throws SQLException {
return resultSet;
}
@Override
public int getMaxRows() throws SQLException {
return maxRows;
}
@Override
public void setMaxRows(int max) throws SQLException {
if (max < 0) {
throw new SQLException("max rows must be >= 0");
}
maxRows = max;
}
@Override
public int getUpdateCount() throws SQLException {
return -1;
}
@Override
public void setFetchSize(int i) throws SQLException {
fetchSize = i;
}
@Override
public int getFetchSize() throws SQLException {
return fetchSize;
}
/**
* This method is not private to let {@link ExploreResultSet} access it when closing its results.
*/
void closeClientOperation() throws SQLException {
if (futureResults != null) {
try {
futureResults.cancel(true);
} finally {
futureResults = null;
}
}
}
@Override
public void close() throws SQLException {
if (isClosed) {
return;
}
try {
// As stated by ResultSet javadoc, "A ResultSet object is automatically closed when
// the Statement object that generated it is closed"
if (resultSet != null) {
resultSet.close();
}
closeClientOperation();
} finally {
connection = null;
exploreClient = null;
resultSet = null;
isClosed = true;
}
}
@Override
public boolean isClosed() throws SQLException {
return isClosed;
}
@Override
public void cancel() throws SQLException {
if (isClosed) {
throw new SQLException("Can't cancel after statement has been closed");
}
if (futureResults == null) {
LOG.info("Trying to cancel with no query.");
return;
}
boolean success = futureResults.cancel(true);
if (!success) {
throw new SQLException("Could not cancel query - query is cancelled: " + futureResults.isCancelled());
}
}
@Override
public Connection getConnection() throws SQLException {
if (isClosed) {
throw new SQLException("Can't get connection after statement has been closed");
}
return connection;
}
@Override
public int executeUpdate(String sql) throws SQLException {
// We don't support writes in explore yet
throw new SQLFeatureNotSupportedException();
}
@Override
public int getMaxFieldSize() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void setMaxFieldSize(int i) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void setEscapeProcessing(boolean b) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int getQueryTimeout() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void setQueryTimeout(int i) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public SQLWarning getWarnings() throws SQLException {
return warningChain;
}
@Override
public void clearWarnings() throws SQLException {
warningChain = null;
}
@Override
public void setCursorName(String s) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean getMoreResults() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void setFetchDirection(int i) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int getFetchDirection() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int getResultSetConcurrency() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int getResultSetType() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void addBatch(String s) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void clearBatch() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int[] executeBatch() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean getMoreResults(int i) throws SQLException {
// In case our client.execute returned more than one list of results, which is never the case
throw new SQLFeatureNotSupportedException();
}
@Override
public ResultSet getGeneratedKeys() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int executeUpdate(String s, int i) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int executeUpdate(String s, int[] ints) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int executeUpdate(String s, String[] strings) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean execute(String s, int i) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean execute(String s, int[] ints) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean execute(String s, String[] strings) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public int getResultSetHoldability() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public void setPoolable(boolean b) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean isPoolable() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public <T> T unwrap(Class<T> tClass) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean isWrapperFor(Class<?> aClass) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
public void closeOnCompletion() throws SQLException {
// JDK 1.7
throw new SQLFeatureNotSupportedException();
}
public boolean isCloseOnCompletion() throws SQLException {
// JDK 1.7
throw new SQLFeatureNotSupportedException();
}
}