Skip to content

Commit 6c85e2c

Browse files
muskan124947wooln
andauthored
PreparedStatement.executeBatch() Throws BatchUpdateException "Unable to retrieve column metadata." When Insert SQL Column Name Case Does Not Match Table (#2695)
* test: add tests for column case sensitivity in batch execution - Add test case for matching column case in batch execution - Add test case for mismatched column case in batch execution, expected SQLServerException * fix(batch): improve column metadata handling for batch execution - Update column index search to be case-insensitive in SQLServerPreparedStatement - Remove failing test case in BatchExecutionTest that was checking for specific exception * test(BatchExecutionTest): Add cleanup for caseSensitiveTable in the finally block * test: remove unused test case for column case matching * feat(collation): add case sensitivity check for database collation - Add getIsCaseSensitive() method to SQLCollation class - Update SQLServerPreparedStatement to use case sensitivity information - Improve performance by avoiding unnecessary case-insensitive comparisons * fix(batch): handle case sensitivity for column names in batch execution - Add support for case-sensitive column names in SQLServerBulkBatchInsertRecord - Update SQLServerBulkRecord to handle case sensitivity in column metadata - Modify SQLServerPreparedStatement to pass case sensitivity information to batch records - Add tests for case-sensitive and case-insensitive column name handling in batch execution * refactor: Replace wildcard import with specific SQLServerException import in BatchExecutionTest * revert: throw BatchUpdateException * perf: remove redundant column index calculation * test(BatchExecutionTest): Add case sensitive and case insensitive database in unit test. * fix: handle null SortOrder in getIsCaseSensitive to keep it defensive - Return false (case-insensitive) if sortOrder is not found * Updated logic for null handling --------- Co-authored-by: 徐云金YunjinXu <[email protected]>
1 parent 960791b commit 6c85e2c

File tree

5 files changed

+154
-5
lines changed

5 files changed

+154
-5
lines changed

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,17 @@ int getCollationSortID() {
7878
return this.sortId;
7979
}
8080

81+
/**
82+
* return collation is case-sensitive or not
83+
*
84+
* @return
85+
*/
86+
boolean getIsCaseSensitive() {
87+
SortOrder sortOrder = sortOrderIndex.get(this.sortId);
88+
// consider case-insensitive if no SortOrder entry
89+
return sortOrder != null && sortOrder.name.contains("_CS_");
90+
}
91+
8192
boolean isEqual(SQLCollation col) {
8293
return (col != null && col.info == info && col.sortId == sortId);
8394
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class SQLServerBulkBatchInsertRecord extends SQLServerBulkRecord {
4444
* Constructs a SQLServerBulkBatchInsertRecord with the batch parameter, column list, value list, and encoding
4545
*/
4646
SQLServerBulkBatchInsertRecord(ArrayList<Parameter[]> batchParam, ArrayList<String> columnList,
47-
ArrayList<String> valueList, String encoding) throws SQLServerException {
47+
ArrayList<String> valueList, String encoding, boolean columnNameCaseSensitive) throws SQLServerException {
4848
initLoggerResources();
4949
if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) {
5050
loggerExternal.entering(loggerPackageName, loggerClassName, new Object[] {batchParam, encoding});
@@ -61,6 +61,7 @@ class SQLServerBulkBatchInsertRecord extends SQLServerBulkRecord {
6161
this.batchParam = batchParam;
6262
this.columnList = columnList;
6363
this.valueList = valueList;
64+
this.columnNameCaseSensitive = columnNameCaseSensitive;
6465
columnMetadata = new HashMap<>();
6566

6667
loggerExternal.exiting(loggerPackageName, loggerClassName);

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ protected class ColumnMetadata {
6464
*/
6565
protected transient DateTimeFormatter timeFormatter = null;
6666

67+
protected boolean columnNameCaseSensitive = false;
68+
6769
/*
6870
* Logger
6971
*/
@@ -153,7 +155,10 @@ void checkDuplicateColumnName(int positionInTable, String colName) throws SQLSer
153155
// duplicate check is not performed in case of same
154156
// positionInTable value
155157
if (null != entry && entry.getKey() != positionInTable && null != entry.getValue()
156-
&& colName.trim().equalsIgnoreCase(entry.getValue().columnName)) {
158+
&& (this.columnNameCaseSensitive
159+
? colName.trim().equals(entry.getValue().columnName)
160+
: colName.trim().equalsIgnoreCase(entry.getValue().columnName)
161+
)) {
157162
throw new SQLServerException(SQLServerException.getErrString("R_BulkDataDuplicateColumn"), null);
158163
}
159164
}

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,7 +2199,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
21992199
}
22002200

22012201
SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(
2202-
batchParamValues, bcOperationColumnList, bcOperationValueList, null);
2202+
batchParamValues, bcOperationColumnList, bcOperationValueList, null, isDBColationCaseSensitive());
22032203

22042204
for (int i = 1; i <= rs.getColumnCount(); i++) {
22052205
Column c = rs.getColumn(i);
@@ -2216,7 +2216,22 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
22162216
jdbctype = ti.getSSType().getJDBCType().getIntValue();
22172217
}
22182218
if (null != bcOperationColumnList && !bcOperationColumnList.isEmpty()) {
2219-
int columnIndex = bcOperationColumnList.indexOf(c.getColumnName());
2219+
// connection contains database name
2220+
boolean isCaseSensitive = isDBColationCaseSensitive();
2221+
int columnIndex = -1;
2222+
if (isCaseSensitive) {
2223+
columnIndex = bcOperationColumnList.indexOf(c.getColumnName());
2224+
} else {
2225+
// find index ignore case
2226+
for (int opi = 0; opi < bcOperationColumnList.size(); opi++) {
2227+
String opCol = bcOperationColumnList.get(opi);
2228+
if (opCol != null && opCol.equalsIgnoreCase(c.getColumnName())) {
2229+
columnIndex = opi;
2230+
break;
2231+
}
2232+
}
2233+
}
2234+
22202235
if (columnIndex > -1) {
22212236
columnMappings.put(columnIndex + 1, i);
22222237
batchRecord.addColumnMetadata(columnIndex + 1, c.getColumnName(), jdbctype,
@@ -2392,7 +2407,7 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio
23922407
}
23932408

23942409
SQLServerBulkBatchInsertRecord batchRecord = new SQLServerBulkBatchInsertRecord(
2395-
batchParamValues, bcOperationColumnList, bcOperationValueList, null);
2410+
batchParamValues, bcOperationColumnList, bcOperationValueList, null, isDBColationCaseSensitive());
23962411

23972412
for (int i = 1; i <= rs.getColumnCount(); i++) {
23982413
Column c = rs.getColumn(i);
@@ -2484,6 +2499,12 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio
24842499
}
24852500
}
24862501

2502+
private boolean isDBColationCaseSensitive() throws SQLServerException {
2503+
if (null == connection.getDatabaseCollation())
2504+
return false;
2505+
return connection.getDatabaseCollation().getIsCaseSensitive();
2506+
}
2507+
24872508
private void checkValidColumns(TypeInfo ti) throws SQLServerException {
24882509
int jdbctype = ti.getSSType().getJDBCType().getIntValue();
24892510
String typeName;

src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/BatchExecutionTest.java

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import static org.junit.jupiter.api.Assertions.assertEquals;
1111
import static org.junit.jupiter.api.Assertions.assertTrue;
1212
import static org.junit.jupiter.api.Assertions.fail;
13+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
1314

1415
import java.lang.reflect.Field;
1516
import java.sql.BatchUpdateException;
@@ -72,6 +73,8 @@ public class BatchExecutionTest extends AbstractTest {
7273
.escapeIdentifier(RandomUtil.getIdentifier("timestamptable1"));
7374
private static String timestampTable2 = AbstractSQLGenerator
7475
.escapeIdentifier(RandomUtil.getIdentifier("timestamptable2"));
76+
private static String caseSensitiveDatabase = "BD_Collation_SQL_Latin1_General_CP1_CS_AS";
77+
private static String caseInsensitiveDatabase = "BD_Collation_SQL_Latin1_General_CP1_CI_AS";
7578

7679
/**
7780
* This tests the updateCount when the error query does cause a SQL state HY008.
@@ -568,6 +571,114 @@ public void testBatchStatementCancellation() throws Exception {
568571
}
569572
}
570573

574+
@Test
575+
@Tag(Constants.xAzureSQLDB)
576+
public void testExecuteBatchColumnCaseMismatch_CI() throws Exception {
577+
String connectionStringCollationCaseInsensitive = TestUtils.addOrOverrideProperty(connectionString, "databaseName", caseInsensitiveDatabase);
578+
String tableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("caseInsensitiveTable"));
579+
580+
try (Connection conTemp = DriverManager.getConnection(connectionString); Statement stmt = conTemp.createStatement()) {
581+
TestUtils.dropDatabaseIfExists(caseInsensitiveDatabase, connectionString);
582+
stmt.executeUpdate("CREATE DATABASE " + caseInsensitiveDatabase);
583+
stmt.executeUpdate("ALTER DATABASE " + caseInsensitiveDatabase + " COLLATE SQL_Latin1_General_CP1_CI_AS;");
584+
585+
// Insert Timestamp using prepared statement when useBulkCopyForBatchInsert=true
586+
try (Connection con = DriverManager.getConnection(connectionStringCollationCaseInsensitive
587+
+ ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;")) {
588+
try (Statement statement = con.createStatement()) {
589+
TestUtils.dropTableIfExists(tableName, statement);
590+
String createSql = "CREATE TABLE" + tableName + " (c1 varchar(10))";
591+
statement.execute(createSql);
592+
}
593+
// upper case C1
594+
try (PreparedStatement preparedStatement = con.prepareStatement("INSERT INTO " + tableName + "(C1) VALUES(?)")) {
595+
preparedStatement.setObject(1, "value1");
596+
preparedStatement.addBatch();
597+
preparedStatement.setObject(1, "value2");
598+
preparedStatement.addBatch();
599+
preparedStatement.executeBatch();
600+
}
601+
}
602+
} finally {
603+
TestUtils.dropDatabaseIfExists(caseInsensitiveDatabase, connectionString);
604+
}
605+
}
606+
607+
// adapter Azure pipeline CI, need to add a new environment variable `mssql_jdbc_test_connection_properties_collation_cs`
608+
@Test
609+
@Tag(Constants.xAzureSQLDB)
610+
public void testExecuteBatchColumnCaseMismatch_CS_throwException() throws Exception {
611+
String connectionStringCollationCaseSensitive = TestUtils.addOrOverrideProperty(connectionString, "databaseName", caseSensitiveDatabase);
612+
String tableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("caseSensitiveTable"));
613+
614+
try (Connection conTemp = DriverManager.getConnection(connectionString); Statement stmt = conTemp.createStatement()) {
615+
TestUtils.dropDatabaseIfExists(caseSensitiveDatabase, connectionString);
616+
stmt.executeUpdate("CREATE DATABASE " + caseSensitiveDatabase);
617+
stmt.executeUpdate("ALTER DATABASE " + caseSensitiveDatabase + " COLLATE SQL_Latin1_General_CP1_CS_AS;");
618+
619+
try (Connection con = DriverManager.getConnection(connectionStringCollationCaseSensitive
620+
+ ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;")) {
621+
try (Statement statement = con.createStatement()) {
622+
TestUtils.dropTableIfExists(tableName, statement);
623+
String createSql = "CREATE TABLE" + tableName + " (c1 varchar(10))";
624+
statement.execute(createSql);
625+
}
626+
// upper case C1
627+
try (PreparedStatement preparedStatement = con.prepareStatement("INSERT INTO " + tableName + "(C1) VALUES(?)")) {
628+
preparedStatement.setObject(1, "value1");
629+
preparedStatement.addBatch();
630+
preparedStatement.setObject(1, "value2");
631+
preparedStatement.addBatch();
632+
try {
633+
preparedStatement.executeBatch();
634+
fail("Should have failed");
635+
} catch (Exception ex) {
636+
assertInstanceOf(java.sql.BatchUpdateException.class, ex);
637+
assertEquals("Unable to retrieve column metadata.", ex.getMessage());
638+
}
639+
}
640+
}
641+
} finally {
642+
TestUtils.dropDatabaseIfExists(caseSensitiveDatabase, connectionString);
643+
}
644+
}
645+
646+
// adapter Azure pipeline CI, need to add a new environment variable `mssql_jdbc_test_connection_properties_collation_cs`
647+
@Test
648+
@Tag(Constants.xAzureSQLDB)
649+
public void testExecuteBatchColumnCaseMismatch_CS() throws Exception {
650+
String connectionStringCollationCaseSensitive = TestUtils.addOrOverrideProperty(connectionString, "databaseName", caseSensitiveDatabase);
651+
String tableName = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("caseSensitiveTable"));
652+
653+
try (Connection conTemp = DriverManager.getConnection(connectionString); Statement stmt = conTemp.createStatement()) {
654+
TestUtils.dropDatabaseIfExists(caseSensitiveDatabase, connectionString);
655+
stmt.executeUpdate("CREATE DATABASE " + caseSensitiveDatabase);
656+
stmt.executeUpdate("ALTER DATABASE " + caseSensitiveDatabase + " COLLATE SQL_Latin1_General_CP1_CS_AS;");
657+
658+
// Insert Timestamp using prepared statement when useBulkCopyForBatchInsert=true
659+
try (Connection con = DriverManager.getConnection(connectionStringCollationCaseSensitive
660+
+ ";useBulkCopyForBatchInsert=true;sendTemporalDataTypesAsStringForBulkCopy=false;")) {
661+
try (Statement statement = con.createStatement()) {
662+
TestUtils.dropTableIfExists(tableName, statement);
663+
String createSql = "CREATE TABLE" + tableName + " (c1 varchar(10), C1 varchar(10))";
664+
statement.execute(createSql);
665+
}
666+
// upper case C1
667+
try (PreparedStatement preparedStatement = con.prepareStatement("INSERT INTO " + tableName + "(c1, C1) VALUES(?,?)")) {
668+
preparedStatement.setObject(1, "value1-1");
669+
preparedStatement.setObject(2, "value1-2");
670+
preparedStatement.addBatch();
671+
preparedStatement.setObject(1, "value2-1");
672+
preparedStatement.setObject(2, "value2-2");
673+
preparedStatement.addBatch();
674+
preparedStatement.executeBatch();
675+
}
676+
}
677+
} finally {
678+
TestUtils.dropDatabaseIfExists(caseSensitiveDatabase, connectionString);
679+
}
680+
}
681+
571682
/**
572683
* Get a PreparedStatement object and call the addBatch() method with 3 SQL statements and call the executeBatch()
573684
* method and it should return array of Integer values of length 3

0 commit comments

Comments
 (0)