Skip to content

Commit 49e7fe3

Browse files
committed
Fix swallowed update count after error in mixed SQL batch execution
1 parent ad31dfb commit 49e7fe3

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1579,7 +1579,9 @@ boolean onDone(TDSReader tdsReader) throws SQLServerException {
15791579
// the DML/DDL update count is not valid, and this result should be skipped
15801580
// unless it's for a batch where it's ok to report a "done without count"
15811581
// status (Statement.SUCCESS_NO_INFO)
1582-
if (-1 == doneToken.getUpdateCount() && EXECUTE_BATCH != executeMethod)
1582+
1583+
// Prevent driver from skipping a failed DONE token and losing the next statement’s update count.
1584+
if (-1 == doneToken.getUpdateCount() && EXECUTE_BATCH != executeMethod && !doneToken.isError())
15831585
return true;
15841586

15851587
if (-1 != doneToken.getUpdateCount() && EXECUTE_QUERY == executeMethod)

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

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3455,6 +3455,129 @@ public void testPreparedStatementInsertMultipleValuesWithTrigger() throws SQLExc
34553455
}
34563456
}
34573457

3458+
/**
3459+
* Tests execute a mixed SQL batch (INSERT → INSERT error → INSERT → SELECT) using
3460+
* {@link Statement#execute(String)} and verifies correct result traversal after
3461+
* a primary key violation.
3462+
*
3463+
* After catching the expected error, the test calls {@link Statement#getMoreResults()}
3464+
* to continue processing the batch and validates that the update count of the
3465+
* subsequent successful INSERT is not swallowed before reaching the SELECT.
3466+
*
3467+
* Validates that update counts are reported correctly even after an error occurs in the batch.
3468+
* Github issue #2850 : Statement.execute() skips valid update count after catching SQLException in mixed batch execution
3469+
*
3470+
* @throws SQLException
3471+
*/
3472+
@Test
3473+
public void testBatchUpdateCount() throws SQLException {
3474+
// Create separate test tables to avoid conflicts with existing setup
3475+
String testTable = AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("UpdateCountTestTable"));
3476+
3477+
try (Connection conn = getConnection();
3478+
Statement stmt = conn.createStatement()) {
3479+
3480+
TestUtils.dropTableIfExists(testTable, stmt);
3481+
3482+
// Create table
3483+
String createTableSql = String.format("""
3484+
CREATE TABLE %s (
3485+
id int primary key, column_name varchar(100))
3486+
""", testTable);
3487+
3488+
stmt.execute(createTableSql);
3489+
3490+
// SQL Batch breakdown:
3491+
// 1. INSERT (Success)
3492+
// 2. INSERT (Failure - Primary Key Conflict Error 2627)
3493+
// 3. INSERT (Success - BUT this update count gets swallowed by the driver bug)
3494+
// 4. SELECT (Result Set)
3495+
3496+
String sqlBatch =
3497+
"insert into " + testTable + " values (1, 'test'); " +
3498+
"insert into " + testTable + " values (1, 'test'); " +
3499+
"insert into " + testTable + " values (2, 'test'); " +
3500+
"select * from " + testTable + ";";
3501+
3502+
boolean hasResult = stmt.execute(sqlBatch);
3503+
int resultCount = 0;
3504+
List<Integer> updateCounts = new ArrayList<>();
3505+
int resultSetCount = 0;
3506+
boolean exceptionCaught = false;
3507+
3508+
while (true) {
3509+
resultCount++;
3510+
try {
3511+
// Standard JDBC processing logic
3512+
if (hasResult) {
3513+
try (ResultSet rs = stmt.getResultSet()) {
3514+
resultSetCount++;;
3515+
int rowCount = 0;
3516+
while (rs.next()) {
3517+
rowCount++;
3518+
}
3519+
// Verify we get the expected 2 rows in the final SELECT
3520+
if (resultSetCount == 1) {
3521+
assertEquals(2, rowCount, "Final SELECT should return 2 rows");
3522+
}
3523+
}
3524+
} else {
3525+
int updateCount = stmt.getUpdateCount();
3526+
if (updateCount == -1) {
3527+
break; // Exit loop - no more results
3528+
}
3529+
updateCounts.add(updateCount);
3530+
}
3531+
3532+
// Attempt to get the next result
3533+
hasResult = stmt.getMoreResults();
3534+
3535+
} catch (SQLException e) {
3536+
exceptionCaught = true;
3537+
assertEquals(2627, e.getErrorCode(), "Expected primary key violation error");
3538+
assertTrue(e.getMessage().contains("PRIMARY KEY constraint"),
3539+
"Expected primary key constraint violation message");
3540+
3541+
// ================= Core Recovery Logic =================
3542+
// The driver throws an exception for the 2nd SQL (Duplicate Key).
3543+
// We catch it and try to move to the next result (3rd SQL).
3544+
try {
3545+
// Force move pointer to continue processing the batch
3546+
hasResult = stmt.getMoreResults();
3547+
} catch (Exception ex) {
3548+
fail("Failed to recover from batch exception: " + ex.getMessage());
3549+
}
3550+
// =======================================================
3551+
}
3552+
}
3553+
3554+
// Verify test results
3555+
assertTrue(exceptionCaught, "Expected exception for duplicate key was not caught");
3556+
assertEquals(1, resultSetCount, "Should have processed exactly 1 ResultSet");
3557+
3558+
// This is the key assertion that will fail with the current bug:
3559+
// We should get 2 update counts (first INSERT and third INSERT)
3560+
// But due to the bug, we only get 1 update count (the first INSERT)
3561+
// The third INSERT's update count gets swallowed by the driver
3562+
assertEquals(2, updateCounts.size(),
3563+
"Should have 2 update counts (1st INSERT success + 3rd INSERT success), " +
3564+
"but driver bug causes 3rd INSERT update count to be swallowed");
3565+
3566+
// Verify the update counts are correct
3567+
assertEquals(Integer.valueOf(1), updateCounts.get(0), "First INSERT should affect 1 row");
3568+
assertEquals(Integer.valueOf(1), updateCounts.get(1), "Third INSERT should affect 1 row");
3569+
3570+
// Verify final table state
3571+
try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM " + testTable)) {
3572+
assertTrue(rs.next());
3573+
assertEquals(2, rs.getInt(1), "Table should contain exactly 2 rows");
3574+
}
3575+
3576+
TestUtils.dropTableIfExists(testTable, stmt);
3577+
3578+
}
3579+
}
3580+
34583581
@AfterEach
34593582
public void terminate() {
34603583
try (Connection con = getConnection(); Statement stmt = con.createStatement()) {

0 commit comments

Comments
 (0)