diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index 1d2868879..2dfb40d89 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java @@ -258,11 +258,17 @@ private void checkClosed() throws SQLServerException { private static final String IS_AUTOINCREMENT = "IS_AUTOINCREMENT"; private static final String ACTIVITY_ID = " ActivityId: "; + private static final String NVARCHAR = JDBCType.NVARCHAR.name(); + private static final String VARCHAR = JDBCType.VARCHAR.name(); + private static final String INTEGER = JDBCType.INTEGER.name(); + private static final String SMALLINT = JDBCType.SMALLINT.name(); + private static final String SQL_KEYWORDS = createSqlKeyWords(); // Use LinkedHashMap to force retrieve elements in order they were inserted /** getColumns columns */ private LinkedHashMap getColumnsDWColumns = null; + private LinkedHashMap getTypesDWColumns = null; /** getImportedKeys columns */ private volatile LinkedHashMap getImportedKeysDWColumns; private static final Lock LOCK = new ReentrantLock(); @@ -630,10 +636,13 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table + "INSERT INTO @mssqljdbc_temp_sp_columns_result EXEC sp_columns_100 ?,?,?,?,?,?;" - + "SELECT TABLE_QUALIFIER AS TABLE_CAT, TABLE_OWNER AS TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, DATA_TYPE," - + "TYPE_NAME, PRECISION AS COLUMN_SIZE, LENGTH AS BUFFER_LENGTH, SCALE AS DECIMAL_DIGITS, RADIX AS NUM_PREC_RADIX," - + "NULLABLE, REMARKS, COLUMN_DEF, SQL_DATA_TYPE, SQL_DATETIME_SUB, CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE," - + "NULL AS SCOPE_CATALOG, NULL AS SCOPE_SCHEMA, NULL AS SCOPE_TABLE, SS_DATA_TYPE AS SOURCE_DATA_TYPE," + + "SELECT TABLE_QUALIFIER AS TABLE_CAT, TABLE_OWNER AS TABLE_SCHEM, TABLE_NAME, COLUMN_NAME, " + + "CAST(DATA_TYPE AS INT) AS DATA_TYPE,TYPE_NAME, PRECISION AS COLUMN_SIZE, LENGTH AS BUFFER_LENGTH, " + + "CAST(SCALE AS INT) AS DECIMAL_DIGITS, CAST(RADIX AS INT) AS NUM_PREC_RADIX,CAST(NULLABLE AS INT) AS NULLABLE, " + + "CAST(REMARKS AS VARCHAR) AS REMARKS, COLUMN_DEF, CAST(SQL_DATA_TYPE AS INT) AS SQL_DATA_TYPE, " + + "CAST(SQL_DATETIME_SUB AS INT) AS SQL_DATETIME_SUB, CHAR_OCTET_LENGTH, ORDINAL_POSITION, IS_NULLABLE," + + "CAST(NULL AS VARCHAR) AS SCOPE_CATALOG, CAST(NULL AS VARCHAR) AS SCOPE_SCHEMA, CAST(NULL AS VARCHAR) AS SCOPE_TABLE, " + + "CAST(SS_DATA_TYPE AS SMALLINT) AS SOURCE_DATA_TYPE, " + "CASE SS_IS_IDENTITY WHEN 0 THEN 'NO' WHEN 1 THEN 'YES' WHEN '' THEN '' END AS IS_AUTOINCREMENT," + "CASE SS_IS_COMPUTED WHEN 0 THEN 'NO' WHEN 1 THEN 'YES' WHEN '' THEN '' END AS IS_GENERATEDCOLUMN, " + "SS_IS_SPARSE, SS_IS_COLUMN_SET, SS_UDT_CATALOG_NAME, SS_UDT_SCHEMA_NAME, SS_UDT_ASSEMBLY_TYPE_NAME," @@ -721,6 +730,53 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table getColumnsDWColumns.put(27, SS_XML_SCHEMACOLLECTION_SCHEMA_NAME); getColumnsDWColumns.put(28, SS_XML_SCHEMACOLLECTION_NAME); } + if (null == getTypesDWColumns) { + getTypesDWColumns = new LinkedHashMap<>(); + getTypesDWColumns.put(1, NVARCHAR); // TABLE_CAT + getTypesDWColumns.put(2, NVARCHAR); // TABLE_SCHEM + getTypesDWColumns.put(3, NVARCHAR); // TABLE_NAME + getTypesDWColumns.put(4, NVARCHAR); // COLUMN_NAME + getTypesDWColumns.put(5, INTEGER); // DATA_TYPE + getTypesDWColumns.put(6, NVARCHAR); // TYPE_NAME + getTypesDWColumns.put(7, INTEGER); // COLUMN_SIZE + getTypesDWColumns.put(8, INTEGER); // BUFFER_LENGTH + getTypesDWColumns.put(9, INTEGER); // DECIMAL_DIGITS + getTypesDWColumns.put(10, INTEGER); // NUM_PREC_RADIX + getTypesDWColumns.put(11, INTEGER); // NULLABLE + getTypesDWColumns.put(12, VARCHAR); // REMARKS + getTypesDWColumns.put(13, NVARCHAR); // COLUMN_DEF + getTypesDWColumns.put(14, INTEGER); // SQL_DATA_TYPE + getTypesDWColumns.put(15, INTEGER); // SQL_DATETIME_SUB + getTypesDWColumns.put(16, INTEGER); // CHAR_OCTET_LENGTH + getTypesDWColumns.put(17, INTEGER); // ORDINAL_POSITION + getTypesDWColumns.put(18, VARCHAR); // IS_NULLABLE + /* + * Use negative value keys to indicate that this column doesn't exist in SQL Server and should just + * be queried as 'NULL' + */ + getTypesDWColumns.put(-1, VARCHAR); // SCOPE_CATALOG + getTypesDWColumns.put(-2, VARCHAR); // SCOPE_SCHEMA + getTypesDWColumns.put(-3, VARCHAR); // SCOPE_TABLE + getTypesDWColumns.put(29, SMALLINT); // SOURCE_DATA_TYPE + getTypesDWColumns.put(22, VARCHAR); // IS_AUTOINCREMENT + getTypesDWColumns.put(21, VARCHAR); // IS_GENERATEDCOLUMN + getTypesDWColumns.put(19, SMALLINT); // SS_IS_SPARSE + getTypesDWColumns.put(20, SMALLINT); // SS_IS_COLUMN_SET + getTypesDWColumns.put(23, NVARCHAR); // SS_UDT_CATALOG_NAME + getTypesDWColumns.put(24, NVARCHAR); // SS_UDT_SCHEMA_NAME + getTypesDWColumns.put(25, NVARCHAR); // SS_UDT_ASSEMBLY_TYPE_NAME + getTypesDWColumns.put(26, NVARCHAR); // SS_XML_SCHEMACOLLECTION_CATALOG_NAME + getTypesDWColumns.put(27, NVARCHAR); // SS_XML_SCHEMACOLLECTION_SCHEMA_NAME + getTypesDWColumns.put(28, NVARCHAR); // SS_XML_SCHEMACOLLECTION_NAME + } + + // Ensure there is a data type for every metadata column + if (getColumnsDWColumns.size() != getTypesDWColumns.size()) { + MessageFormat form = new MessageFormat( + SQLServerException.getErrString("R_colCountNotMatchColTypeCount")); + Object[] msgArgs = {getColumnsDWColumns.size(), getTypesDWColumns.size()}; + throw new IllegalArgumentException(form.format(msgArgs)); + } } finally { LOCK.unlock(); } @@ -744,7 +800,7 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table if (!isFirstRow) { azureDwSelectBuilder.append(" UNION ALL "); } - azureDwSelectBuilder.append(generateAzureDWSelect(rs, getColumnsDWColumns)); + azureDwSelectBuilder.append(generateAzureDWSelect(rs, getColumnsDWColumns, getTypesDWColumns)); isFirstRow = false; } @@ -780,28 +836,41 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table } } - private String generateAzureDWSelect(ResultSet rs, Map columns) throws SQLException { + private String generateAzureDWSelect(ResultSet rs, Map columns, + Map types) throws SQLException { StringBuilder sb = new StringBuilder("SELECT "); + for (Entry p : columns.entrySet()) { + String dataType = types.get(p.getKey()); + + // Verify there is a valid column entry in the Data Type lookup map + if (dataType == null) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidArgument")); + Object[] msgArgs = {p.getKey()}; + throw new SQLServerException(null, form.format(msgArgs), null, 0, true); + } + + sb.append("CAST("); if (p.getKey() < 0) { - sb.append("NULL"); + sb.append("NULL AS " + dataType); } else { Object o = rs.getObject(p.getKey()); if (null == o) { - sb.append("NULL"); + sb.append("NULL AS " + dataType); } else if (o instanceof Number) { if (IS_AUTOINCREMENT.equalsIgnoreCase(p.getValue()) || IS_GENERATEDCOLUMN.equalsIgnoreCase(p.getValue())) { sb.append("'").append(Util.escapeSingleQuotes(Util.zeroOneToYesNo(((Number) o).intValue()))) - .append("'"); + .append("' AS ").append(dataType); } else { - sb.append(o.toString()); + sb.append(o.toString()).append(" AS ").append(dataType); } } else { - sb.append("'").append(Util.escapeSingleQuotes(o.toString())).append("'"); + sb.append("'").append(Util.escapeSingleQuotes(o.toString())).append("' AS ").append(dataType) + .append("(").append(Integer.toString(o.toString().length())).append(")"); } } - sb.append(" AS ").append(p.getValue()).append(","); + sb.append(") AS ").append(p.getValue()).append(","); } sb.setLength(sb.length() - 1); return sb.toString(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 95b5ffa0b..6cd502b72 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -545,7 +545,8 @@ protected Object[][] getContents() { {"R_ManagedIdentityTokenAcquisitionFail", "Failed to acquire managed identity token. Request for the token succeeded, but no token was returned. The token is null."}, {"R_AmbiguousRowUpdate", "Failed to execute updateRow(). The update is attempting an ambiguous update on tables \"{0}\" and \"{1}\". Ensure all columns being updated prior to the updateRow() call belong to the same table."}, {"R_InvalidSqlQuery", "Invalid SQL Query: {0}"}, - {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."} + {"R_InvalidScale", "Scale of input value is larger than the maximum allowed by SQL Server."}, + {"R_colCountNotMatchColTypeCount", "Number of provided columns {0} does not match the column data types definition {1}."}, }; } // @formatter:on diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index e14bb671a..607254030 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -212,5 +212,6 @@ protected Object[][] getContents() { {"R_failedFedauth", "Failed to acquire fedauth token: "}, {"R_noLoginModulesConfiguredForJdbcDriver", "javax.security.auth.login.LoginException (No LoginModules configured for SQLJDBCDriver)"}, - {"R_unexpectedThreadCount", "Thread count is higher than expected."}}; + {"R_unexpectedThreadCount", "Thread count is higher than expected."}, + {"R_expectedClassDoesNotMatchActualClass", "Expected column class {0} does not match actual column class {1} for column {2}."}}; } 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 ff2f2fc73..999ccdc1e 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -63,6 +63,38 @@ public class DatabaseMetaDataTest extends AbstractTest { private static final String functionName = RandomUtil.getIdentifier("DBMetadataFunction"); private static Map getColumnsDWColumns = null; private static Map getImportedKeysDWColumns = null; + private static final String TABLE_CAT = "TABLE_CAT"; + private static final String TABLE_SCHEM = "TABLE_SCHEM"; + private static final String TABLE_NAME = "TABLE_NAME"; + private static final String COLUMN_NAME = "COLUMN_NAME"; + private static final String DATA_TYPE = "DATA_TYPE"; + private static final String TYPE_NAME = "TYPE_NAME"; + private static final String COLUMN_SIZE = "COLUMN_SIZE"; + private static final String BUFFER_LENGTH = "BUFFER_LENGTH"; + private static final String DECIMAL_DIGITS = "DECIMAL_DIGITS"; + private static final String NUM_PREC_RADIX = "NUM_PREC_RADIX"; + private static final String NULLABLE = "NULLABLE"; + private static final String REMARKS = "REMARKS"; + private static final String COLUMN_DEF = "COLUMN_DEF"; + private static final String SQL_DATA_TYPE = "SQL_DATA_TYPE"; + private static final String SQL_DATETIME_SUB = "SQL_DATETIME_SUB"; + private static final String CHAR_OCTET_LENGTH = "CHAR_OCTET_LENGTH"; + private static final String ORDINAL_POSITION = "ORDINAL_POSITION"; + private static final String IS_NULLABLE = "IS_NULLABLE"; + private static final String SCOPE_CATALOG = "SCOPE_CATALOG"; + private static final String SCOPE_SCHEMA = "SCOPE_SCHEMA"; + private static final String SCOPE_TABLE = "SCOPE_TABLE"; + private static final String SOURCE_DATA_TYPE = "SOURCE_DATA_TYPE"; + private static final String IS_AUTOINCREMENT = "IS_AUTOINCREMENT"; + private static final String IS_GENERATEDCOLUMN = "IS_GENERATEDCOLUMN"; + private static final String SS_IS_SPARSE = "SS_IS_SPARSE"; + private static final String SS_IS_COLUMN_SET = "SS_IS_COLUMN_SET"; + private static final String SS_UDT_CATALOG_NAME = "SS_UDT_CATALOG_NAME"; + private static final String SS_UDT_SCHEMA_NAME = "SS_UDT_SCHEMA_NAME"; + private static final String SS_UDT_ASSEMBLY_TYPE_NAME = "SS_UDT_ASSEMBLY_TYPE_NAME"; + private static final String SS_XML_SCHEMACOLLECTION_CATALOG_NAME = "SS_XML_SCHEMACOLLECTION_CATALOG_NAME"; + private static final String SS_XML_SCHEMACOLLECTION_SCHEMA_NAME = "SS_XML_SCHEMACOLLECTION_SCHEMA_NAME"; + private static final String SS_XML_SCHEMACOLLECTION_NAME = "SS_XML_SCHEMACOLLECTION_NAME"; /** * Verify DatabaseMetaData#isWrapperFor and DatabaseMetaData#unwrap. @@ -887,6 +919,78 @@ public void testGetImportedKeysDW() throws SQLException { } } + /** + * Validates the metadata data types defined by JDBC spec. + * Refer to DatabaseMetadata getColumns() specs + * + * @throws SQLException + */ + @Test + public void testValidateColumnMetadata() throws SQLException { + Map> getColumnMetaDataClass = new LinkedHashMap<>(); + + getColumnMetaDataClass.put(TABLE_CAT, String.class); + getColumnMetaDataClass.put(TABLE_SCHEM, String.class); + getColumnMetaDataClass.put(TABLE_NAME, String.class); + getColumnMetaDataClass.put(COLUMN_NAME, String.class); + getColumnMetaDataClass.put(DATA_TYPE, Integer.class); + getColumnMetaDataClass.put(TYPE_NAME, String.class); + getColumnMetaDataClass.put(COLUMN_SIZE, Integer.class); + getColumnMetaDataClass.put(BUFFER_LENGTH, Integer.class); // Not used + getColumnMetaDataClass.put(DECIMAL_DIGITS, Integer.class); + getColumnMetaDataClass.put(NUM_PREC_RADIX, Integer.class); + getColumnMetaDataClass.put(NULLABLE, Integer.class); + getColumnMetaDataClass.put(REMARKS, String.class); + getColumnMetaDataClass.put(COLUMN_DEF, String.class); + getColumnMetaDataClass.put(SQL_DATA_TYPE, Integer.class); + getColumnMetaDataClass.put(SQL_DATETIME_SUB, Integer.class); + getColumnMetaDataClass.put(CHAR_OCTET_LENGTH, Integer.class); + getColumnMetaDataClass.put(ORDINAL_POSITION, Integer.class); + getColumnMetaDataClass.put(IS_NULLABLE, String.class); + getColumnMetaDataClass.put(SCOPE_CATALOG, String.class); + getColumnMetaDataClass.put(SCOPE_SCHEMA, String.class); + getColumnMetaDataClass.put(SCOPE_TABLE, String.class); + getColumnMetaDataClass.put(SOURCE_DATA_TYPE, Short.class); + getColumnMetaDataClass.put(IS_AUTOINCREMENT, String.class); + getColumnMetaDataClass.put(IS_GENERATEDCOLUMN, String.class); + getColumnMetaDataClass.put(SS_IS_SPARSE, Short.class); + getColumnMetaDataClass.put(SS_IS_COLUMN_SET, Short.class); + getColumnMetaDataClass.put(SS_UDT_CATALOG_NAME, String.class); + getColumnMetaDataClass.put(SS_UDT_SCHEMA_NAME, String.class); + getColumnMetaDataClass.put(SS_UDT_ASSEMBLY_TYPE_NAME, String.class); + getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_CATALOG_NAME, String.class); + getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_SCHEMA_NAME, String.class); + getColumnMetaDataClass.put(SS_XML_SCHEMACOLLECTION_NAME, String.class); + + try (Connection conn = getConnection()) { + ResultSetMetaData metadata = conn.getMetaData().getColumns(null, null, tableName, null).getMetaData(); + + // Ensure that there is an expected class for every column in the metadata result set + assertEquals(metadata.getColumnCount(), getColumnMetaDataClass.size()); + + for (int i = 1; i < metadata.getColumnCount(); i++) { + String columnLabel = metadata.getColumnLabel(i); + String columnClassName = metadata.getColumnClassName(i); + Class expectedClass = getColumnMetaDataClass.get(columnLabel); + + // Ensure the metadata column is in the metadata column class map + if (expectedClass == null) { + MessageFormat form1 = new MessageFormat(TestResource.getResource("R_objectNullOrEmpty")); + Object[] msgArgs1 = {"expected metadata column class for column " + columnLabel}; + fail(form1.format(msgArgs1)); + } + + // Ensure the actual and expected column metadata types match + if (!columnClassName.equals(expectedClass.getName())) { + MessageFormat form1 = new MessageFormat( + TestResource.getResource("R_expectedClassDoesNotMatchActualClass")); + Object[] msgArgs1 = {expectedClass.getName(), columnClassName, columnLabel}; + fail(form1.format(msgArgs1)); + } + } + } + } + @BeforeAll public static void setupTable() throws Exception { setConnection();