Skip to content

Commit c31a6cf

Browse files
committed
Fix collation conflict in getIndexInfo() UNION ALL with different server/database collations
1 parent 303aeb9 commit c31a6cf

File tree

2 files changed

+152
-18
lines changed

2 files changed

+152
-18
lines changed

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,22 @@ private void checkClosed() throws SQLServerException {
275275
"); " +
276276
"INSERT INTO @temp_sp_statistics " +
277277
"EXEC sp_statistics ?, ?, ?, ?, ?, ?; " +
278-
"SELECT TABLE_QUALIFIER AS TABLE_CAT, TABLE_OWNER AS TABLE_SCHEM, " +
279-
"TABLE_NAME, NON_UNIQUE, INDEX_QUALIFIER, INDEX_NAME, TYPE, " +
280-
"ORDINAL_POSITION, COLUMN_NAME, ASC_OR_DESC, CARDINALITY, PAGES, FILTER_CONDITION " +
278+
"SELECT TABLE_QUALIFIER COLLATE DATABASE_DEFAULT AS TABLE_CAT, " +
279+
"TABLE_OWNER COLLATE DATABASE_DEFAULT AS TABLE_SCHEM, " +
280+
"TABLE_NAME COLLATE DATABASE_DEFAULT AS TABLE_NAME, NON_UNIQUE, " +
281+
"INDEX_QUALIFIER COLLATE DATABASE_DEFAULT AS INDEX_QUALIFIER, " +
282+
"INDEX_NAME COLLATE DATABASE_DEFAULT AS INDEX_NAME, TYPE, " +
283+
"ORDINAL_POSITION, COLUMN_NAME COLLATE DATABASE_DEFAULT AS COLUMN_NAME, ASC_OR_DESC, " +
284+
"CARDINALITY, PAGES, FILTER_CONDITION " +
281285
"FROM @temp_sp_statistics " +
282286
"UNION ALL " +
283-
"SELECT db_name() AS TABLE_CAT, sch.name AS TABLE_SCHEM, t.name AS TABLE_NAME, " +
284-
"CASE WHEN i.is_unique = 1 THEN 0 ELSE 1 END AS NON_UNIQUE, t.name AS INDEX_QUALIFIER, i.name AS INDEX_NAME, " +
285-
"i.type AS TYPE, ic.key_ordinal AS ORDINAL_POSITION, c.name AS COLUMN_NAME, " +
287+
"SELECT db_name() AS TABLE_CAT, sch.name COLLATE DATABASE_DEFAULT AS TABLE_SCHEM, " +
288+
"t.name COLLATE DATABASE_DEFAULT AS TABLE_NAME, " +
289+
"CASE WHEN i.is_unique = 1 THEN 0 ELSE 1 END AS NON_UNIQUE, " +
290+
"t.name COLLATE DATABASE_DEFAULT AS INDEX_QUALIFIER, " +
291+
"i.name COLLATE DATABASE_DEFAULT AS INDEX_NAME, " +
292+
"i.type AS TYPE, ic.key_ordinal AS ORDINAL_POSITION, " +
293+
"c.name COLLATE DATABASE_DEFAULT AS COLUMN_NAME, " +
286294
"CASE WHEN ic.is_descending_key = 1 THEN 'D' ELSE 'A' END AS ASC_OR_DESC, " +
287295
"CASE WHEN i.index_id <= 1 THEN ps.row_count ELSE NULL END AS CARDINALITY, " +
288296
"CASE WHEN i.index_id <= 1 THEN ps.used_page_count ELSE NULL END AS PAGES, " +
@@ -294,7 +302,7 @@ private void checkClosed() throws SQLServerException {
294302
"INNER JOIN sys.schemas sch ON t.schema_id = sch.schema_id " +
295303
"LEFT JOIN sys.dm_db_partition_stats ps ON ps.object_id = i.object_id AND ps.index_id = i.index_id AND ps.index_id IN (0,1) " +
296304
"WHERE t.name = ? AND sch.name = ? AND ic.key_ordinal = 0 " +
297-
"ORDER BY NON_UNIQUE, TYPE, INDEX_NAME, ORDINAL_POSITION";
305+
"ORDER BY NON_UNIQUE, TYPE, INDEX_NAME COLLATE DATABASE_DEFAULT, ORDINAL_POSITION";
298306

299307
private static final String INDEX_INFO_QUERY_DW = "SELECT db_name() AS TABLE_CAT, " +
300308
"sch.name AS TABLE_SCHEM, " +
@@ -1300,17 +1308,25 @@ public java.sql.ResultSet getIndexInfo(String cat, String schema, String table,
13001308
if (this.connection.isAzureDW()) {
13011309
return getIndexInfoAzureDW(arguments, table, schema);
13021310
} else {
1303-
PreparedStatement pstmt = (SQLServerPreparedStatement) this.connection.prepareStatement(INDEX_INFO_COMBINED_QUERY);
1304-
pstmt.setString(1, arguments[0]); // table name for sp_statistics
1305-
pstmt.setString(2, arguments[1]); // schema name for sp_statistics
1306-
pstmt.setString(3, arguments[2]); // catalog for sp_statistics
1307-
pstmt.setString(4, arguments[3]); // index name pattern for sp_statistics
1308-
pstmt.setString(5, arguments[4]); // is_unique for sp_statistics
1309-
pstmt.setString(6, arguments[5]); // accuracy for sp_statistics
1310-
pstmt.setString(7, table); // table name for columnstore query
1311-
pstmt.setString(8, schema); // schema name for columnstore query
1312-
1313-
return pstmt.executeQuery();
1311+
try {
1312+
PreparedStatement pstmt = (SQLServerPreparedStatement) this.connection.prepareStatement(INDEX_INFO_COMBINED_QUERY);
1313+
pstmt.setString(1, arguments[0]); // table name for sp_statistics
1314+
pstmt.setString(2, arguments[1]); // schema name for sp_statistics
1315+
pstmt.setString(3, arguments[2]); // catalog for sp_statistics
1316+
pstmt.setString(4, arguments[3]); // index name pattern for sp_statistics
1317+
pstmt.setString(5, arguments[4]); // is_unique for sp_statistics
1318+
pstmt.setString(6, arguments[5]); // accuracy for sp_statistics
1319+
pstmt.setString(7, table); // table name for columnstore query
1320+
pstmt.setString(8, schema); // schema name for columnstore query
1321+
1322+
return pstmt.executeQuery();
1323+
} catch (SQLException e) {
1324+
if (loggerExternal.isLoggable(Level.FINER)) {
1325+
loggerExternal.finer("INDEX_INFO_COMBINED_QUERY failed, falling back to sp_statistics: " + e.getMessage());
1326+
}
1327+
return getResultSetWithProvidedColumnNames(cat, CallableHandles.SP_STATISTICS, arguments,
1328+
getIndexInfoColumnNames);
1329+
}
13141330
}
13151331
} finally {
13161332
if (null != orgCat) {

src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.sql.CallableStatement;
2222
import java.sql.Connection;
2323
import java.sql.DatabaseMetaData;
24+
import java.sql.PreparedStatement;
2425
import java.sql.ResultSet;
2526
import java.sql.ResultSetMetaData;
2627
import java.sql.SQLException;
@@ -1382,6 +1383,123 @@ public void testGetIndexInfoResultSetNextAfterFalse() throws SQLException {
13821383
}
13831384
}
13841385
}
1386+
1387+
@Test
1388+
@Tag(Constants.xAzureSQLDW)
1389+
@Tag(Constants.xAzureSQLDB)
1390+
public void testGetIndexInfoCollationConflict() throws SQLException {
1391+
/**
1392+
* Test case for GitHub Issue #2856: SQLServerDatabaseMetaData getIndexInfo collation conflict reproduction
1393+
*
1394+
* This test reproduces the collation conflict issue where getIndexInfo() fails when
1395+
* server collation differs from database collation in INDEX_INFO_COMBINED_QUERY.
1396+
*/
1397+
1398+
UUID id = UUID.randomUUID();
1399+
String testDbName = "CollationTestDB" + id;
1400+
String testTableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("CollationTestTable"));
1401+
1402+
try (Connection masterConnection = getConnection()) {
1403+
TestUtils.dropDatabaseIfExists(testDbName, connectionString);
1404+
1405+
// Create test database with different collation
1406+
try (Statement stmt = masterConnection.createStatement()) {
1407+
stmt.execute(String.format("CREATE DATABASE [%s] COLLATE Finnish_Swedish_CI_AS", testDbName));
1408+
}
1409+
// Connect to the new database and test getIndexInfo
1410+
testGetIndexInfoInDifferentCollationDatabase(testDbName, testTableName);
1411+
1412+
} catch (Exception e) {
1413+
fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage());
1414+
} finally {
1415+
TestUtils.dropDatabaseIfExists(testDbName, connectionString);
1416+
}
1417+
}
1418+
1419+
/**
1420+
* Test getIndexInfo with the different collation database
1421+
*/
1422+
private void testGetIndexInfoInDifferentCollationDatabase(String dbName, String tableName) throws SQLException {
1423+
try (Connection connection = getConnection(); Statement stmt = connection.createStatement()) {
1424+
// Switch to the test database with different collation
1425+
stmt.execute(String.format("USE [%s]", dbName));
1426+
1427+
// Create test table and indexes in the different collation database
1428+
setupTestTableWithIndexes(connection, tableName);
1429+
1430+
DatabaseMetaData dbmd = connection.getMetaData();
1431+
1432+
// Test getIndexInfo - this should trigger INDEX_INFO_COMBINED_QUERY
1433+
try (ResultSet rs = dbmd.getIndexInfo(null, "dbo", tableName.replaceAll("\\[|\\]", ""), false, false)) {
1434+
boolean foundIndexes = false;
1435+
int count = 0;
1436+
1437+
while (rs.next() && count < 20) {
1438+
String indexName = rs.getString("INDEX_NAME");
1439+
1440+
if (indexName != null) {
1441+
foundIndexes = true;
1442+
}
1443+
count++;
1444+
}
1445+
assertTrue(foundIndexes, "Should find indexes on the test table");
1446+
}
1447+
}
1448+
}
1449+
1450+
/**
1451+
* Create test table with mixed collation columns and indexes
1452+
*/
1453+
private void setupTestTableWithIndexes(Connection connection, String tableName) throws SQLException {
1454+
// Clean up any existing test table first
1455+
try (Statement stmt = connection.createStatement()) {
1456+
TestUtils.dropTableIfExists(tableName.replaceAll("\\[|\\]", ""), stmt);
1457+
}
1458+
1459+
// Create table with mixed collation columns
1460+
String createTableSql =
1461+
"CREATE TABLE " + tableName + " (" +
1462+
"ID INT IDENTITY(1,1) PRIMARY KEY, " +
1463+
"NameDefault NVARCHAR(100), " + // Will inherit Finnish_Swedish_CI_AS
1464+
"NameLatin1 NVARCHAR(100) COLLATE SQL_Latin1_General_CP1_CI_AS, " + // Explicit Latin1
1465+
"NameFinnish NVARCHAR(100), " + // Will inherit Finnish_Swedish_CI_AS
1466+
"Description NVARCHAR(255)" + // Will inherit Finnish_Swedish_CI_AS
1467+
")";
1468+
1469+
try (PreparedStatement stmt = connection.prepareStatement(createTableSql)) {
1470+
stmt.executeUpdate();
1471+
}
1472+
1473+
// Insert test data
1474+
String insertSql = "INSERT INTO " + tableName + " (NameDefault, NameLatin1, NameFinnish, Description) VALUES (?, ?, ?, ?)";
1475+
try (PreparedStatement stmt = connection.prepareStatement(insertSql)) {
1476+
stmt.setString(1, "Testiarvo1"); // Default (Finnish)
1477+
stmt.setString(2, "TestValue1"); // Latin1
1478+
stmt.setString(3, "Ääkköset1"); // Finnish with special chars
1479+
stmt.setString(4, "Kuvaus 1"); // Description in Finnish
1480+
stmt.executeUpdate();
1481+
1482+
stmt.setString(1, "Testiarvo2"); // Default (Finnish)
1483+
stmt.setString(2, "TestValue2"); // Latin1
1484+
stmt.setString(3, "Öljytuote2"); // Finnish with special chars
1485+
stmt.setString(4, "Kuvaus 2"); // Description in Finnish
1486+
stmt.executeUpdate();
1487+
}
1488+
1489+
// Create indexes that may trigger collation conflicts in INDEX_INFO_COMBINED_QUERY
1490+
String[] indexQueries = {
1491+
"CREATE NONCLUSTERED INDEX " + AbstractSQLGenerator.escapeIdentifier("IX_CollationTest_Default") + " ON " + tableName + " (NameDefault)",
1492+
"CREATE NONCLUSTERED INDEX " + AbstractSQLGenerator.escapeIdentifier("IX_CollationTest_Latin1") + " ON " + tableName + " (NameLatin1)",
1493+
"CREATE NONCLUSTERED INDEX " + AbstractSQLGenerator.escapeIdentifier("IX_CollationTest_Finnish") + " ON " + tableName + " (NameFinnish)",
1494+
"CREATE NONCLUSTERED INDEX " + AbstractSQLGenerator.escapeIdentifier("IX_CollationTest_Mixed") + " ON " + tableName + " (NameDefault, NameLatin1, NameFinnish)"
1495+
};
1496+
1497+
for (String indexSql : indexQueries) {
1498+
try (PreparedStatement stmt = connection.prepareStatement(indexSql)) {
1499+
stmt.executeUpdate();
1500+
}
1501+
}
1502+
}
13851503
}
13861504

13871505
private void setupProcedures(String schemaName, String proc1, String proc1Body,

0 commit comments

Comments
 (0)