Skip to content

Commit

Permalink
Revisit stored procedure detection
Browse files Browse the repository at this point in the history
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
  • Loading branch information
snicoll committed Feb 21, 2024
1 parent 93f0ec2 commit 5d6501c
Show file tree
Hide file tree
Showing 3 changed files with 306 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
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;
Expand Down Expand Up @@ -305,47 +304,42 @@ private void processProcedureColumns(DatabaseMetaData databaseMetaData,
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<String> 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<String> 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('.'));
Expand All @@ -368,25 +362,25 @@ else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) {
}

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") +
" (probably a member of a collection)");
}
}
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);
Expand All @@ -413,6 +407,36 @@ else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) {
}
}

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<String> 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<String> 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) {
Expand All @@ -436,4 +460,12 @@ private static boolean isInOrOutColumn(int columnType, boolean function) {
}
}

private record ProcedureMetadata(@Nullable String schemaName, @Nullable String procedureName,
List<String> matches, boolean function) {

int hits() {
return this.matches.size();
}
}

}

0 comments on commit 5d6501c

Please sign in to comment.