Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stored procedure call returns wrong BigDecimal scale and integrated Mockito Dependency into JDBC Driver Codebase. #2582

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,9 @@ dependencies {
'org.bouncycastle:bcprov-jdk18on:1.78',
'com.azure:azure-security-keyvault-keys:4.7.3',
'com.azure:azure-identity:1.12.2',
'com.h2database:h2:2.2.220'
'com.h2database:h2:2.2.220',
'org.mockito:mockito-core:4.11.0',
'org.mockito:mockito-inline:4.11.0',
'net.bytebuddy:byte-buddy:1.15.11',
'net.bytebuddy:byte-buddy-agent:1.15.11'
}
23 changes: 23 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,29 @@
<scope>provided</scope>
</dependency>
<!-- dependencies for running tests -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.15.11</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>1.15.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-console</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.sql.SQLXML;
import java.sql.Time;
import java.sql.Timestamp;
import java.sql.ParameterMetaData;
import java.text.MessageFormat;
import java.time.LocalDateTime;
import java.util.Calendar;
Expand Down Expand Up @@ -150,6 +151,22 @@ public void registerOutParameter(int index, int sqlType) throws SQLServerExcepti
case microsoft.sql.Types.DATETIMEOFFSET:
param.setOutScale(7);
break;
case java.sql.Types.DECIMAL:
// Dynamically handle the scale for DECIMAL output parameters.
// The scale for the DECIMAL type is fetched from the ParameterMetaData.
// This provides flexibility to automatically apply the correct scale as per the database metadata.
ParameterMetaData parameterMetaData = this.getParameterMetaData();
if (parameterMetaData != null) {
try {
// Fetch scale from metadata for DECIMAL type
int scale = parameterMetaData.getScale(index);
param.setOutScale(scale);
} catch (SQLException e) {
loggerExternal.warning("Failed to fetch scale for DECIMAL type parameter at index " + index + ": " + e.getMessage());
throw new SQLServerException(SQLServerException.getErrString("R_InvalidScale"), null, 0, e);
}
}
break;
default:
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.fail;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;

import java.io.StringReader;
import java.math.BigDecimal;
Expand All @@ -17,6 +21,7 @@
import java.sql.Clob;
import java.sql.Connection;
import java.sql.NClob;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
Expand Down Expand Up @@ -1197,15 +1202,14 @@ public void testJdbc41CallableStatementMethods() throws Exception {
assertEquals("2017-05-19 10:47:15.1234567 +02:00",
cstmt.getObject("col14Value", microsoft.sql.DateTimeOffset.class).toString());

// BigDecimal#equals considers the number of decimal places (OutParams always return 4 decimal
// digits rounded up)
assertEquals(0, cstmt.getObject(15, BigDecimal.class).compareTo(new BigDecimal("0.1235")));
// BigDecimal#equals considers the number of decimal places (OutParams always return full precision as specified in the DB schema)
assertEquals(0, cstmt.getObject(15, BigDecimal.class).compareTo(new BigDecimal("0.123456789")));
assertEquals(0,
cstmt.getObject("col15Value", BigDecimal.class).compareTo(new BigDecimal("0.1235")));
cstmt.getObject("col15Value", BigDecimal.class).compareTo(new BigDecimal("0.123456789")));

assertEquals(0, cstmt.getObject(16, BigDecimal.class).compareTo(new BigDecimal("0.1235")));
assertEquals(0, cstmt.getObject(16, BigDecimal.class).compareTo(new BigDecimal("0.1234567890123456789012345678901234567")));
assertEquals(0,
cstmt.getObject("col16Value", BigDecimal.class).compareTo(new BigDecimal("0.1235")));
cstmt.getObject("col16Value", BigDecimal.class).compareTo(new BigDecimal("0.1234567890123456789012345678901234567")));
}
}
}
Expand Down Expand Up @@ -2694,7 +2698,145 @@ public void terminate() throws Exception {
}
}
}


@Nested
@Tag(Constants.xAzureSQLDW)
public class BigDecimalPrecisionTest {

private final String procName1 = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("test_bigdecimal_3"));
private final String procName2 = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("test_bigdecimal_5"));
private final String procNameMaxScale = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("test_bigdecimal_max_scale"));
private final String procNameMaxPrecision = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("test_bigdecimal_max_precision"));
private final String procNameForCatchBlock = AbstractSQLGenerator
.escapeIdentifier(RandomUtil.getIdentifier("test_bigdecimal_catch_block"));

@BeforeEach
public void init() throws SQLException {
try (Connection connection = getConnection()) {
try (Statement stmt = connection.createStatement()) {
TestUtils.dropProcedureIfExists(procName1, stmt);
TestUtils.dropProcedureIfExists(procName2, stmt);
TestUtils.dropProcedureIfExists(procNameMaxScale, stmt);
TestUtils.dropProcedureIfExists(procNameMaxPrecision, stmt);
TestUtils.dropProcedureIfExists(procNameForCatchBlock, stmt);
}

String createProcedureSQL1 = "CREATE PROCEDURE " + procName1 + "\n"
+ " @big_decimal_type decimal(15, 3),\n"
+ " @big_decimal_type_o decimal(15, 3) OUTPUT\n" + "AS\n" + "BEGIN\n"
+ " SET @big_decimal_type_o = @big_decimal_type;\n" + "END;";
String createProcedureSQL2 = "CREATE PROCEDURE " + procName2 + "\n"
+ " @big_decimal_type decimal(15, 5),\n"
+ " @big_decimal_type_o decimal(15, 5) OUTPUT\n" + "AS\n" + "BEGIN\n"
+ " SET @big_decimal_type_o = @big_decimal_type;\n" + "END;";
String createProcedureMaxScale = "CREATE PROCEDURE " + procNameMaxScale + "\n"
+ " @big_decimal_type decimal(38, 38),\n"
+ " @big_decimal_type_o decimal(38, 38) OUTPUT\n" + "AS\n" + "BEGIN\n"
+ " SET @big_decimal_type_o = @big_decimal_type;\n" + "END;";
String createProcedureMaxPrecision = "CREATE PROCEDURE " + procNameMaxPrecision + "\n"
+ " @big_decimal_type decimal(38, 0),\n"
+ " @big_decimal_type_o decimal(38, 0) OUTPUT\n" + "AS\n" + "BEGIN\n"
+ " SET @big_decimal_type_o = @big_decimal_type;\n" + "END;";
String createProcedureForCatchBlock = "CREATE PROCEDURE " + procNameForCatchBlock + "\n" +
"@outParam DECIMAL(10,2) OUTPUT " +
"AS BEGIN SET @outParam = 123.45 END";

try (Statement stmt = connection.createStatement()) {
stmt.execute(createProcedureSQL1);
stmt.execute(createProcedureSQL2);
stmt.execute(createProcedureMaxScale);
stmt.execute(createProcedureMaxPrecision);
stmt.execute(createProcedureForCatchBlock);
}
}
}

@AfterEach
public void terminate() throws SQLException {
try (Connection connection = getConnection(); Statement stmt = connection.createStatement()) {
TestUtils.dropProcedureIfExists(procName1, stmt);
TestUtils.dropProcedureIfExists(procName2, stmt);
TestUtils.dropProcedureIfExists(procNameMaxScale, stmt);
TestUtils.dropProcedureIfExists(procNameMaxPrecision, stmt);
TestUtils.dropProcedureIfExists(procNameForCatchBlock, stmt);
}
}

@Test
@Tag("BigDecimal")
public void testBigDecimalPrecision() throws SQLException {
try (Connection connection = getConnection()) {
// Test for DECIMAL(15, 3)
String callSQL1 = "{call " + procName1 + "(100.241, ?)}";
try (CallableStatement call = connection.prepareCall(callSQL1)) {
call.registerOutParameter(1, Types.DECIMAL);
call.execute();
BigDecimal actual1 = call.getBigDecimal(1);
assertEquals(new BigDecimal("100.241"), actual1);
}

// Test for DECIMAL(15, 5)
String callSQL2 = "{call " + procName2 + "(100.24112, ?)}";
try (CallableStatement call = connection.prepareCall(callSQL2)) {
call.registerOutParameter(1, Types.DECIMAL);
call.execute();
BigDecimal actual2 = call.getBigDecimal(1);
assertEquals(new BigDecimal("100.24112"), actual2);
}

// Test for DECIMAL(38, 38)
String callSQLMaxScale = "{call " + procNameMaxScale + "(?, ?)}";
try (CallableStatement call = connection.prepareCall(callSQLMaxScale)) {
BigDecimal maxScaleValue = new BigDecimal("0.11111111111111111111111111111111111111");
call.setBigDecimal(1, maxScaleValue);
call.registerOutParameter(2, Types.DECIMAL);
call.execute();
BigDecimal actualMaxScale = call.getBigDecimal(2);
assertEquals(maxScaleValue, actualMaxScale, "DECIMAL(38, 38) max scale test failed");
}

// Test for DECIMAL(38, 0)
String callSQLMaxPrecision = "{call " + procNameMaxPrecision + "(?, ?)}";
try (CallableStatement call = connection.prepareCall(callSQLMaxPrecision)) {
BigDecimal maxPrecisionValue = new BigDecimal("99999999999999999999999999999999999999");
call.setBigDecimal(1, maxPrecisionValue);
call.registerOutParameter(2, Types.DECIMAL);
call.execute();
BigDecimal actualMaxPrecision = call.getBigDecimal(2);
assertEquals(maxPrecisionValue, actualMaxPrecision, "DECIMAL(38, 0) max precision test failed");
}
}
}

@Test
@Tag("BigDecimal")
public void testRegisterOutParameterForBigDecimalCatchBlock() throws SQLException {
try (Connection con = getConnection()) {
try (CallableStatement realCallableStatement = con.prepareCall("{call "+ procNameForCatchBlock + "(?)}")) {

CallableStatement spyCallableStatement = spy(realCallableStatement);
ParameterMetaData spyMetaData = spy(realCallableStatement.getParameterMetaData());

// Simulate an exception for getScale
doThrow(new SQLException("Simulated error for BigDecimal scale retrieval"))
.when(spyMetaData).getScale(1);

// Inject the mocked ParameterMetaData back into the spy CallableStatement
doReturn(spyMetaData).when(spyCallableStatement).getParameterMetaData();

// Assert that the catch block for SQLException is executed
SQLServerException thrownException = assertThrows(SQLServerException.class, () -> {
spyCallableStatement.registerOutParameter(1, java.sql.Types.DECIMAL);
});
assertTrue(thrownException.getMessage().matches(TestUtils.formatErrorMsg("R_InvalidScale")), thrownException.getMessage());
}
}
}
}

@Nested
public class TCGenKeys {
Expand Down Expand Up @@ -3077,5 +3219,4 @@ public void terminate() {
}
}
}

}
Loading