-
Notifications
You must be signed in to change notification settings - Fork 453
Description
Driver version
mssql-jdbc-13.3.0.jre11-preview
SQL Server version
Microsoft SQL Server 2016 (SP2) (KB4052908) - 13.0.5026.0 (X64)
Mar 18 2018 09:11:49
Copyright (c) Microsoft Corporation
Enterprise Edition (64-bit) on Windows Server 2016 Standard 10.0 (Build 14393: ) (Hypervisor)
Client Operating System
macOS 14.8.2
JAVA/JVM version
11
Problem description
When executing a mixed batch of SQL statements (Insert -> Insert Error -> Insert -> Select) using Statement.execute(), if an exception occurs (e.g., PK violation) and the application catches it and calls getMoreResults(), the driver incorrectly consumes/swallows the update count of the next valid INSERT statement.
Reproduction Steps
1.Create a table with a Primary Key.
2.Execute a batch string containing:
2.1 SQL 1: Valid Insert
2.2 SQL 2: Invalid Insert (Duplicate Key)
2.3 SQL 3: Valid Insert (This result is lost)
2.4 SQL 4: Select
Iterate through results. Catch the exception from SQL 2 and call getMoreResults() to continue.
Actual Behavior: The loop jumps from SQL 2's exception directly to SQL 4's ResultSet. SQL 3's update count is never returned.
Expected Behavior: After recovering from SQL 2's exception, getMoreResults() should return false (indicating update count) and getUpdateCount() should return 1 for SQL 3.
Root Cause Analysis
I believe the issue lies in SQLServerStatement.java, specifically inside the NextResult inner class of TDSTokenHandler.
In onDone(), the driver consumes the DONE token. When processing the DONE token for the failed SQL (SQL 2), the token has UpdateCount = -1 (because it failed) but it is NOT marked as EXECUTE_BATCH.
The logic below incorrectly returns true (continue parsing), which causes the parser to consume the next token (SQL 3's DONE token) immediately, effectively skipping it.
// Current Code in SQLServerStatement.java (inner class NextResult)
// If it's a DML without update count (like an error or SET NOCOUNT), it skips.
// BUT it fails to check if the token itself is an Error token.
if (-1 == doneToken.getUpdateCount() && EXECUTE_BATCH != executeMethod)
return true;Suggested Fix
We should ensure we do not skip the token if it is an error token, so the parser stops and throws the exception at the correct position in the stream.
// Suggested Fix
if (-1 == doneToken.getUpdateCount() && EXECUTE_BATCH != executeMethod && !doneToken.isError())
return true;test code
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
public class JdbcBatchTest {
// Replace with your database connection information
private static final String DB_URL = "jdbc:sqlserver://localhost:38081;databaseName=test;encrypt=false;trustServerCertificate=true;";
private static final String USER = "sa";
private static final String PASS = "1qa@WS3ed";
public static void main(String[] args) {
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
Statement stmt = conn.createStatement()) {
System.out.println("1. Initializing test table...");
initTestTable(stmt);
System.out.println("\n2. Preparing to execute 4 mixed SQL statements (INSERT, INSERT(Error), INSERT, SELECT)...");
// SQL Batch breakdown:
// 1. INSERT (Success)
// 2. INSERT (Failure - Primary Key Conflict Error 2627)
// 3. INSERT (Success - BUT this update count gets swallowed by the driver bug)
// 4. SELECT (Result Set)
String sqlBatch =
"insert into test_schema.table_name values (1, 'test'); " +
"insert into test_schema.table_name values (1, 'test'); " +
"insert into test_schema.table_name values (2, 'test'); " +
"select * from test_schema.table_name;";
// ------------------- Core Traversal Logic Starts -------------------
boolean hasResult = stmt.execute(sqlBatch);
int resultCount = 0;
while (true) {
resultCount++;
System.out.println("--- Processing Result #" + resultCount + " ---");
try {
// Standard JDBC processing logic
if (hasResult) {
try (ResultSet rs = stmt.getResultSet()) {
System.out.println(">>> [Result Type] ResultSet");
while (rs.next()) {
System.out.println(" Row: id=" + rs.getInt("id") + ", column_name=" + rs.getString("column_name"));
}
}
} else {
int updateCount = stmt.getUpdateCount();
if (updateCount == -1) {
System.out.println(">>> [End] No more results");
break; // Exit loop
}
System.out.println(">>> [Result Type] Update Count: " + updateCount);
}
// Attempt to get the next result
hasResult = stmt.getMoreResults();
} catch (SQLException e) {
System.err.println("!!! [Exception Caught] " + e.getMessage() + " (Error Code: " + e.getErrorCode() + ")");
// ================= Core Recovery Logic =================
// The driver throws an exception for the 2nd SQL (Duplicate Key).
// We catch it and try to move to the next result (3rd SQL).
System.out.println(" [Recovery Attempt] Trying to recover from exception and fetch next result...");
try {
// Force move pointer to continue processing the batch
hasResult = stmt.getMoreResults();
} catch (Exception ex) {
System.err.println(" [Recovery Failed] Error during recovery: " + ex.getMessage());
break;
}
// =======================================================
}
}
// ------------------- Core Traversal Logic Ends -------------------
} catch (Exception e) {
e.printStackTrace();
}
}
private static void initTestTable(Statement stmt) {
try {
// Ensure clean state
try {
stmt.execute("drop table test_schema.table_name;");
} catch (Exception ignored) {}
stmt.execute("create table test_schema.table_name (id int primary key, column_name varchar(100));");
} catch (Exception e) {
System.err.println("Failed to initialize table: " + e.getMessage());
}
}
}