/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.sql.embedded;
import com.foundationdb.qp.operator.QueryBindings;
import com.foundationdb.server.error.ErrorCode;
import com.foundationdb.server.error.StaleStatementException;
import com.foundationdb.sql.embedded.JDBCException.Wrapper;
import com.foundationdb.sql.server.ServerStatement;
import java.sql.*;
import java.util.ArrayList;
import java.util.Queue;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class JDBCStatement implements Statement
{
private static final Logger LOG = LoggerFactory.getLogger(JDBCStatement.class);
protected final JDBCConnection connection;
protected String sql;
private boolean closed;
private JDBCWarning warnings;
private int currentUpdateCount;
// Note that result sets need not be for this connection. For
// example, if a stored procedure with dynamic result sets called
// is, we don't know where its results came
// from. secondaryResultSets are always from the same connection;
// but this is only how they are set up.
private ResultSet currentResultSet, generatedKeys;
private List<ResultSet> secondaryResultSets; // For instance, nested.
private Queue<ResultSet> pendingResultSets; // For instance, from stored procedure.
protected JDBCStatement(JDBCConnection connection) {
this.connection = connection;
}
public boolean executeInternal(ExecutableStatement stmt, EmbeddedQueryContext context, QueryBindings bindings)
throws SQLException {
if (context == null) {
if (stmt.getParameterMetaData() != null)
throw new JDBCException("Statement requires parameters; must prepare", ErrorCode.UNPREPARED_STATEMENT_WITH_PARAMETERS);
context = new EmbeddedQueryContext(this);
}
connection.beforeExecuteStatement(sql, stmt);
if (stmt.getAISGenerationMode() == ServerStatement.AISGenerationMode.NOT_ALLOWED) {
connection.updateAIS(context);
if (stmt.getAISGeneration() != connection.getAIS().getGeneration())
throw JDBCException.throwUnwrapped(new StaleStatementException());
}
if (bindings == null) {
bindings = context.createBindings();
}
boolean hasResultSet = false;
Throwable failure = null;
try {
ExecuteResults results = stmt.execute(context, bindings);
currentUpdateCount = results.getUpdateCount();
if (results.getCursor() != null) {
JDBCResultSet resultSet = new JDBCResultSet(this, stmt.getResultSetMetaData(), results.getCursor());
if (results.hasResultSet()) {
// Cursor is ordinary result. This will keep an
// auto-commit transaction open until it is
// closed.
connection.openingResultSet(resultSet);
currentResultSet = resultSet;
hasResultSet = true;
}
else {
// These are copied (to get update count) and do
// not need a transaction. Note that behavior of
// generated keys is explicitly ill-defined by the
// JDBC spec in auto-commit mode.
generatedKeys = resultSet;
}
}
else if (results.getAdditionalResultSets() != null) {
pendingResultSets = results.getAdditionalResultSets();
hasResultSet = getMoreResults();
}
}
catch (RuntimeException ex) {
failure = ex;
if (failure instanceof Wrapper) {
failure = (SQLException)failure.getCause();
}
final ErrorCode code = ErrorCode.getCodeForRESTException(failure);
code.logAtImportance(
LOG, "Statement execution for query {} failed with exception {}", sql, failure);
throw JDBCException.throwUnwrapped(ex);
}
catch (SQLException ex) {
failure = ex;
throw ex;
}
catch (Error err) {
failure = err;
throw err;
}
finally {
connection.afterExecuteStatement(stmt, failure);
}
return hasResultSet;
}
public ResultSet executeQueryInternal(ExecutableStatement stmt,
EmbeddedQueryContext context,
QueryBindings bindings)
throws SQLException {
boolean hasResultSet = executeInternal(stmt, context, bindings);
if (!hasResultSet) throw new JDBCException("Statement is not SELECT", ErrorCode.RESULTSET_SELECT_MISMATCH);
return getResultSet();
}
public int executeUpdateInternal(ExecutableStatement stmt,
EmbeddedQueryContext context,
QueryBindings bindings)
throws SQLException {
boolean hasResultSet = executeInternal(stmt, context, bindings);
if (hasResultSet) throw new JDBCException("Statement is SELECT", ErrorCode.RESULTSET_SELECT_MISMATCH);
return getUpdateCount();
}
protected void addWarning(JDBCWarning warning) {
if (warnings == null)
warnings = warning;
else
warnings.setNextWarning(warning);
}
protected void secondaryResultSet(JDBCResultSet resultSet) {
if (secondaryResultSets == null)
secondaryResultSets = new ArrayList<>();
secondaryResultSets.add(resultSet);
connection.openingResultSet(resultSet);
}
protected void closingResultSet(JDBCResultSet resultSet) {
if (currentResultSet == resultSet)
currentResultSet = null;
if (secondaryResultSets != null)
secondaryResultSets.remove(resultSet);
connection.closingResultSet(resultSet);
}
/* Wrapper */
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
throw new SQLException("Not supported");
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
/* Statement */
@Override
public ResultSet executeQuery(String sql) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql);
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeQueryInternal(stmt, null, null);
}
@Override
public int executeUpdate(String sql) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql);
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeUpdateInternal(stmt, null, null);
}
@Override
public void close() throws SQLException {
if (currentResultSet != null) {
currentResultSet.close(); // Which will call thru us to connection.
currentResultSet = null;
}
if (generatedKeys != null) {
generatedKeys.close();
generatedKeys = null;
}
if (secondaryResultSets != null) {
while (!secondaryResultSets.isEmpty()) {
secondaryResultSets.get(0).close();
}
secondaryResultSets = null;
}
if (pendingResultSets != null) {
while (!pendingResultSets.isEmpty())
pendingResultSets.remove().close();
pendingResultSets = null;
}
closed = true;
}
@Override
public int getMaxFieldSize() throws SQLException {
return 0;
}
@Override
public void setMaxFieldSize(int max) throws SQLException {
}
@Override
public int getMaxRows() throws SQLException {
return 0;
}
@Override
public void setMaxRows(int max) throws SQLException {
}
@Override
public void setEscapeProcessing(boolean enable) throws SQLException {
}
@Override
public int getQueryTimeout() throws SQLException {
return 0;
}
@Override
public void setQueryTimeout(int seconds) throws SQLException {
}
@Override
public void cancel() throws SQLException {
}
@Override
public SQLWarning getWarnings() throws SQLException {
return warnings;
}
@Override
public void clearWarnings() throws SQLException {
warnings = null;
}
@Override
public void setCursorName(String name) throws SQLException {
}
@Override
public boolean execute(String sql) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql);
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeInternal(stmt, null, null);
}
@Override
public ResultSet getResultSet() throws SQLException {
return currentResultSet;
}
@Override
public int getUpdateCount() throws SQLException {
return currentUpdateCount;
}
@Override
public boolean getMoreResults() throws SQLException {
if (currentResultSet != null) {
currentResultSet.close();
currentResultSet = null;
}
if (pendingResultSets == null)
return false;
currentResultSet = pendingResultSets.poll();
return (currentResultSet != null);
}
@Override
public void setFetchDirection(int direction) throws SQLException {
if (direction != ResultSet.FETCH_FORWARD)
throw new SQLFeatureNotSupportedException();
}
@Override
public int getFetchDirection() throws SQLException {
return ResultSet.FETCH_FORWARD;
}
@Override
public void setFetchSize(int rows) throws SQLException {
}
@Override
public int getFetchSize() throws SQLException {
return 1;
}
@Override
public int getResultSetConcurrency() throws SQLException {
return ResultSet.CONCUR_READ_ONLY;
}
@Override
public int getResultSetType() throws SQLException {
return ResultSet.TYPE_FORWARD_ONLY;
}
@Override
public void addBatch(String sql) 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 Connection getConnection() throws SQLException {
return connection;
}
@Override
public boolean getMoreResults(int current) throws SQLException {
return false;
}
@Override
public ResultSet getGeneratedKeys() throws SQLException {
return generatedKeys;
}
@Override
public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql,
ExecuteAutoGeneratedKeys.of(autoGeneratedKeys));
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeUpdateInternal(stmt, null, null);
}
@Override
public int executeUpdate(String sql, int[] columnIndexes) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql,
ExecuteAutoGeneratedKeys.of(columnIndexes));
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeUpdateInternal(stmt, null, null);
}
@Override
public int executeUpdate(String sql, String[] columnNames) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql,
ExecuteAutoGeneratedKeys.of(columnNames));
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeUpdateInternal(stmt, null, null);
}
@Override
public boolean execute(String sql, int autoGeneratedKeys) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql,
ExecuteAutoGeneratedKeys.of(autoGeneratedKeys));
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeInternal(stmt, null, null);
}
@Override
public boolean execute(String sql, int[] columnIndexes) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql,
ExecuteAutoGeneratedKeys.of(columnIndexes));
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeInternal(stmt, null, null);
}
@Override
public boolean execute(String sql, String[] columnNames) throws SQLException {
this.sql = sql;
ExecutableStatement stmt;
try {
stmt = connection.compileExecutableStatement(sql,
ExecuteAutoGeneratedKeys.of(columnNames));
}
catch (RuntimeException ex) {
throw JDBCException.throwUnwrapped(ex);
}
return executeInternal(stmt, null, null);
}
@Override
public int getResultSetHoldability() throws SQLException {
return ResultSet.CLOSE_CURSORS_AT_COMMIT;
}
@Override
public boolean isClosed() throws SQLException {
return closed;
}
@Override
public void setPoolable(boolean poolable) throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean isPoolable() throws SQLException {
return false;
}
@Override
public void closeOnCompletion() throws SQLException {
throw new SQLFeatureNotSupportedException();
}
@Override
public boolean isCloseOnCompletion() throws SQLException {
return false;
}
}