Merge branch '7.0.x'

This commit is contained in:
Sam Brannen
2026-09-03 11:43:00 +02:00
5 changed files with 153 additions and 5 deletions
@@ -374,9 +374,14 @@ public class CallMetaDataContext {
if (declaredParams.containsKey(paramNameToCheck) || (meta.isReturnParameter() && returnDeclared)) {
SqlParameter param;
if (meta.isReturnParameter()) {
param = declaredParams.get(getFunctionReturnName());
// Same normalization as the declaredParams keys above; the function
// return name may have been adopted from a declared out parameter
param = declaredParams.get(paramNameToCheck);
if (param == null) {
param = declaredParams.get(lowerCase(provider.parameterNameToUse(getFunctionReturnName())));
}
if (param == null && !getOutParameterNames().isEmpty()) {
param = declaredParams.get(getOutParameterNames().get(0).toLowerCase(Locale.ROOT));
param = declaredParams.get(lowerCase(provider.parameterNameToUse(getOutParameterNames().get(0))));
}
if (param == null) {
throw new InvalidDataAccessApiUsageException(
@@ -18,6 +18,8 @@ package org.springframework.jdbc.core.simple;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
@@ -41,9 +43,10 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Mock object based tests for CallMetaDataContext.
* Mock object based tests for {@link CallMetaDataContext}.
*
* @author Thomas Risberg
* @author Sam Brannen
*/
class CallMetaDataContextTests {
@@ -103,4 +106,54 @@ class CallMetaDataContextTests {
assertThat(callParameters).as("Wrong number of call parameters").hasSize(3);
}
@Test // gh-37206
void reconcileParametersMatchesFunctionReturnParameterDeclaredBeforeOutParameter() throws Exception {
initializeGetTotalFunctionMetaData();
List<SqlParameter> parameters = List.of(
new SqlOutParameter("RESULT", Types.INTEGER),
new SqlOutParameter("out_status", Types.INTEGER));
context.setFunction(true);
context.setProcedureName("GET_TOTAL");
context.initializeMetaData(dataSource);
context.processParameters(parameters);
assertThat(context.getCallParameters()).extracting(SqlParameter::getName)
.containsExactly("RESULT", "AMOUNT", "out_status");
}
@Test // gh-37206
void reconcileParametersMatchesFunctionReturnParameterDeclaredAfterOutParameter() throws Exception {
initializeGetTotalFunctionMetaData();
List<SqlParameter> parameters = List.of(
new SqlOutParameter("out_status", Types.INTEGER),
new SqlOutParameter("RESULT", Types.INTEGER));
context.setFunction(true);
context.setProcedureName("GET_TOTAL");
context.initializeMetaData(dataSource);
context.processParameters(parameters);
assertThat(context.getCallParameters()).extracting(SqlParameter::getName)
.containsExactly("RESULT", "AMOUNT", "out_status");
}
private void initializeGetTotalFunctionMetaData() throws SQLException {
ResultSet proceduresResultSet = mock();
ResultSet procedureColumnsResultSet = mock();
given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle");
given(databaseMetaData.getUserName()).willReturn("ME");
given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true);
given(databaseMetaData.getProcedures("", "ME", "GET_TOTAL")).willReturn(proceduresResultSet);
given(databaseMetaData.getProcedureColumns("", "ME", "GET_TOTAL", null)).willReturn(procedureColumnsResultSet);
given(proceduresResultSet.next()).willReturn(true, false);
given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("GET_TOTAL");
given(procedureColumnsResultSet.next()).willReturn(true, true, true, false);
given(procedureColumnsResultSet.getInt("DATA_TYPE")).willReturn(Types.INTEGER);
given(procedureColumnsResultSet.getString("COLUMN_NAME")).willReturn(null, "amount", "out_status");
given(procedureColumnsResultSet.getInt("COLUMN_TYPE")).willReturn(5, 1, 4);
}
}
@@ -266,6 +266,61 @@ class SimpleJdbcCallTests {
verify(procedureColumnsResultSet).close();
}
@Test // gh-37206
void functionWithAdditionalOutParameterDeclaredBeforeReturn() throws Exception {
initializeGetTotalFunctionWithMetaData();
SimpleJdbcCall function = new SimpleJdbcCall(dataSource).withFunctionName("get_total");
function.declareParameters(
new SqlOutParameter("out_status", Types.INTEGER),
new SqlOutParameter("RESULT", Types.INTEGER));
function.compile();
assertThat(function.getCallParameters()).extracting(SqlParameter::getName)
.containsExactly("RESULT", "AMOUNT", "out_status");
verifyStatement(function, "{? = call GET_TOTAL(?, ?)}");
Integer total = function.executeFunction(Integer.class, 5);
assertThat(total).isEqualTo(42);
}
@Test // gh-37206
void functionWithAdditionalOutParameterDeclaredAfterReturn() throws Exception {
initializeGetTotalFunctionWithMetaData();
SimpleJdbcCall function = new SimpleJdbcCall(dataSource).withFunctionName("get_total");
function.declareParameters(
new SqlOutParameter("RESULT", Types.INTEGER),
new SqlOutParameter("out_status", Types.INTEGER));
function.compile();
assertThat(function.getCallParameters()).extracting(SqlParameter::getName)
.containsExactly("RESULT", "AMOUNT", "out_status");
Integer total = function.executeFunction(Integer.class, 5);
assertThat(total).isEqualTo(42);
}
@Test // gh-37206
void sqlServerProcedureWithReturnValueDeclaredAfterOutParameter() throws Exception {
initializeSqlServerProcedureWithReturnValue();
SimpleJdbcCall procedure = new SimpleJdbcCall(dataSource).withProcedureName("my_proc").withReturnValue();
procedure.declareParameters(
new SqlOutParameter("@out_total", Types.INTEGER),
new SqlOutParameter("RETURN_VALUE", Types.INTEGER));
procedure.compile();
assertThat(procedure.getCallParameters()).extracting(SqlParameter::getName)
.containsExactly("RETURN_VALUE", "amount", "@out_total");
verifyStatement(procedure, "{? = call my_proc(?, ?)}");
}
@Test // gh-37206
void sqlServerProcedureWithReturnValueDeclaredFirst() throws Exception {
initializeSqlServerProcedureWithReturnValue();
SimpleJdbcCall procedure = new SimpleJdbcCall(dataSource).withProcedureName("my_proc").withReturnValue();
procedure.declareParameters(
new SqlOutParameter("RETURN_VALUE", Types.INTEGER),
new SqlOutParameter("@out_total", Types.INTEGER));
procedure.compile();
assertThat(procedure.getCallParameters()).extracting(SqlParameter::getName)
.containsExactly("RETURN_VALUE", "amount", "@out_total");
verifyStatement(procedure, "{? = call my_proc(?, ?)}");
}
private void verifyStatement(SimpleJdbcCall adder, String expected) {
assertThat(adder.getCallString()).as("Incorrect call statement").isEqualTo(expected);
@@ -350,6 +405,41 @@ class SimpleJdbcCallTests {
verify(procedureColumnsResultSet).close();
}
private void initializeGetTotalFunctionWithMetaData() throws SQLException {
ResultSet proceduresResultSet = mock();
ResultSet procedureColumnsResultSet = mock();
given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle");
given(databaseMetaData.getUserName()).willReturn("ME");
given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true);
given(databaseMetaData.getProcedures("", "ME", "GET_TOTAL")).willReturn(proceduresResultSet);
given(databaseMetaData.getProcedureColumns("", "ME", "GET_TOTAL", null)).willReturn(procedureColumnsResultSet);
given(proceduresResultSet.next()).willReturn(true, false);
given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("get_total");
given(procedureColumnsResultSet.next()).willReturn(true, true, true, false);
given(procedureColumnsResultSet.getInt("DATA_TYPE")).willReturn(4);
given(procedureColumnsResultSet.getString("COLUMN_NAME")).willReturn(null, "amount", "out_status");
given(procedureColumnsResultSet.getInt("COLUMN_TYPE")).willReturn(5, 1, 4);
given(connection.prepareCall("{? = call GET_TOTAL(?, ?)}")).willReturn(callableStatement);
given(callableStatement.execute()).willReturn(false);
given(callableStatement.getUpdateCount()).willReturn(-1);
given(callableStatement.getObject(1)).willReturn(42);
given(callableStatement.getObject(3)).willReturn(7);
}
private void initializeSqlServerProcedureWithReturnValue() throws SQLException {
ResultSet proceduresResultSet = mock();
ResultSet procedureColumnsResultSet = mock();
given(databaseMetaData.getDatabaseProductName()).willReturn("Microsoft SQL Server");
given(databaseMetaData.getProcedures(null, null, "my_proc")).willReturn(proceduresResultSet);
given(databaseMetaData.getProcedureColumns(null, null, "my_proc", null)).willReturn(procedureColumnsResultSet);
given(proceduresResultSet.next()).willReturn(true, false);
given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("my_proc");
given(procedureColumnsResultSet.next()).willReturn(true, true, true, false);
given(procedureColumnsResultSet.getInt("DATA_TYPE")).willReturn(4);
given(procedureColumnsResultSet.getString("COLUMN_NAME")).willReturn("@RETURN_VALUE", "@amount", "@out_total");
given(procedureColumnsResultSet.getInt("COLUMN_TYPE")).willReturn(5, 1, 4);
}
@Test
void correctSybaseFunctionStatementNamed() throws Exception {
given(databaseMetaData.getDatabaseProductName()).willReturn("Sybase");