Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix to ensure metadata returned follows JDBC data type specs #2326

Merged
merged 10 commits into from
Mar 5, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import com.microsoft.sqlserver.jdbc.JDBCType;
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved


/**
Expand Down Expand Up @@ -258,11 +259,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<Integer, String> getColumnsDWColumns = null;
private LinkedHashMap<Integer, String> getTypesDWColumns = null;
/** getImportedKeys columns */
private volatile LinkedHashMap<Integer, String> getImportedKeysDWColumns;
private static final Lock LOCK = new ReentrantLock();
Expand Down Expand Up @@ -630,10 +637,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,"
Expand Down Expand Up @@ -721,6 +731,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();
}
Expand All @@ -744,7 +801,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;
}

Expand Down Expand Up @@ -780,28 +837,41 @@ public java.sql.ResultSet getColumns(String catalog, String schema, String table
}
}

private String generateAzureDWSelect(ResultSet rs, Map<Integer, String> columns) throws SQLException {
private String generateAzureDWSelect(ResultSet rs, Map<Integer, String> columns,
Map<Integer, String> types) throws SQLException {
StringBuilder sb = new StringBuilder("SELECT ");

for (Entry<Integer, String> 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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,7 @@ 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_classLoaderNotFoundForColumnType", "Class Loader for type {0} not found for column {1}"},
{"R_classNotAssignable", "Class {0} is not assignable from class {1} for column {2}"}};
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,39 @@ public class DatabaseMetaDataTest extends AbstractTest {
private static final String functionName = RandomUtil.getIdentifier("DBMetadataFunction");
private static Map<Integer, String> getColumnsDWColumns = null;
private static Map<Integer, String> getImportedKeysDWColumns = null;
private static Map<String, Class<?>> getColumnMetaDataClass = null;
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -887,6 +920,67 @@ public void testGetImportedKeysDW() throws SQLException {
}
}

@Test
// Validates the metadata data types defined by JDBC spec
// https://docs.oracle.com/javase/8/docs/api/java/sql/DatabaseMetaData.html#getColumns-java.lang.String-java.lang.String-java.lang.String-java.lang.String-
public void testValidateColumnMetadata() throws SQLException {
lilgreenbird marked this conversation as resolved.
Show resolved Hide resolved
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved

if (getColumnMetaDataClass == null) {
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
assertNotNull(expectedClass);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was mentioned in a previous comment above, but ensure that expectedClass is not null, don't assert.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assertNotNull(expectedClass);
if (null != expectedClass) {

Copy link
Member

@lilgreenbird lilgreenbird Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this really shouldn't even be necessary, we just defined this above, this would never be null

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we accidentally remove the check for isAssignableFrom that was in previous commit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expected class can be null if the the column label/name in the expected class lookup table does not match the actual column label from the result set (i.e. the lookup by column name will fail). This shouldn't happen but it is possible if there was a future code change or perhaps the returned metadata columns change.

Yes the IsAssignableFrom() is no longer used as I changed the BUFFER_LENGTH metadata class from Object to Integer. It was because of this generic Object class that we needed to use the IsAssignableFrom() check. Is The reason why it was originally specified as a generic Object was that the JDBC spec does not use the BUFFER_LENGTH column and so I reflected that as a generic Object being the expected data type. But in the code I have always ensured that BUFFER_LENGTH was an Integer. Therefore to simplify the logic and remove the IsAssignableFrom() check, I switched the expected class for the BUFFER_LENGTH column from Object to Integer.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have kept the check but no longer use an assert to perform the check. I found that in the unlikely event there was an issue, a more detail error message was required that would include not just the actual and expected types but the column as well. Without the column name/label, it would initially be difficult t determine what the problem column was in the metadata


// Ensure the actual and expected column metadata types match
assertEquals(columnClassName, expectedClass.getName());
}
}
}

@BeforeAll
public static void setupTable() throws Exception {
setConnection();
Expand Down
Loading