diff --git a/build.gradle b/build.gradle
index 3e5d87a9e..f40fa3be1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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'
}
diff --git a/pom.xml b/pom.xml
index 026144f46..57b88810b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -140,6 +140,29 @@
provided
+
+ org.mockito
+ mockito-core
+ 4.11.0
+ test
+
+
+ org.mockito
+ mockito-inline
+ 4.11.0
+ test
+
+
+ net.bytebuddy
+ byte-buddy
+ 1.15.11
+
+
+ net.bytebuddy
+ byte-buddy-agent
+ 1.15.11
+ test
+
org.junit.platform
junit-platform-console
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java
index e6e42ada6..8e40c2fca 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCallableStatement.java
@@ -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;
@@ -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;
}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java
index 959837f0a..4df2efb31 100644
--- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java
@@ -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;
@@ -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;
@@ -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")));
}
}
}
@@ -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 {
@@ -3077,5 +3219,4 @@ public void terminate() {
}
}
}
-
}