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");