Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support ResultSet from stored procedures #2576

Merged
merged 9 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- add a new `integration-test` module for tests that require different parts of the code base. Should be used to write test cases for issue investigations.
- support `null` as a value for binding bean, method, field and pojo objects (Suggested by @xak2000 in #2562)
- Add testcontainers support for MS SQLServer
- support returning a `ResultSet` from `Call` statements for databases that do not support cursor parameters. (suggested in #2557 by @metaforte and @0x1F528 in #2546)
- support `int`, `long`, `short`, `double` and `float` return values from out parameters directly.

# 3.42.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.function.Supplier;

import org.jdbi.v3.core.config.JdbiConfig;
import org.jdbi.v3.core.result.internal.EmptyResultSet;
import org.jdbi.v3.core.statement.StatementContext;

/**
Expand Down Expand Up @@ -126,10 +127,10 @@ public ResultProducers allowNoResults(boolean allowNoResults) {
return this;
}

@FunctionalInterface
/**
* Returns a ResultSet from a Statement.
*/
@FunctionalInterface
public interface ResultSetCreator {

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jdbi.v3.core.result;
package org.jdbi.v3.core.result.internal;

import java.io.InputStream;
import java.io.Reader;
Expand All @@ -34,7 +34,7 @@
import java.util.Calendar;
import java.util.Map;

class EmptyResultSet implements ResultSet {
public final class EmptyResultSet implements ResultSet {
@Override
public <T> T unwrap(Class<T> iface) {
return iface.cast(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jdbi.v3.core.result;
package org.jdbi.v3.core.result.internal;

import java.sql.ResultSetMetaData;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,7 @@ public ResultSetResultIterable(
@Override
public ResultIterator<T> iterator() {
try {
ResultSet resultSet = resultSetSupplier.get();
ctx.addCleanable(resultSet::close);

return new ResultSetResultIterator<>(resultSet, mapper, ctx);
return new ResultSetResultIterator<>(resultSetSupplier, mapper, ctx);
} catch (final SQLException e) {
throw new ResultSetException("Unable to iterate result set", e, ctx);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,56 @@
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.NoSuchElementException;
import java.util.function.Supplier;

import org.jdbi.v3.core.internal.exceptions.Sneaky;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.result.ResultIterator;
import org.jdbi.v3.core.result.ResultSetException;
import org.jdbi.v3.core.statement.StatementContext;

import static java.util.Objects.requireNonNull;

class ResultSetResultIterator<T> implements ResultIterator<T> {
private final ResultSet resultSet;
private final RowMapper<T> rowMapper;

private final ResultSetSupplier resultSetSupplier;
private final StatementContext context;

private volatile boolean alreadyAdvanced = false;
private volatile boolean hasNext = false;
private volatile boolean closed = false;

ResultSetResultIterator(ResultSet resultSet,
RowMapper<T> rowMapper,
StatementContext context) throws SQLException {
this.resultSet = requireNonNull(resultSet);
this.rowMapper = rowMapper.specialize(resultSet, context);
ResultSetResultIterator(Supplier<ResultSet> resultSetSupplier,
RowMapper<T> rowMapper,
StatementContext context) throws SQLException {

this.context = context;

if (resultSetSupplier instanceof ResultSetSupplier) {
stevenschlansker marked this conversation as resolved.
Show resolved Hide resolved
this.resultSetSupplier = (ResultSetSupplier) resultSetSupplier;
} else {
this.resultSetSupplier = ResultSetSupplier.closingContext(resultSetSupplier, context);
}

this.resultSet = this.resultSetSupplier.get();

if (resultSet != null) {
context.addCleanable(resultSet::close);
this.rowMapper = rowMapper.specialize(resultSet, context);
} else {
close();
this.rowMapper = null;
}
}

@Override
public void close() {
closed = true;
context.close();
try {
resultSetSupplier.close();
} catch (SQLException e) {
throw Sneaky.throwAnyway(e);
}
}

@Override
Expand All @@ -70,10 +91,6 @@ public boolean hasNext() {

@Override
public T next() {
if (closed) {
throw new IllegalStateException("iterator is closed");
}

if (!hasNext()) {
close();
throw new NoSuchElementException("No element to advance to");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* 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 org.jdbi.v3.core.result.internal;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.function.Supplier;

import org.jdbi.v3.core.statement.Cleanable;
import org.jdbi.v3.core.statement.StatementContext;

public abstract class ResultSetSupplier implements Supplier<ResultSet>, Cleanable {

public static ResultSetSupplier closingContext(Supplier<ResultSet> supplier, StatementContext context) {
return new ResultSetSupplier(supplier) {
@Override
public void close() throws SQLException {
try (context) {
cleanable.close();
}
}
};
}

public static ResultSetSupplier notClosingContext(Supplier<ResultSet> supplier) {
return new ResultSetSupplier(supplier) {
@Override
public void close() throws SQLException {
cleanable.close();
}
};
}

private final Supplier<ResultSet> supplier;

protected Cleanable cleanable = Cleanable.NO_OP;

private ResultSetSupplier(Supplier<ResultSet> supplier) {
this.supplier = supplier;
}

@Override
public ResultSet get() {
ResultSet resultSet = supplier.get();
if (resultSet != null) {
this.cleanable = resultSet::close;
}
return resultSet;
}
}
126 changes: 99 additions & 27 deletions core/src/main/java/org/jdbi/v3/core/statement/Call.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,33 @@

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

import org.jdbi.v3.core.Handle;
import org.jdbi.v3.core.argument.Argument;
import org.jdbi.v3.core.internal.exceptions.Sneaky;
import org.jdbi.v3.core.result.ResultBearing;
import org.jdbi.v3.core.result.internal.ResultSetSupplier;

/**
* Used for invoking stored procedures.
* Used for invoking stored procedures. The most common way to use this is to register {@link OutParameters} with the call and then use the {@link Call#invoke()} method
* to retrieve the return values from the invoked procedure.
* <br>
* There are some databases, most prominently MS SqlServer that only support limited OUT parameters, especially do not support cursors
* (for MS SqlServer see <a href="https://learn.microsoft.com/en-us/sql/connect/jdbc/using-a-stored-procedure-with-output-parameters">Using a stored procedure with output parameters</a>).
* Those databases may support returning a result set from the procedure invocation, to access this result set use the {@link OutParameters#getResultSet()} method to retrieve
* the result set from the underlying call operation.
*/
public class Call extends SqlStatement<Call> {
private final List<OutParamArgument> params = new ArrayList<>();
private final List<OutParamArgument> outParamArguments = new ArrayList<>();

public Call(Handle handle, CharSequence sql) {
super(handle, sql);
Expand Down Expand Up @@ -117,31 +129,84 @@ public void invoke(Consumer<OutParameters> resultConsumer) {
* returning a computed value of type {@code T}.
*/
public <T> T invoke(Function<OutParameters, T> resultComputer) {
try {
// it is ok to ignore the PreparedStatement returned here. internalExecute registers it to close with the context and the
// nullSafeCleanUp below will take care of it.
internalExecute();
OutParameters out = new OutParameters(getContext());
for (OutParamArgument param : params) {
final Object obj = param.map((CallableStatement) stmt);

// convert from JDBC 1-based position to Jdbi's 0-based
final int index = param.position - 1;

if (param.isNull((CallableStatement) stmt)) {
out.getMap().put(index, null);
} else {
out.getMap().put(index, obj);
}
// it is ok to ignore the PreparedStatement returned here. internalExecute registers it to close with the context and the
// nullSafeCleanUp below will take care of it.
internalExecute();

if (param.name != null) {
out.getMap().put(param.name, obj);
}
final Supplier<ResultSet> resultSetSupplier = () -> {
try {
return stmt.getResultSet();
} catch (SQLException e) {
throw Sneaky.throwAnyway(e);
}
return resultComputer.apply(out);
} finally {
close();
}
};

final ResultBearing resultSet = ResultBearing.of(ResultSetSupplier.notClosingContext(resultSetSupplier), getContext());

OutParameters out = new OutParameters(resultSet, getContext());

outParamArguments.forEach(outparamArgument -> {
Supplier<Object> supplier = outparamArgument.supplier((CallableStatement) stmt);
// index is 0 based, position is 1 based.
out.putValueSupplier(outparamArgument.position - 1, outparamArgument.name, supplier);
});

return resultComputer.apply(out);
}

/**
* Specify the fetch size for the call. This should cause the results to be
stevenschlansker marked this conversation as resolved.
Show resolved Hide resolved
* fetched from the underlying RDBMS in groups of rows equal to the number passed.
* This is useful for doing chunked streaming of results when exhausting memory
* could be a problem.
*
* @param fetchSize the number of rows to fetch in a bunch
*
* @return the modified call
* @since 3.43.0
*/
public Call setFetchSize(final int fetchSize) {
return addCustomizer(StatementCustomizers.fetchSize(fetchSize));
}

/**
* Specify the maximum number of rows the call is to return. This uses the underlying JDBC
* {@link Statement#setMaxRows(int)}}.
*
* @param maxRows maximum number of rows to return
*
* @return modified call
* @since 3.43.0
*/
public Call setMaxRows(final int maxRows) {
return addCustomizer(StatementCustomizers.maxRows(maxRows));
}

/**
* Specify the maximum field size in the result set. This uses the underlying JDBC
* {@link Statement#setMaxFieldSize(int)}
*
* @param maxFields maximum field size
*
* @return modified call
* @since 3.43.0
*/
public Call setMaxFieldSize(final int maxFields) {
return addCustomizer(StatementCustomizers.maxFieldSize(maxFields));
}

/**
* Specify that the result set should be concurrent updatable.
*
* This will allow the update methods to be called on the result set produced by this
* Call.
*
* @return the modified call
* @since 3.43.0
*/
public Call concurrentUpdatable() {
getContext().setConcurrentUpdatable(true);
return this;
}

// TODO tostring?
Expand All @@ -155,7 +220,7 @@ private class OutParamArgument implements Argument {
this.sqlType = sqlType;
this.mapper = mapper;
this.name = name;
params.add(this);
outParamArguments.add(this);
}

@Override
Expand All @@ -164,7 +229,14 @@ public void apply(int outPosition, PreparedStatement statement, StatementContext
this.position = outPosition;
}

public Object map(CallableStatement stmt) {
public Supplier<Object> supplier(CallableStatement stmt) {
return () -> {
Object value = map(stmt);
return isNull(stmt) ? null : value;
stevenschlansker marked this conversation as resolved.
Show resolved Hide resolved
};
}

private Object map(CallableStatement stmt) {
try {
if (mapper != null) {
return mapper.map(position, stmt);
Expand Down
8 changes: 8 additions & 0 deletions core/src/main/java/org/jdbi/v3/core/statement/Cleanable.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@
*/
@FunctionalInterface
public interface Cleanable extends AutoCloseable {

/**
* A cleanable that does nothing.
*
* @since 3.43.0
*/
Cleanable NO_OP = () -> {};

@Override
void close() throws SQLException;

Expand Down