From 5d6501c75efbb67320b9acc169cb30b350eafebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Wed, 21 Feb 2024 15:26:51 +0100 Subject: [PATCH] Revisit stored procedure detection This commit revisits the improved detection algorithm for stored procedure as, unfortunately, certain JDBC drivers do not support the documented pattern for schema and procedure name. To work around this limitation, this commit applies the escaping of wildcard characters to the case where multiple procedures have been found for a given search. Closes gh-32295 --- .../metadata/GenericCallMetaDataProvider.java | 112 +++++--- .../GenericCallMetaDataProviderTests.java | 256 ++++++++++++++++-- .../jdbc/core/simple/SimpleJdbcCallTests.java | 14 +- 3 files changed, 309 insertions(+), 73 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java index 6305251156e..36383fefc57 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java @@ -22,7 +22,6 @@ import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -305,47 +304,42 @@ public class GenericCallMetaDataProvider implements CallMetaDataProvider { String metaDataSchemaName = metaDataSchemaNameToUse(schemaName); String metaDataProcedureName = procedureNameToUse(procedureName); try { - String searchStringEscape = databaseMetaData.getSearchStringEscape(); - String escapedSchemaName = escapeNamePattern(metaDataSchemaName, searchStringEscape); - String escapedProcedureName = escapeNamePattern(metaDataProcedureName, searchStringEscape); - if (logger.isDebugEnabled()) { - String schemaInfo = (Objects.equals(escapedSchemaName, metaDataSchemaName) - ? metaDataSchemaName : metaDataCatalogName + "(" + escapedSchemaName + ")"); - String procedureInfo = (Objects.equals(escapedProcedureName, metaDataProcedureName) - ? metaDataProcedureName : metaDataProcedureName + "(" + escapedProcedureName + ")"); - logger.debug("Retrieving meta-data for " + metaDataCatalogName + '/' + - schemaInfo + '/' + procedureInfo); - } - - List found = new ArrayList<>(); - boolean function = false; - - try (ResultSet procedures = databaseMetaData.getProcedures( - metaDataCatalogName, escapedSchemaName, escapedProcedureName)) { - while (procedures.next()) { - found.add(procedures.getString("PROCEDURE_CAT") + '.' + procedures.getString("PROCEDURE_SCHEM") + - '.' + procedures.getString("PROCEDURE_NAME")); + ProcedureMetadata procedureMetadata = getProcedureMetadata(databaseMetaData, + metaDataCatalogName, metaDataSchemaName, metaDataProcedureName); + if (procedureMetadata.hits() > 1) { + // Try again with exact match in case of placeholders + String searchStringEscape = databaseMetaData.getSearchStringEscape(); + if (searchStringEscape != null) { + procedureMetadata = getProcedureMetadata(databaseMetaData, metaDataCatalogName, + escapeNamePattern(metaDataSchemaName, searchStringEscape), + escapeNamePattern(metaDataProcedureName, searchStringEscape)); } } - - if (found.isEmpty()) { + if (procedureMetadata.hits() == 0) { // Functions not exposed as procedures anymore on PostgreSQL driver 42.2.11 - try (ResultSet functions = databaseMetaData.getFunctions( - metaDataCatalogName, escapedSchemaName, escapedProcedureName)) { - while (functions.next()) { - found.add(functions.getString("FUNCTION_CAT") + '.' + functions.getString("FUNCTION_SCHEM") + - '.' + functions.getString("FUNCTION_NAME")); - function = true; + procedureMetadata = getProcedureMetadataAsFunction(databaseMetaData, + metaDataCatalogName, metaDataSchemaName, metaDataProcedureName); + if (procedureMetadata.hits() > 1) { + // Try again with exact match in case of placeholders + String searchStringEscape = databaseMetaData.getSearchStringEscape(); + if (searchStringEscape != null) { + procedureMetadata = getProcedureMetadataAsFunction( + databaseMetaData, metaDataCatalogName, + escapeNamePattern(metaDataSchemaName, searchStringEscape), + escapeNamePattern(metaDataProcedureName, searchStringEscape)); } } } + // Handling matches - if (found.size() > 1) { + boolean isFunction = procedureMetadata.function(); + List matches = procedureMetadata.matches; + if (matches.size() > 1) { throw new InvalidDataAccessApiUsageException( "Unable to determine the correct call signature - multiple signatures for '" + - metaDataProcedureName + "': found " + found + " " + (function ? "functions" : "procedures")); + metaDataProcedureName + "': found " + matches + " " + (isFunction ? "functions" : "procedures")); } - else if (found.isEmpty()) { + else if (matches.isEmpty()) { if (metaDataProcedureName != null && metaDataProcedureName.contains(".") && !StringUtils.hasText(metaDataCatalogName)) { String packageName = metaDataProcedureName.substring(0, metaDataProcedureName.indexOf('.')); @@ -368,16 +362,16 @@ public class GenericCallMetaDataProvider implements CallMetaDataProvider { } if (logger.isDebugEnabled()) { - logger.debug("Retrieving column meta-data for " + (function ? "function" : "procedure") + ' ' + - metaDataCatalogName + '/' + metaDataSchemaName + '/' + metaDataProcedureName); + logger.debug("Retrieving column meta-data for " + (isFunction ? "function" : "procedure") + ' ' + + metaDataCatalogName + '/' + procedureMetadata.schemaName + '/' + procedureMetadata.procedureName); } - try (ResultSet columns = function ? - databaseMetaData.getFunctionColumns(metaDataCatalogName, escapedSchemaName, escapedProcedureName, null) : - databaseMetaData.getProcedureColumns(metaDataCatalogName, escapedSchemaName, escapedProcedureName, null)) { + try (ResultSet columns = isFunction ? + databaseMetaData.getFunctionColumns(metaDataCatalogName, procedureMetadata.schemaName, procedureMetadata.procedureName, null) : + databaseMetaData.getProcedureColumns(metaDataCatalogName, procedureMetadata.schemaName, procedureMetadata.procedureName, null)) { while (columns.next()) { String columnName = columns.getString("COLUMN_NAME"); int columnType = columns.getInt("COLUMN_TYPE"); - if (columnName == null && isInOrOutColumn(columnType, function)) { + if (columnName == null && isInOrOutColumn(columnType, isFunction)) { if (logger.isDebugEnabled()) { logger.debug("Skipping meta-data for: " + columnType + " " + columns.getInt("DATA_TYPE") + " " + columns.getString("TYPE_NAME") + " " + columns.getInt("NULLABLE") + @@ -385,8 +379,8 @@ public class GenericCallMetaDataProvider implements CallMetaDataProvider { } } else { - int nullable = (function ? DatabaseMetaData.functionNullable : DatabaseMetaData.procedureNullable); - CallParameterMetaData meta = new CallParameterMetaData(function, columnName, columnType, + int nullable = (isFunction ? DatabaseMetaData.functionNullable : DatabaseMetaData.procedureNullable); + CallParameterMetaData meta = new CallParameterMetaData(isFunction, columnName, columnType, columns.getInt("DATA_TYPE"), columns.getString("TYPE_NAME"), columns.getInt("NULLABLE") == nullable); this.callParameterMetaData.add(meta); @@ -413,6 +407,36 @@ public class GenericCallMetaDataProvider implements CallMetaDataProvider { } } + private ProcedureMetadata getProcedureMetadata(DatabaseMetaData databaseMetaData, + @Nullable String catalogName, @Nullable String schemaName, @Nullable String procedureName) throws SQLException { + if (logger.isDebugEnabled()) { + logger.debug("Retrieving meta-data for " + catalogName + '/' + schemaName + '/' + procedureName); + } + List matches = new ArrayList<>(); + try (ResultSet procedures = databaseMetaData.getProcedures(catalogName, schemaName, procedureName)) { + while (procedures.next()) { + matches.add(procedures.getString("PROCEDURE_CAT") + '.' + procedures.getString("PROCEDURE_SCHEM") + + '.' + procedures.getString("PROCEDURE_NAME")); + } + } + return new ProcedureMetadata(schemaName, procedureName, matches, false); + } + + private ProcedureMetadata getProcedureMetadataAsFunction(DatabaseMetaData databaseMetaData, + @Nullable String catalogName, @Nullable String schemaName, @Nullable String procedureName) throws SQLException { + if (logger.isDebugEnabled()) { + logger.debug("Fallback on retrieving function meta-data for " + catalogName + '/' + schemaName + '/' + procedureName); + } + List matches = new ArrayList<>(); + try (ResultSet functions = databaseMetaData.getFunctions(catalogName, schemaName, procedureName)) { + while (functions.next()) { + matches.add(functions.getString("FUNCTION_CAT") + '.' + functions.getString("FUNCTION_SCHEM") + + '.' + functions.getString("FUNCTION_NAME")); + } + } + return new ProcedureMetadata(schemaName, procedureName, matches, true); + } + @Nullable private static String escapeNamePattern(@Nullable String name, @Nullable String escape) { if (name == null || escape == null) { @@ -436,4 +460,12 @@ public class GenericCallMetaDataProvider implements CallMetaDataProvider { } } + private record ProcedureMetadata(@Nullable String schemaName, @Nullable String procedureName, + List matches, boolean function) { + + int hits() { + return this.matches.size(); + } + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java index 7aaf9e5271d..7370f6cc678 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java @@ -17,14 +17,26 @@ package org.springframework.jdbc.core.metadata; import java.sql.DatabaseMetaData; +import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.IntFunction; import org.junit.jupiter.api.Test; +import org.mockito.InOrder; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; +import org.springframework.util.function.ThrowingBiFunction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests for {@link GenericCallMetaDataProvider}. @@ -36,36 +48,230 @@ class GenericCallMetaDataProviderTests { private final DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class); @Test - void procedureNameWithPatternIsEscape() throws SQLException { - given(this.databaseMetaData.getSearchStringEscape()).willReturn("@"); + void procedureNameWithNoMatch() throws SQLException { GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + + ResultSet noProcedure = mockProcedures(); + given(this.databaseMetaData.getProcedures(null, null, "MY_PROCEDURE")) + .willReturn(noProcedure); + ResultSet noFunction = mockProcedures(); + given(this.databaseMetaData.getFunctions(null, null, "MY_PROCEDURE")) + .willReturn(noFunction); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> provider.initializeWithProcedureColumnMetaData(this.databaseMetaData, null, null, "my_procedure")) + .withMessageContaining("'MY_PROCEDURE'"); + + InOrder inOrder = inOrder(this.databaseMetaData); + inOrder.verify(this.databaseMetaData).getUserName(); + inOrder.verify(this.databaseMetaData).getProcedures(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getFunctions(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getDatabaseProductName(); + verifyNoMoreInteractions(this.databaseMetaData); + } + + @Test + void procedureNameWithExactMatch() throws SQLException { + GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + + ResultSet myProcedure = mockProcedures(new Procedure(null, null, "MY_PROCEDURE")); + given(this.databaseMetaData.getProcedures(null, null, "MY_PROCEDURE")) + .willReturn(myProcedure); + ResultSet myProcedureColumn = mockProcedureColumns("TEST", DatabaseMetaData.procedureColumnIn); + given(this.databaseMetaData.getProcedureColumns(null, null, "MY_PROCEDURE", null)) + .willReturn(myProcedureColumn); + + provider.initializeWithProcedureColumnMetaData(this.databaseMetaData, null, null, "my_procedure"); + assertThat(provider.getCallParameterMetaData()).singleElement().satisfies(callParameterMetaData -> { + assertThat(callParameterMetaData.getParameterName()).isEqualTo("TEST"); + assertThat(callParameterMetaData.getParameterType()).isEqualTo(DatabaseMetaData.procedureColumnIn); + }); + + InOrder inOrder = inOrder(this.databaseMetaData); + inOrder.verify(this.databaseMetaData).getUserName(); + inOrder.verify(this.databaseMetaData).getProcedures(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getProcedureColumns(null, null, "MY_PROCEDURE", null); + verifyNoMoreInteractions(this.databaseMetaData); + } + + @Test + void procedureNameWithSeveralMatchesFallBackOnEscaped() throws SQLException { + GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + + given(this.databaseMetaData.getSearchStringEscape()).willReturn("@"); + ResultSet myProcedures = mockProcedures(new Procedure(null, null, "MY_PROCEDURE"), + new Procedure(null, null, "MYBPROCEDURE")); + given(this.databaseMetaData.getProcedures(null, null, "MY_PROCEDURE")) + .willReturn(myProcedures); + ResultSet myProcedureEscaped = mockProcedures(new Procedure(null, null, "MY@_PROCEDURE")); given(this.databaseMetaData.getProcedures(null, null, "MY@_PROCEDURE")) - .willThrow(new IllegalStateException("Expected")); - assertThatIllegalStateException().isThrownBy(() -> provider.initializeWithProcedureColumnMetaData( - this.databaseMetaData, null, null, "my_procedure")); - verify(this.databaseMetaData).getProcedures(null, null, "MY@_PROCEDURE"); + .willReturn(myProcedureEscaped); + ResultSet myProcedureColumn = mockProcedureColumns("TEST", DatabaseMetaData.procedureColumnIn); + given(this.databaseMetaData.getProcedureColumns(null, null, "MY@_PROCEDURE", null)) + .willReturn(myProcedureColumn); + + provider.initializeWithProcedureColumnMetaData(this.databaseMetaData, null, null, "my_procedure"); + assertThat(provider.getCallParameterMetaData()).singleElement().satisfies(callParameterMetaData -> { + assertThat(callParameterMetaData.getParameterName()).isEqualTo("TEST"); + assertThat(callParameterMetaData.getParameterType()).isEqualTo(DatabaseMetaData.procedureColumnIn); + }); + + InOrder inOrder = inOrder(this.databaseMetaData); + inOrder.verify(this.databaseMetaData).getUserName(); + inOrder.verify(this.databaseMetaData).getProcedures(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getSearchStringEscape(); + inOrder.verify(this.databaseMetaData).getProcedures(null, null, "MY@_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getProcedureColumns(null, null, "MY@_PROCEDURE", null); + verifyNoMoreInteractions(this.databaseMetaData); } @Test - void schemaNameWithPatternIsEscape() throws SQLException { - given(this.databaseMetaData.getSearchStringEscape()).willReturn("@"); - GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); - given(this.databaseMetaData.getProcedures(null, "MY@_SCHEMA", "TEST")) - .willThrow(new IllegalStateException("Expected")); - assertThatIllegalStateException().isThrownBy(() -> provider.initializeWithProcedureColumnMetaData( - this.databaseMetaData, null, "my_schema", "test")); - verify(this.databaseMetaData).getProcedures(null, "MY@_SCHEMA", "TEST"); - } - - @Test - void nameIsNotEscapedIfEscapeCharacterIsNotAvailable() throws SQLException { + void procedureNameWithSeveralMatchesDoesNotFallBackOnEscapedIfEscapeCharacterIsNotAvailable() throws SQLException { given(this.databaseMetaData.getSearchStringEscape()).willReturn(null); GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); - given(this.databaseMetaData.getProcedures(null, "MY_SCHEMA", "MY_TEST")) - .willThrow(new IllegalStateException("Expected")); - assertThatIllegalStateException().isThrownBy(() -> provider.initializeWithProcedureColumnMetaData( - this.databaseMetaData, null, "my_schema", "my_test")); - verify(this.databaseMetaData).getProcedures(null, "MY_SCHEMA", "MY_TEST"); + + ResultSet myProcedures = mockProcedures(new Procedure(null, null, "MY_PROCEDURE"), + new Procedure(null, null, "MYBPROCEDURE")); + given(this.databaseMetaData.getProcedures(null, null, "MY_PROCEDURE")) + .willReturn(myProcedures); + + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> provider.initializeWithProcedureColumnMetaData(this.databaseMetaData, null, null, "my_procedure")) + .withMessageContainingAll("'MY_PROCEDURE'", "null.null.MY_PROCEDURE", "null.null.MYBPROCEDURE"); + + InOrder inOrder = inOrder(this.databaseMetaData); + inOrder.verify(this.databaseMetaData).getUserName(); + inOrder.verify(this.databaseMetaData).getProcedures(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getSearchStringEscape(); + verifyNoMoreInteractions(this.databaseMetaData); + } + + @Test + void procedureNameWitNoMatchFallbackOnFunction() throws SQLException { + GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + + given(this.databaseMetaData.getSearchStringEscape()).willReturn("@"); + ResultSet noProcedure = mockProcedures(); + given(this.databaseMetaData.getProcedures(null, null, "MY_PROCEDURE")) + .willReturn(noProcedure); + ResultSet noProcedureWithEscaped = mockProcedures(); + given(this.databaseMetaData.getProcedures(null, null, "MY@_PROCEDURE")) + .willReturn(noProcedureWithEscaped); + ResultSet function = mockFunctions(new Procedure(null, null, "MY_PROCEDURE")); + given(this.databaseMetaData.getFunctions(null, null, "MY_PROCEDURE")) + .willReturn(function); + ResultSet myProcedureColumn = mockProcedureColumns("TEST", DatabaseMetaData.procedureColumnIn); + given(this.databaseMetaData.getFunctionColumns(null, null, "MY_PROCEDURE", null)) + .willReturn(myProcedureColumn); + + provider.initializeWithProcedureColumnMetaData(this.databaseMetaData, null, null, "my_procedure"); + assertThat(provider.getCallParameterMetaData()).singleElement().satisfies(callParameterMetaData -> { + assertThat(callParameterMetaData.getParameterName()).isEqualTo("TEST"); + assertThat(callParameterMetaData.getParameterType()).isEqualTo(DatabaseMetaData.procedureColumnIn); + }); + + InOrder inOrder = inOrder(this.databaseMetaData); + inOrder.verify(this.databaseMetaData).getUserName(); + inOrder.verify(this.databaseMetaData).getProcedures(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getFunctions(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getFunctionColumns(null, null, "MY_PROCEDURE", null); + verifyNoMoreInteractions(this.databaseMetaData); + } + + @Test + void procedureNameWitNoMatchAndSeveralFunctionsFallbacksOnEscaped() throws SQLException { + GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + + given(this.databaseMetaData.getSearchStringEscape()).willReturn("@"); + ResultSet noProcedure = mockProcedures(); + given(this.databaseMetaData.getProcedures(null, null, "MY_PROCEDURE")) + .willReturn(noProcedure); + ResultSet functions = mockFunctions(new Procedure(null, null, "MY_PROCEDURE"), + new Procedure(null, null, "MYBPROCEDURE")); + given(this.databaseMetaData.getFunctions(null, null, "MY_PROCEDURE")) + .willReturn(functions); + ResultSet functionEscaped = mockFunctions(new Procedure(null, null, "MY@_PROCEDURE")); + given(this.databaseMetaData.getFunctions(null, null, "MY@_PROCEDURE")) + .willReturn(functionEscaped); + ResultSet myProcedureColumn = mockProcedureColumns("TEST", DatabaseMetaData.procedureColumnIn); + given(this.databaseMetaData.getFunctionColumns(null, null, "MY@_PROCEDURE", null)) + .willReturn(myProcedureColumn); + + provider.initializeWithProcedureColumnMetaData(this.databaseMetaData, null, null, "my_procedure"); + assertThat(provider.getCallParameterMetaData()).singleElement().satisfies(callParameterMetaData -> { + assertThat(callParameterMetaData.getParameterName()).isEqualTo("TEST"); + assertThat(callParameterMetaData.getParameterType()).isEqualTo(DatabaseMetaData.procedureColumnIn); + }); + + InOrder inOrder = inOrder(this.databaseMetaData); + inOrder.verify(this.databaseMetaData).getUserName(); + inOrder.verify(this.databaseMetaData).getProcedures(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getFunctions(null, null, "MY_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getSearchStringEscape(); + inOrder.verify(this.databaseMetaData).getFunctions(null, null, "MY@_PROCEDURE"); + inOrder.verify(this.databaseMetaData).getFunctionColumns(null, null, "MY@_PROCEDURE", null); + verifyNoMoreInteractions(this.databaseMetaData); + } + + private ResultSet mockProcedures(Procedure... procedures) { + ResultSet resultSet = mock(ResultSet.class); + List next = new ArrayList<>(); + Arrays.stream(procedures).forEach(p -> next.add(true)); + applyStrings(Arrays.stream(procedures).map(Procedure::catalog).toList(), (first, then) -> + given(resultSet.getString("PROCEDURE_CAT")).willReturn(first, then)); + applyStrings(Arrays.stream(procedures).map(Procedure::schema).toList(), (first, then) -> + given(resultSet.getString("PROCEDURE_SCHEM")).willReturn(first, then)); + applyStrings(Arrays.stream(procedures).map(Procedure::name).toList(), (first, then) -> + given(resultSet.getString("PROCEDURE_NAME")).willReturn(first, then)); + next.add(false); + applyBooleans(next, (first, then) -> given(resultSet.next()).willReturn(first, then)); + + return resultSet; + } + + private ResultSet mockFunctions(Procedure... procedures) { + ResultSet resultSet = mock(ResultSet.class); + List next = new ArrayList<>(); + Arrays.stream(procedures).forEach(p -> next.add(true)); + applyStrings(Arrays.stream(procedures).map(Procedure::catalog).toList(), (first, then) -> + given(resultSet.getString("FUNCTION_CAT")).willReturn(first, then)); + applyStrings(Arrays.stream(procedures).map(Procedure::schema).toList(), (first, then) -> + given(resultSet.getString("FUNCTION_SCHEM")).willReturn(first, then)); + applyStrings(Arrays.stream(procedures).map(Procedure::name).toList(), (first, then) -> + given(resultSet.getString("FUNCTION_NAME")).willReturn(first, then)); + next.add(false); + applyBooleans(next, (first, then) -> given(resultSet.next()).willReturn(first, then)); + + return resultSet; + } + + private ResultSet mockProcedureColumns(String columnName, int columnType) throws SQLException { + ResultSet resultSet = mock(ResultSet.class); + given(resultSet.next()).willReturn(true, false); + given(resultSet.getString("COLUMN_NAME")).willReturn(columnName); + given(resultSet.getInt("COLUMN_TYPE")).willReturn(columnType); + return resultSet; + } + + record Procedure(@Nullable String catalog, @Nullable String schema, String name) { + + } + + private void applyBooleans(List content, ThrowingBiFunction split) { + apply(content, Boolean[]::new, split); + } + + private void applyStrings(List content, ThrowingBiFunction split) { + apply(content, String[]::new, split); + } + + private void apply(List content, IntFunction generator, ThrowingBiFunction split) { + if (content.isEmpty()) { + return; + } + T first = content.get(0); + T[] array = content.subList(1, content.size()).toArray(generator); + split.apply(first, array); } } 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 2e6050d6358..9d4342a7857 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 @@ -239,9 +239,8 @@ class SimpleJdbcCallTests { given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle"); given(databaseMetaData.getUserName()).willReturn("ME"); given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true); - given(databaseMetaData.getSearchStringEscape()).willReturn("@"); - given(databaseMetaData.getProcedures("", "ME", "ADD@_INVOICE")).willReturn(proceduresResultSet); - given(databaseMetaData.getProcedureColumns("", "ME", "ADD@_INVOICE", null)).willReturn(procedureColumnsResultSet); + given(databaseMetaData.getProcedures("", "ME", "ADD_INVOICE")).willReturn(proceduresResultSet); + given(databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null)).willReturn(procedureColumnsResultSet); given(proceduresResultSet.next()).willReturn(true, false); given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("add_invoice"); @@ -307,9 +306,8 @@ class SimpleJdbcCallTests { given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle"); given(databaseMetaData.getUserName()).willReturn("ME"); given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true); - given(databaseMetaData.getSearchStringEscape()).willReturn("@"); - given(databaseMetaData.getProcedures("", "ME", "ADD@_INVOICE")).willReturn(proceduresResultSet); - given(databaseMetaData.getProcedureColumns("", "ME", "ADD@_INVOICE", null)).willReturn(procedureColumnsResultSet); + given(databaseMetaData.getProcedures("", "ME", "ADD_INVOICE")).willReturn(proceduresResultSet); + given(databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null)).willReturn(procedureColumnsResultSet); given(proceduresResultSet.next()).willReturn(true, false); given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("add_invoice"); @@ -332,8 +330,8 @@ class SimpleJdbcCallTests { } private void verifyAddInvoiceWithMetaData(boolean isFunction) throws SQLException { - ResultSet proceduresResultSet = databaseMetaData.getProcedures("", "ME", "ADD@_INVOICE"); - ResultSet procedureColumnsResultSet = databaseMetaData.getProcedureColumns("", "ME", "ADD@_INVOICE", null); + ResultSet proceduresResultSet = databaseMetaData.getProcedures("", "ME", "ADD_INVOICE"); + ResultSet procedureColumnsResultSet = databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null); if (isFunction) { verify(callableStatement).registerOutParameter(1, 4); verify(callableStatement).setObject(2, 1103, 4);