postgresql-jdbc.git / summary / log / commit / refs
commit 3a4679e02fe415141ca0b398097ffffee9c970d2
Author: Vladimir Sitnikov <sitnikov.vladimir@gmail.com>
Date: Sat Jun 20 13:46:53 2026 +0000
fix(jdbc): honor scale in ResultSet.getBigDecimal(int, int)
getBigDecimal(int columnIndex, int scale) used -1 internally as a "do not
rescale" sentinel, so an explicit scale of -1 was silently ignored even
though it is a valid BigDecimal scale. It also called BigDecimal.setScale
without a rounding mode, so requesting a smaller scale than the value
carried (e.g. getBigDecimal(col, 2) on 1.1234) threw "Bad value" instead
of rounding.
Replace the -1 sentinel with a nullable Integer (null means "no
rescale") and round with RoundingMode.HALF_EVEN. The binary numeric path
now applies the requested scale too, matching the text path. The public
static toBigDecimal(String, int) stays public and delegates to a private
nullable overload, so the API is unchanged.
Decode the numeric typmod scale as a signed 11-bit value, so numeric
types with a negative scale (e.g. numeric(2,-2), supported since
PostgreSQL 15) are scaled correctly in both the text and binary paths
instead of being masked to a large positive scale.
Co-authored-by: eperez <eperez@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
.../main/java/org/postgresql/jdbc/PgResultSet.java | 34 ++++++++++++++++++----
.../test/jdbc2/PreparedStatementTest.java | 26 +++++++++++++++++
2 files changed, 54 insertions(+), 6 deletions(-)
diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
index 7f96dd96..1deb66f6 100644
--- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
+++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
@@ -51,6 +51,7 @@ import java.io.Reader;
import java.io.StringReader;
import java.math.BigDecimal;
import java.math.BigInteger;
+import java.math.RoundingMode;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
@@ -234,7 +235,7 @@ public class PgResultSet implements ResultSet, PGRefCursorResultSet {
case Types.NUMERIC:
case Types.DECIMAL:
return getNumeric(columnIndex,
- field.getMod() == -1 ? -1 : ((field.getMod() - 4) & 0xffff), true);
+ field.getMod() == -1 ? null : (Integer) decodeNumericScale(field.getMod()), true);
case Types.REAL:
return getFloat(columnIndex);
case Types.FLOAT:
@@ -442,7 +443,7 @@ public class PgResultSet implements ResultSet, PGRefCursorResultSet {
@Override
public @Nullable BigDecimal getBigDecimal(@Positive int columnIndex) throws SQLException {
- return getBigDecimal(columnIndex, -1);
+ return (BigDecimal) getNumeric(columnIndex, null, false);
}
@Override
@@ -2884,7 +2885,7 @@ public class PgResultSet implements ResultSet, PGRefCursorResultSet {
@Pure
private @Nullable Number getNumeric(
- int columnIndex, int scale, boolean allowSpecial) throws SQLException {
+ int columnIndex, @Nullable Integer scale, boolean allowSpecial) throws SQLException {
byte[] value = getRawValue(columnIndex);
if (value == null) {
return null;
@@ -2910,6 +2911,9 @@ public class PgResultSet implements ResultSet, PGRefCursorResultSet {
throw new PSQLException(GT.tr("Bad value for type {0} : {1}", "BigDecimal", val),
PSQLState.NUMERIC_VALUE_OUT_OF_RANGE);
}
+ if (num instanceof BigDecimal) {
+ return scaleBigDecimal((BigDecimal) num, scale);
+ }
return num;
}
@@ -3542,6 +3546,11 @@ public class PgResultSet implements ResultSet, PGRefCursorResultSet {
}
public static @PolyNull BigDecimal toBigDecimal(@PolyNull String s, int scale) throws SQLException {
+ return toBigDecimal(s, (Integer) scale);
+ }
+
+ private static @PolyNull BigDecimal toBigDecimal(@PolyNull String s,
+ @Nullable Integer scale) throws SQLException {
if (s == null) {
return null;
}
@@ -3549,12 +3558,25 @@ public class PgResultSet implements ResultSet, PGRefCursorResultSet {
return scaleBigDecimal(val, scale);
}
- private static BigDecimal scaleBigDecimal(BigDecimal val, int scale) throws PSQLException {
- if (scale == -1) {
+ /**
+ * Extracts the scale of a {@code numeric} column from its type modifier. Since PostgreSQL 15 the
+ * scale is a signed 11-bit value (e.g. {@code numeric(2,-2)}), so it must be sign-extended rather
+ * than masked with {@code 0xffff}.
+ *
+ * @param typmod the column type modifier, which must not be {@code -1}
+ * @return the (possibly negative) scale
+ */
+ private static int decodeNumericScale(int typmod) {
+ return ((((typmod - 4) & 0x7ff) ^ 0x400) - 0x400);
+ }
+
+ private static BigDecimal scaleBigDecimal(BigDecimal val, @Nullable Integer scale)
+ throws PSQLException {
+ if (scale == null) {
return val;
}
try {
- return val.setScale(scale);
+ return val.setScale(scale, RoundingMode.HALF_EVEN);
} catch (ArithmeticException e) {
throw new PSQLException(
GT.tr("Bad value for type {0} : {1}", "BigDecimal", val),
diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
index 82f494c3..7c5a6f57 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java
@@ -1248,6 +1248,8 @@ public class PreparedStatementTest extends BaseTest4 {
assertTrue(rs.wasNull(), "rs.getBigDecimal after rs.getLong");
assertEquals(BigDecimal.valueOf(maxInt).setScale(1, RoundingMode.HALF_EVEN), rs.getBigDecimal(1, 1), "maxInt as rs.getBigDecimal(scale=1)");
assertEquals(BigDecimal.valueOf(minInt).setScale(1, RoundingMode.HALF_EVEN), rs.getBigDecimal(2, 1), "minInt as rs.getBigDecimal(scale=1)");
+ assertEquals(BigDecimal.valueOf(maxInt).setScale(-1, RoundingMode.HALF_EVEN), rs.getBigDecimal(1, -1), "maxInt as rs.getBigDecimal(scale=-1)");
+ assertEquals(BigDecimal.valueOf(minInt).setScale(-1, RoundingMode.HALF_EVEN), rs.getBigDecimal(2, -1), "minInt as rs.getBigDecimal(scale=-1)");
rs.getFloat(3);
assertTrue(rs.wasNull());
rs.close();
@@ -1255,6 +1257,30 @@ public class PreparedStatementTest extends BaseTest4 {
}
+ @Test
+ public void testNegativeNumericScale() throws SQLException {
+ assumeMinimumServerVersion("numeric with a negative scale typmod requires v15",
+ ServerVersion.v15);
+ try (PreparedStatement pstmt = con.prepareStatement("select 1500::numeric(2,-2)");
+ ResultSet rs = pstmt.executeQuery()) {
+ assertTrue(rs.next());
+ BigDecimal expected = new BigDecimal("1500").setScale(-2, RoundingMode.HALF_EVEN);
+ assertEquals(expected, rs.getObject(1), "1500::numeric(2,-2) as rs.getObject");
+ }
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ public void testBigDecimalWithScale() throws SQLException {
+ final String bigDecimalString = "1.1234";
+ try (PreparedStatement pstmt = con.prepareStatement("select " + bigDecimalString);
+ ResultSet rs = pstmt.executeQuery()) {
+ assertTrue(rs.next());
+ assertEquals(new BigDecimal(bigDecimalString).setScale(2, RoundingMode.HALF_EVEN),
+ rs.getBigDecimal(1, 2), "rs.getBigDecimal(scale=2) should round to the requested scale");
+ }
+ }
+
@Test
public void testSetSmallIntFloat() throws SQLException {
PreparedStatement pstmt = con.prepareStatement(
[parent: 81b719dee58e]