Skip to content

Commit 5d6501c

Browse files
committed
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
1 parent 93f0ec2 commit 5d6501c

File tree

3 files changed

+306
-70
lines changed

3 files changed

+306
-70
lines changed

Diff for: spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java

+72-40
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
import java.sql.Types;
2323
import java.util.ArrayList;
2424
import java.util.List;
25-
import java.util.Objects;
2625

2726
import org.apache.commons.logging.Log;
2827
import org.apache.commons.logging.LogFactory;
@@ -305,47 +304,42 @@ private void processProcedureColumns(DatabaseMetaData databaseMetaData,
305304
String metaDataSchemaName = metaDataSchemaNameToUse(schemaName);
306305
String metaDataProcedureName = procedureNameToUse(procedureName);
307306
try {
308-
String searchStringEscape = databaseMetaData.getSearchStringEscape();
309-
String escapedSchemaName = escapeNamePattern(metaDataSchemaName, searchStringEscape);
310-
String escapedProcedureName = escapeNamePattern(metaDataProcedureName, searchStringEscape);
311-
if (logger.isDebugEnabled()) {
312-
String schemaInfo = (Objects.equals(escapedSchemaName, metaDataSchemaName)
313-
? metaDataSchemaName : metaDataCatalogName + "(" + escapedSchemaName + ")");
314-
String procedureInfo = (Objects.equals(escapedProcedureName, metaDataProcedureName)
315-
? metaDataProcedureName : metaDataProcedureName + "(" + escapedProcedureName + ")");
316-
logger.debug("Retrieving meta-data for " + metaDataCatalogName + '/' +
317-
schemaInfo + '/' + procedureInfo);
318-
}
319-
320-
List<String> found = new ArrayList<>();
321-
boolean function = false;
322-
323-
try (ResultSet procedures = databaseMetaData.getProcedures(
324-
metaDataCatalogName, escapedSchemaName, escapedProcedureName)) {
325-
while (procedures.next()) {
326-
found.add(procedures.getString("PROCEDURE_CAT") + '.' + procedures.getString("PROCEDURE_SCHEM") +
327-
'.' + procedures.getString("PROCEDURE_NAME"));
307+
ProcedureMetadata procedureMetadata = getProcedureMetadata(databaseMetaData,
308+
metaDataCatalogName, metaDataSchemaName, metaDataProcedureName);
309+
if (procedureMetadata.hits() > 1) {
310+
// Try again with exact match in case of placeholders
311+
String searchStringEscape = databaseMetaData.getSearchStringEscape();
312+
if (searchStringEscape != null) {
313+
procedureMetadata = getProcedureMetadata(databaseMetaData, metaDataCatalogName,
314+
escapeNamePattern(metaDataSchemaName, searchStringEscape),
315+
escapeNamePattern(metaDataProcedureName, searchStringEscape));
328316
}
329317
}
330-
331-
if (found.isEmpty()) {
318+
if (procedureMetadata.hits() == 0) {
332319
// Functions not exposed as procedures anymore on PostgreSQL driver 42.2.11
333-
try (ResultSet functions = databaseMetaData.getFunctions(
334-
metaDataCatalogName, escapedSchemaName, escapedProcedureName)) {
335-
while (functions.next()) {
336-
found.add(functions.getString("FUNCTION_CAT") + '.' + functions.getString("FUNCTION_SCHEM") +
337-
'.' + functions.getString("FUNCTION_NAME"));
338-
function = true;
320+
procedureMetadata = getProcedureMetadataAsFunction(databaseMetaData,
321+
metaDataCatalogName, metaDataSchemaName, metaDataProcedureName);
322+
if (procedureMetadata.hits() > 1) {
323+
// Try again with exact match in case of placeholders
324+
String searchStringEscape = databaseMetaData.getSearchStringEscape();
325+
if (searchStringEscape != null) {
326+
procedureMetadata = getProcedureMetadataAsFunction(
327+
databaseMetaData, metaDataCatalogName,
328+
escapeNamePattern(metaDataSchemaName, searchStringEscape),
329+
escapeNamePattern(metaDataProcedureName, searchStringEscape));
339330
}
340331
}
341332
}
333+
// Handling matches
342334

343-
if (found.size() > 1) {
335+
boolean isFunction = procedureMetadata.function();
336+
List<String> matches = procedureMetadata.matches;
337+
if (matches.size() > 1) {
344338
throw new InvalidDataAccessApiUsageException(
345339
"Unable to determine the correct call signature - multiple signatures for '" +
346-
metaDataProcedureName + "': found " + found + " " + (function ? "functions" : "procedures"));
340+
metaDataProcedureName + "': found " + matches + " " + (isFunction ? "functions" : "procedures"));
347341
}
348-
else if (found.isEmpty()) {
342+
else if (matches.isEmpty()) {
349343
if (metaDataProcedureName != null && metaDataProcedureName.contains(".") &&
350344
!StringUtils.hasText(metaDataCatalogName)) {
351345
String packageName = metaDataProcedureName.substring(0, metaDataProcedureName.indexOf('.'));
@@ -368,25 +362,25 @@ else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) {
368362
}
369363

370364
if (logger.isDebugEnabled()) {
371-
logger.debug("Retrieving column meta-data for " + (function ? "function" : "procedure") + ' ' +
372-
metaDataCatalogName + '/' + metaDataSchemaName + '/' + metaDataProcedureName);
365+
logger.debug("Retrieving column meta-data for " + (isFunction ? "function" : "procedure") + ' ' +
366+
metaDataCatalogName + '/' + procedureMetadata.schemaName + '/' + procedureMetadata.procedureName);
373367
}
374-
try (ResultSet columns = function ?
375-
databaseMetaData.getFunctionColumns(metaDataCatalogName, escapedSchemaName, escapedProcedureName, null) :
376-
databaseMetaData.getProcedureColumns(metaDataCatalogName, escapedSchemaName, escapedProcedureName, null)) {
368+
try (ResultSet columns = isFunction ?
369+
databaseMetaData.getFunctionColumns(metaDataCatalogName, procedureMetadata.schemaName, procedureMetadata.procedureName, null) :
370+
databaseMetaData.getProcedureColumns(metaDataCatalogName, procedureMetadata.schemaName, procedureMetadata.procedureName, null)) {
377371
while (columns.next()) {
378372
String columnName = columns.getString("COLUMN_NAME");
379373
int columnType = columns.getInt("COLUMN_TYPE");
380-
if (columnName == null && isInOrOutColumn(columnType, function)) {
374+
if (columnName == null && isInOrOutColumn(columnType, isFunction)) {
381375
if (logger.isDebugEnabled()) {
382376
logger.debug("Skipping meta-data for: " + columnType + " " + columns.getInt("DATA_TYPE") +
383377
" " + columns.getString("TYPE_NAME") + " " + columns.getInt("NULLABLE") +
384378
" (probably a member of a collection)");
385379
}
386380
}
387381
else {
388-
int nullable = (function ? DatabaseMetaData.functionNullable : DatabaseMetaData.procedureNullable);
389-
CallParameterMetaData meta = new CallParameterMetaData(function, columnName, columnType,
382+
int nullable = (isFunction ? DatabaseMetaData.functionNullable : DatabaseMetaData.procedureNullable);
383+
CallParameterMetaData meta = new CallParameterMetaData(isFunction, columnName, columnType,
390384
columns.getInt("DATA_TYPE"), columns.getString("TYPE_NAME"),
391385
columns.getInt("NULLABLE") == nullable);
392386
this.callParameterMetaData.add(meta);
@@ -413,6 +407,36 @@ else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) {
413407
}
414408
}
415409

410+
private ProcedureMetadata getProcedureMetadata(DatabaseMetaData databaseMetaData,
411+
@Nullable String catalogName, @Nullable String schemaName, @Nullable String procedureName) throws SQLException {
412+
if (logger.isDebugEnabled()) {
413+
logger.debug("Retrieving meta-data for " + catalogName + '/' + schemaName + '/' + procedureName);
414+
}
415+
List<String> matches = new ArrayList<>();
416+
try (ResultSet procedures = databaseMetaData.getProcedures(catalogName, schemaName, procedureName)) {
417+
while (procedures.next()) {
418+
matches.add(procedures.getString("PROCEDURE_CAT") + '.' + procedures.getString("PROCEDURE_SCHEM") +
419+
'.' + procedures.getString("PROCEDURE_NAME"));
420+
}
421+
}
422+
return new ProcedureMetadata(schemaName, procedureName, matches, false);
423+
}
424+
425+
private ProcedureMetadata getProcedureMetadataAsFunction(DatabaseMetaData databaseMetaData,
426+
@Nullable String catalogName, @Nullable String schemaName, @Nullable String procedureName) throws SQLException {
427+
if (logger.isDebugEnabled()) {
428+
logger.debug("Fallback on retrieving function meta-data for " + catalogName + '/' + schemaName + '/' + procedureName);
429+
}
430+
List<String> matches = new ArrayList<>();
431+
try (ResultSet functions = databaseMetaData.getFunctions(catalogName, schemaName, procedureName)) {
432+
while (functions.next()) {
433+
matches.add(functions.getString("FUNCTION_CAT") + '.' + functions.getString("FUNCTION_SCHEM") +
434+
'.' + functions.getString("FUNCTION_NAME"));
435+
}
436+
}
437+
return new ProcedureMetadata(schemaName, procedureName, matches, true);
438+
}
439+
416440
@Nullable
417441
private static String escapeNamePattern(@Nullable String name, @Nullable String escape) {
418442
if (name == null || escape == null) {
@@ -436,4 +460,12 @@ private static boolean isInOrOutColumn(int columnType, boolean function) {
436460
}
437461
}
438462

463+
private record ProcedureMetadata(@Nullable String schemaName, @Nullable String procedureName,
464+
List<String> matches, boolean function) {
465+
466+
int hits() {
467+
return this.matches.size();
468+
}
469+
}
470+
439471
}

0 commit comments

Comments
 (0)