mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-10-05 22:49:02 +00:00
Normalize function return parameter lookup in CallMetaDataContext
CallMetaDataContext.reconcileParameters() keys the map of declared parameters by lowerCase(provider.parameterNameToUse(name)), but the branch that matches the return parameter reported by the database metadata did not apply the same rule. It looked up the function return name as declared (original case) and fell back to the first declared OUT parameter name with a plain toLowerCase(), without the provider transformation that strips the '@' prefix on SQL Server and Sybase. The first lookup therefore always missed on Oracle, so the fallback silently used whichever OUT parameter was declared first. Declaring an additional OUT parameter before the return parameter of a function made that parameter double as the return slot: the declared return parameter was dropped from the call parameters, the wrong parameter was bound at position 1, and executeFunction() returned the value of the other out parameter. On SQL Server, a procedure compiled with withReturnValue() and an '@'-prefixed OUT parameter declared before the return parameter failed with InvalidDataAccessApiUsageException because neither lookup could find the declared parameter. The return parameter branch now looks up the metadata-derived name first and normalizes both the function return name and the first OUT parameter fallback with the same rule as the declared parameter map. Tests cover both declaration orders for an Oracle function and for a SQL Server procedure with a return value. Closes gh-37206 Signed-off-by: junhyeong9812 <pickjog@gmail.com>
This commit is contained in:
+7
-2
@@ -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(
|
||||
|
||||
+91
@@ -267,6 +267,62 @@ class SimpleJdbcCallTests {
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
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
|
||||
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
|
||||
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
|
||||
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 +406,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");
|
||||
|
||||
Reference in New Issue
Block a user