diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index bad2e5097f..88d09f4ab0 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java @@ -315,6 +315,38 @@ private void checkClosed() throws SQLServerException { "AND ic.key_ordinal = 0 " + "ORDER BY t.name, i.name, ic.key_ordinal"; + private static final String GET_FUNCTIONS_QUERY_BASE = "SELECT " + + "DB_NAME() AS FUNCTION_CAT, " + + "SCHEMA_NAME(o.schema_id) AS FUNCTION_SCHEM, " + + "o.name AS FUNCTION_NAME, " + + "-1 AS NUM_INPUT_PARAMS, " + + "-1 AS NUM_OUTPUT_PARAMS, " + + "-1 AS NUM_RESULT_SETS, " + + "CAST(NULL AS VARCHAR(254)) AS REMARKS, " + + "CASE o.type " + + "WHEN 'FN' THEN " + java.sql.DatabaseMetaData.functionReturnsTable + " " + + "WHEN 'IF' THEN " + java.sql.DatabaseMetaData.functionReturnsTable + " " + + "WHEN 'TF' THEN " + java.sql.DatabaseMetaData.functionReturnsTable + " " + + "ELSE " + java.sql.DatabaseMetaData.functionNoNulls + " " + + "END AS FUNCTION_TYPE " + + "FROM sys.all_objects o " + + "WHERE o.type IN ("; + + private static final String GET_PROCEDURES_QUERY_BASE = "SELECT " + + "DB_NAME() AS PROCEDURE_CAT, " + + "SCHEMA_NAME(o.schema_id) AS PROCEDURE_SCHEM, " + + "o.name AS PROCEDURE_NAME, " + + "-1 AS NUM_INPUT_PARAMS, " + + "-1 AS NUM_OUTPUT_PARAMS, " + + "-1 AS NUM_RESULT_SETS, " + + "CAST(NULL AS VARCHAR(254)) AS REMARKS, " + + "1 AS PROCEDURE_TYPE " + + "FROM sys.all_objects o " + + "WHERE o.type IN ("; + + private static final String FUNCTIONS_ORDER_BY = " ORDER BY FUNCTION_CAT, FUNCTION_SCHEM, FUNCTION_NAME"; + private static final String PROCEDURES_ORDER_BY = " ORDER BY PROCEDURE_CAT, PROCEDURE_SCHEM, PROCEDURE_NAME"; + // Use LinkedHashMap to force retrieve elements in order they were inserted /** getColumns columns */ private LinkedHashMap getColumnsDWColumns = null; @@ -432,6 +464,81 @@ private SQLServerResultSet getResultSetWithProvidedColumnNames(String catalog, C return rs; } + /* + * Builds a filter string for object types based on the provided object types array. + * + * @param objectTypes + * Array of object types to be included in the filter. + * + * @return A string representing the filter for the object types. + */ + private String buildObjectTypesFilter(String[] objectTypes) { + return "'" + String.join("', '", objectTypes) + "')"; + } + + /* + * Helper method to append catalog, schema, and object name filters to the query + */ + private void appendFiltersAndOrderBy(StringBuilder queryBuilder, String catalog, String schemaPattern, + String objectNamePattern, String orderByClause) { + + // Add catalog filter + if (catalog != null) { + queryBuilder.append(" AND DB_NAME() = ?"); + } + + // Add schema filter + if (schemaPattern != null && !schemaPattern.equals("%")) { + queryBuilder.append(" AND SCHEMA_NAME(o.schema_id) LIKE ?"); + } + + // Add object name filter + if (objectNamePattern != null && !objectNamePattern.equals("%")) { + queryBuilder.append(" AND o.name LIKE ?"); + } + + queryBuilder.append(orderByClause); + } + + /* + * Executes a query against sys.objects to retrieve metadata about database objects. + * This method is used for both functions and procedures. + */ + private SQLServerResultSet executeSysObjectsQuery(String catalog, String schemaPattern, + String objectNamePattern, String query, String[] columnNames) throws SQLException { + + String orgCat = switchCatalogs(catalog); + + try { + PreparedStatement pstmt = connection.prepareStatement(query); + pstmt.closeOnCompletion(); + int paramIndex = 1; + + if (catalog != null) { + pstmt.setString(paramIndex++, escapeIDName(catalog)); + } + if (schemaPattern != null && !schemaPattern.equals("%")) { + pstmt.setString(paramIndex++, escapeIDName(schemaPattern)); + } + if (objectNamePattern != null && !objectNamePattern.equals("%")) { + pstmt.setString(paramIndex++, escapeIDName(objectNamePattern)); + } + + SQLServerResultSet rs = (SQLServerResultSet) pstmt.executeQuery(); + + // Set the column names to match the expected format + for (int i = 0; i < columnNames.length; i++) { + rs.setColumnName(i + 1, columnNames[i]); + } + return rs; + + } finally { + if (null != orgCat) { + connection.setCatalog(orgCat); + } + } + } + /** * Switches the database catalogs. * @@ -947,23 +1054,34 @@ public java.sql.ResultSet getFunctions(String catalog, String schemaPattern, String functionNamePattern) throws SQLException { checkClosed(); - /* - * sp_stored_procedures [ [ @sp_name = ] 'name' ] [ , [ @sp_owner = ] 'schema'] [ , [ @sp_qualifier = ] - * 'qualifier' ] [ , [@fUsePattern = ] 'fUsePattern' ] - */ // use default ie use pattern matching. // catalog cannot be empty in sql server if (null != catalog && catalog.length() == 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument")); - Object[] msgArgs = {"catalog"}; + Object[] msgArgs = { "catalog" }; SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, false); } - String[] arguments = new String[3]; - arguments[0] = escapeIDName(functionNamePattern); - arguments[1] = escapeIDName(schemaPattern); - arguments[2] = catalog; - return getResultSetWithProvidedColumnNames(catalog, CallableHandles.SP_STORED_PROCEDURES, arguments, - getFunctionsColumnNames); + return getFunctionsFromSysObjects(catalog, schemaPattern, functionNamePattern, getFunctionsColumnNames); + } + + /* + * Returns a ResultSet containing metadata about functions in the database. + * This method queries the sys.all_objects system view to retrieve + * information about functions, including their names, schemas, and types. + */ + private SQLServerResultSet getFunctionsFromSysObjects(String catalog, String schemaPattern, + String functionNamePattern, String[] columnNames) throws SQLException { + + String[] functionTypes = { "FN", "IF", "TF" }; + + // Build the query + StringBuilder queryBuilder = new StringBuilder(GET_FUNCTIONS_QUERY_BASE); + queryBuilder.append(buildObjectTypesFilter(functionTypes)); + + appendFiltersAndOrderBy(queryBuilder, catalog, schemaPattern, functionNamePattern, FUNCTIONS_ORDER_BY); + + return executeSysObjectsQuery(catalog, schemaPattern, functionNamePattern, queryBuilder.toString(), + columnNames); } private static final String[] getFunctionsColumnsColumnNames = { /* 1 */ FUNCTION_CAT, /* 2 */ FUNCTION_SCHEM, @@ -1540,22 +1658,34 @@ public java.sql.ResultSet getProcedureColumns(String catalog, String schema, Str @Override public java.sql.ResultSet getProcedures(String catalog, String schema, - String proc) throws SQLServerException, SQLTimeoutException { + String proc) throws SQLServerException, SQLTimeoutException, SQLException { if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); - /* - * sp_stored_procedures [ [ @sp_name = ] 'name' ] [ , [ @sp_owner = ] 'schema'] [ , [ @sp_qualifier = ] - * 'qualifier' ] [ , [@fUsePattern = ] 'fUsePattern' ] - */ - String[] arguments = new String[3]; - arguments[0] = escapeIDName(proc); - arguments[1] = escapeIDName(schema); - arguments[2] = catalog; - return getResultSetWithProvidedColumnNames(catalog, CallableHandles.SP_STORED_PROCEDURES, arguments, - getProceduresColumnNames); + + return getProceduresFromSysObjects(catalog, schema, proc, getProceduresColumnNames); + } + + /* + * Returns a ResultSet containing metadata about stored procedures in the database. + * This method queries the sys.all_objects system view to retrieve information about + * procedures, including their names, schemas, and types. + */ + private SQLServerResultSet getProceduresFromSysObjects(String catalog, String schemaPattern, + String procedureNamePattern, String[] columnNames) throws SQLException { + + String[] procedureTypes = { "P", "PC" }; + + // Build the query + StringBuilder queryBuilder = new StringBuilder(GET_PROCEDURES_QUERY_BASE); + queryBuilder.append(buildObjectTypesFilter(procedureTypes)); + + appendFiltersAndOrderBy(queryBuilder, catalog, schemaPattern, procedureNamePattern, PROCEDURES_ORDER_BY); + + return executeSysObjectsQuery(catalog, schemaPattern, procedureNamePattern, queryBuilder.toString(), + columnNames); } @Override diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java index 060c7648d7..e081338d5f 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestUtils.java @@ -425,12 +425,32 @@ public static void dropTableWithSchemaIfExists(String tableNameWithSchema, "IF OBJECT_ID('" + tableNameWithSchema + "', 'U') IS NOT NULL DROP TABLE " + tableNameWithSchema + ";"); } + /** + * Drops a procedure with schema if it exists. + * + * @param procedureWithSchema + * @param stmt + * @throws SQLException + */ public static void dropProcedureWithSchemaIfExists(String procedureWithSchema, java.sql.Statement stmt) throws SQLException { stmt.execute("IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" + procedureWithSchema + "') AND type in (N'P', N'PC')) DROP PROCEDURE " + procedureWithSchema + ";"); } + /** + * Drops a function with schema if it exists. + * + * @param functionWithSchema + * @param stmt + * @throws SQLException + */ + public static void dropFunctionWithSchemaIfExists(String functionWithSchema, + java.sql.Statement stmt) throws SQLException { + stmt.execute("IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'" + functionWithSchema + + "') AND type in (N'FN', N'IF', N'TF')) DROP FUNCTION " + functionWithSchema + ";"); + } + /** * Deletes the contents of a table. * diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index fea3703e63..0a28218d6d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -29,8 +29,10 @@ import java.text.MessageFormat; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.jar.Attributes; import java.util.jar.Manifest; @@ -1195,6 +1197,247 @@ public void testJSONMetaData() throws SQLException { } } + private void setupProcedures(String schemaName, String proc1, String proc1Body, + String proc2, String proc2Body) throws SQLException { + String escapedSchema = AbstractSQLGenerator.escapeIdentifier(schemaName); + String escapedProc1 = AbstractSQLGenerator.escapeIdentifier(proc1); + String escapedProc2 = AbstractSQLGenerator.escapeIdentifier(proc2); + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + stmt.executeUpdate( + "IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '" + schemaName + "') " + + "EXEC('CREATE SCHEMA " + escapedSchema + "')"); + + stmt.executeUpdate("IF OBJECT_ID('" + schemaName + "." + proc1 + "', 'P') IS NOT NULL " + + "DROP PROCEDURE " + escapedSchema + "." + escapedProc1); + stmt.executeUpdate("CREATE PROCEDURE " + escapedSchema + "." + escapedProc1 + " " + proc1Body); + + stmt.executeUpdate("IF OBJECT_ID('" + schemaName + "." + proc2 + "', 'P') IS NOT NULL " + + "DROP PROCEDURE " + escapedSchema + "." + escapedProc2); + stmt.executeUpdate("CREATE PROCEDURE " + escapedSchema + "." + escapedProc2 + " " + proc2Body); + } + } + + private void setupFunctions(String schemaName, String func1, String func1Body, + String func2, String func2Body) throws SQLException { + String escapedSchema = AbstractSQLGenerator.escapeIdentifier(schemaName); + String escapedFunc1 = AbstractSQLGenerator.escapeIdentifier(func1); + String escapedFunc2 = AbstractSQLGenerator.escapeIdentifier(func2); + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + stmt.executeUpdate( + "IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '" + schemaName + "') " + + "EXEC('CREATE SCHEMA " + escapedSchema + "')"); + + stmt.executeUpdate("IF OBJECT_ID('" + schemaName + "." + func1 + "', 'FN') IS NOT NULL " + + "DROP FUNCTION " + escapedSchema + "." + escapedFunc1); + stmt.executeUpdate("CREATE FUNCTION " + escapedSchema + "." + escapedFunc1 + " " + func1Body); + + stmt.executeUpdate("IF OBJECT_ID('" + schemaName + "." + func2 + "', 'FN') IS NOT NULL " + + "DROP FUNCTION " + escapedSchema + "." + escapedFunc2); + stmt.executeUpdate("CREATE FUNCTION " + escapedSchema + "." + escapedFunc2 + " " + func2Body); + } + } + + /** + * Test to verify getProcedures() metadata structure and PROCEDURE_TYPE values + * + * @throws SQLException + */ + @Test + public void testGetProceduresMetadataValidation() throws SQLException { + String schemaName = "test_schema" + uuid; + String proc1 = "sp_test1" + uuid; + String proc2 = "sp_test2" + uuid; + + setupProcedures(schemaName, + proc1, "AS BEGIN SELECT 1; END", + proc2, "@val INT AS BEGIN SELECT @val * 2; END"); + + try (Connection conn = getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + String[] expectedCols = { + "PROCEDURE_CAT", "PROCEDURE_SCHEM", "PROCEDURE_NAME", "NUM_INPUT_PARAMS", + "NUM_OUTPUT_PARAMS", "NUM_RESULT_SETS", "REMARKS", "PROCEDURE_TYPE" + }; + + try (ResultSet rs = metaData.getProcedures(null, schemaName, "sp_test%")) { + ResultSetMetaData rsMeta = rs.getMetaData(); + assertEquals(expectedCols.length, rsMeta.getColumnCount()); + + for (int i = 1; i <= rsMeta.getColumnCount(); i++) { + assertEquals(expectedCols[i - 1], rsMeta.getColumnName(i)); + } + + boolean foundProcedure = false; + int rowCount = 0; + while (rs.next() && rowCount < 5) { + foundProcedure = true; + rowCount++; + + // Verify required fields are not null/empty + assertNotNull(rs.getString("PROCEDURE_CAT")); + assertNotNull(rs.getString("PROCEDURE_SCHEM")); + assertNotNull(rs.getString("PROCEDURE_NAME")); + + // Verify PROCEDURE_TYPE - should be 1 for procedures that don't return result + int procedureType = rs.getInt("PROCEDURE_TYPE"); + assertEquals(1, procedureType); + + // Verify parameter counts are -1 (unknown) as per JDBC spec + assertEquals(-1, rs.getInt("NUM_INPUT_PARAMS")); + assertEquals(-1, rs.getInt("NUM_OUTPUT_PARAMS")); + assertEquals(-1, rs.getInt("NUM_RESULT_SETS")); + } + + assertTrue(foundProcedure, "At least one procedure should be found in schema"); + System.out.println("Verified " + rowCount + " procedures with PROCEDURE_TYPE = 1"); + + } + } finally { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + TestUtils.dropProcedureWithSchemaIfExists(schemaName + "." + proc1, stmt); + TestUtils.dropProcedureWithSchemaIfExists(schemaName + "." + proc2, stmt); + + TestUtils.dropSchemaIfExists(schemaName, stmt); + } + } + } + + /** + * Test to verify getFunctions() metadata structure and FUNCTION_TYPE values + * + * @throws SQLException + */ + @Test + public void testGetFunctionsMetadataValidation() throws SQLException { + String schemaName = "test_schema" + uuid; + String func1 = "fn_test1" + uuid; + String func2 = "fn_test2" + uuid; + + setupFunctions(schemaName, + func1, "() RETURNS INT AS BEGIN RETURN 42; END", + func2, "(@val INT) RETURNS INT AS BEGIN RETURN @val * 2; END"); + + try (Connection conn = getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + String[] expectedCols = { + "FUNCTION_CAT", "FUNCTION_SCHEM", "FUNCTION_NAME", "NUM_INPUT_PARAMS", + "NUM_OUTPUT_PARAMS", "NUM_RESULT_SETS", "REMARKS", "FUNCTION_TYPE" + }; + + try (ResultSet rs = metaData.getFunctions(null, schemaName, "fn_test%")) { + ResultSetMetaData rsMeta = rs.getMetaData(); + assertEquals(expectedCols.length, rsMeta.getColumnCount()); + for (int i = 1; i <= rsMeta.getColumnCount(); i++) { + assertEquals(expectedCols[i - 1], rsMeta.getColumnName(i)); + } + + boolean foundFunction = false; + int rowCount = 0; + while (rs.next() && rowCount < 5) { + foundFunction = true; + rowCount++; + + // Verify required fields are not null/empty + assertNotNull(rs.getString("FUNCTION_CAT")); + assertNotNull(rs.getString("FUNCTION_SCHEM")); + assertNotNull(rs.getString("FUNCTION_NAME")); + + // Verify FUNCTION_TYPE - should be 2 for SQL functions + int functionType = rs.getInt("FUNCTION_TYPE"); + assertEquals(2, functionType); + + // Verify parameter counts are -1 (unknown) as per JDBC spec + assertEquals(-1, rs.getInt("NUM_INPUT_PARAMS")); + assertEquals(-1, rs.getInt("NUM_OUTPUT_PARAMS")); + assertEquals(-1, rs.getInt("NUM_RESULT_SETS")); + } + + assertTrue(foundFunction, "At least one function should be found in schema"); + System.out.println("Verified " + rowCount + " functions with FUNCTION_TYPE = 2"); + + } + } finally { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + TestUtils.dropFunctionWithSchemaIfExists(schemaName + "." + func1, stmt); + TestUtils.dropFunctionWithSchemaIfExists(schemaName + "." + func2, stmt); + + TestUtils.dropSchemaIfExists(schemaName, stmt); + } + } + } + + /** + * Test to verify getProcedures() with controlled data using specific procedures + * + * @throws SQLException + */ + @Test + public void testGetProceduresWithData() throws SQLException { + String schemaName = "test_Schema" + uuid; + String proc1 = "sproc_test1" + uuid; + String proc2 = "sproc_test2" + uuid; + + setupProcedures(schemaName, + proc1, "AS BEGIN SELECT 1; END", + proc2, "@val INT AS BEGIN SELECT @val * 2; END"); + + try (Connection conn = getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + try (ResultSet rs = metaData.getProcedures(null, schemaName, "sproc_test%")) { + Set foundProcedures = new HashSet<>(); + while (rs.next()) { + foundProcedures.add(rs.getString("PROCEDURE_NAME")); + assertEquals(1, rs.getInt("PROCEDURE_TYPE")); + } + assertEquals(new HashSet<>(Arrays.asList(proc1, proc2)), foundProcedures); + } + } finally { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + TestUtils.dropProcedureWithSchemaIfExists(schemaName + "." + proc1, stmt); + TestUtils.dropProcedureWithSchemaIfExists(schemaName + "." + proc2, stmt); + + TestUtils.dropSchemaIfExists(schemaName, stmt); + } + } + } + + /** + * Test to verify getFunctions() with controlled data using specific functions + * + * @throws SQLException + */ + @Test + public void testGetFunctionsWithData() throws SQLException { + String schemaName = "test_Schema" + uuid; + String func1 = "function_test1" + uuid; + String func2 = "function_test2" + uuid; + + setupFunctions(schemaName, + func1, "() RETURNS INT AS BEGIN RETURN 42; END", + func2, "(@val INT) RETURNS INT AS BEGIN RETURN @val * 2; END"); + + try (Connection conn = getConnection()) { + DatabaseMetaData metaData = conn.getMetaData(); + try (ResultSet rs = metaData.getFunctions(null, schemaName, "function_test%")) { + Set foundFunctions = new HashSet<>(); + while (rs.next()) { + foundFunctions.add(rs.getString("FUNCTION_NAME")); + assertEquals(2, rs.getInt("FUNCTION_TYPE")); + } + assertEquals(new HashSet<>(Arrays.asList(func1, func2)), foundFunctions); + } + } finally { + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + TestUtils.dropFunctionWithSchemaIfExists(schemaName + "." + func1, stmt); + TestUtils.dropFunctionWithSchemaIfExists(schemaName + "." + func2, stmt); + + TestUtils.dropSchemaIfExists(schemaName, stmt); + } + } + } + @BeforeAll public static void setupTable() throws Exception { setConnection();