Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ public void setErrorNumber(int newErrorNumber) {
this.errorNumber = newErrorNumber;
}

public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}

@Override
public SQLException toSqlExceptionOrSqlWarning() {
return new SQLServerException(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ protected Object[][] getContents() {
{"R_vectorByteArrayLength", "Vector byte array length must be at least 8 bytes."},
{"R_invalidVectorData", "The provided type of data is not supported for vector."},
{"R_vectorByteArrayMultipleOfBytesPerDimension", "Byte array length must be a multiple of {0} for vector of type {1}."},
{"R_severeError", "A severe error occurred on the current command. The results, if any, should be discarded."},

};
}
// @formatter:on
9 changes: 9 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/tdsparser.java
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,21 @@ boolean onRetValue(TDSReader tdsReader) throws SQLServerException {
}

boolean onDone(TDSReader tdsReader) throws SQLServerException {
short status = tdsReader.peekStatusFlag();
StreamDone doneToken = new StreamDone();
doneToken.setFromTDS(tdsReader);
if (doneToken.isFinal()) {
// Response is completely processed hence decrement unprocessed response count.
tdsReader.getConnection().getSessionRecovery().decrementUnprocessedResponseCount();
}

if ((status & TDS.DONE_ERROR) != 0 || (status & TDS.DONE_SRVERROR) != 0) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

SqlClient treats DONE_SRVERROR as a fatal error:
https://github.com/dotnet/SqlClient/blob/v6.1.1/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs#L3167

Fatal errors ultimately close the connection and signal up to connection pools about the error. We need to plumb through something similar. See makeFromDatabaseError in SQLServerException.java. We don't have the database error, so we will need to figure something out there.

Copy link
Contributor Author

@divang divang Aug 29, 2025

Choose a reason for hiding this comment

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

I think, adding Error Severity 20 will make sure the notifyPooledConnection() call.
SQLServerError syntheticError = new SQLServerError();
syntheticError.setErrorMessage(SQLServerException.getErrString("R_severeError"));
syntheticError.setErrorSeverity((byte) 20); // A severity of 20 indicates a fatal error in the current
// process.

Ref: makeFromDatabaseError in SQLServerException.java:
// Close the connection if we get a severity 20 or higher error class (nClass is severity of error).
if ((sqlServerError.getErrorSeverity() >= 20) && (null != con)) {
con.notifyPooledConnection(theException);
con.close();
}

SQLServerError syntheticError = new SQLServerError();
syntheticError.setErrorMessage(SQLServerException.getErrString("R_severeError"));
syntheticError.setErrorSeverity((byte) 20); // A severity of 20 indicates a fatal error in the current
// process.
addDatabaseError(syntheticError);
}
return true;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.microsoft.sqlserver.jdbc;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/**
* Unit test for TDSTokenHandler onDone method, specifically testing
* severity 25 (fatal) error handling and SQLServerError generation.
*/
public class TDSTokenHandlerTest {

@Mock
private TDSReader mockTdsReader;

@Mock
private SQLServerConnection mockConnection;

@Mock
private IdleConnectionResiliency mockSessionRecovery;

@Mock
private TDSCommand mockCommand;

@BeforeEach
void setUp() throws SQLServerException {
MockitoAnnotations.openMocks(this);

// Setup basic mocks
when(mockTdsReader.getConnection()).thenReturn(mockConnection);
when(mockConnection.getSessionRecovery()).thenReturn(mockSessionRecovery);
when(mockTdsReader.getCommand()).thenReturn(mockCommand);

// Mock the TDS data reading methods that StreamDone.setFromTDS() needs
when(mockTdsReader.readUnsignedByte()).thenReturn(TDS.TDS_DONE); // token type
when(mockTdsReader.readShort()).thenReturn((short) 0); // status and curCmd
when(mockTdsReader.readLong()).thenReturn(0L); // rowCount
}

/**
* Simplified test case specifically for Severity 25 fatal error simulation.
* This test mocks the TDS status flags to simulate a severity 25 error
* and verifies that SQLServerError is properly generated.
*/
@Test
void testSeverity25FatalErrorSimulation() throws SQLServerException {
// Arrange - Simulate severity 25 fatal error with DONE_ERROR status
short severity25ErrorStatus = (short) (TDS.DONE_ERROR); // 0x0002 - indicates error condition

// Mock TDSReader to return the error status that would be set for severity 25 errors
when(mockTdsReader.peekStatusFlag()).thenReturn(severity25ErrorStatus);

// Override the readShort to return the error status for the StreamDone.setFromTDS() call
// The first readShort call is for the status field in StreamDone
when(mockTdsReader.readShort())
.thenReturn(severity25ErrorStatus);

// Create token handler to test
TDSTokenHandler handler = new TDSTokenHandler("Severity25Test");

// Act - Call onDone method which should detect the error status and create SQLServerError
handler.onDone(mockTdsReader);

// Verify that SQLServerError was generated due to the error status
SQLServerError generatedError = handler.getDatabaseError();
assertNotNull(generatedError, "SQLServerError must be generated for severity 25 error condition");

// Verify the error message is the expected server error message
String expectedErrorMessage = SQLServerException.getErrString("R_severeError");
assertEquals(expectedErrorMessage, generatedError.getErrorMessage(),
"Generated error should have R_severeError message for fatal errors");

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,14 @@ private static long getTime(Timestamp time) {
public void testMoneyWithBulkCopy() throws Exception {
try (Connection conn = PrepUtil.getConnection(connectionString)) {
testMoneyLimits(Constants.MIN_VALUE_SMALLMONEY - 1, Constants.MAX_VALUE_MONEY - 1, conn); // 1 less than SMALLMONEY MIN
}
try (Connection conn = PrepUtil.getConnection(connectionString)) {
testMoneyLimits(Constants.MAX_VALUE_SMALLMONEY + 1, Constants.MAX_VALUE_MONEY - 1, conn); // 1 more than SMALLMONEY MAX
}
try (Connection conn = PrepUtil.getConnection(connectionString)) {
testMoneyLimits(Constants.MAX_VALUE_SMALLMONEY - 1, Constants.MIN_VALUE_MONEY - 1, conn); // 1 less than MONEY MIN
}
try (Connection conn = PrepUtil.getConnection(connectionString)) {
testMoneyLimits(Constants.MAX_VALUE_SMALLMONEY - 1, Constants.MAX_VALUE_MONEY + 1, conn); // 1 more than MONEY MAX
}
}
Expand Down
Loading