From 8f9027a995f2230dc22c2f646e312358862a7394 Mon Sep 17 00:00:00 2001 From: junhyeong9812 Date: Thu, 27 Aug 2026 22:36:03 +0900 Subject: [PATCH] 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. Signed-off-by: junhyeong9812 --- .../core/metadata/CallMetaDataContext.java | 9 +- .../jdbc/core/simple/SimpleJdbcCallTests.java | 91 +++++++++++++++++++ 2 files changed, 98 insertions(+), 2 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java index 49ff96b0cfc9..c79119aa0216 100755 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java @@ -374,9 +374,14 @@ protected List reconcileParameters(List parameters) 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( diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java index c67c6e15eafb..045cc28a68b8 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java @@ -267,6 +267,62 @@ void exceptionThrownWhileRetrievingColumnNamesFromMetadata() throws Exception { } + @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 @@ private void verifyAddInvoiceWithMetaData(boolean isFunction) throws SQLExceptio 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");