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]