diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java index b8b256a17e..9635918d67 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java @@ -243,6 +243,9 @@ private int storeValues(final Object[] arr, int elementOid, final int[] dims, in case Oid.FLOAT8: arr[i] = ByteConverter.float8(fieldBytes, pos); break; + case Oid.NUMERIC: + arr[i] = ByteConverter.numeric(fieldBytes, pos, len); + break; case Oid.TEXT: case Oid.VARCHAR: Encoding encoding = connection.getEncoding(); @@ -389,6 +392,8 @@ private Class elementOidToClass(int oid) throws SQLException { return Float.class; case Oid.FLOAT8: return Double.class; + case Oid.NUMERIC: + return BigDecimal.class; case Oid.TEXT: case Oid.VARCHAR: return String.class; diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java index 2e7b9f2cad..c25a2d26ae 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java @@ -332,6 +332,7 @@ private static Set getBinaryOids(Properties info) throws PSQLException binaryOids.add(Oid.INT8); binaryOids.add(Oid.FLOAT4); binaryOids.add(Oid.FLOAT8); + binaryOids.add(Oid.NUMERIC); binaryOids.add(Oid.TIME); binaryOids.add(Oid.DATE); binaryOids.add(Oid.TIMETZ); @@ -342,6 +343,7 @@ private static Set getBinaryOids(Properties info) throws PSQLException binaryOids.add(Oid.INT8_ARRAY); binaryOids.add(Oid.FLOAT4_ARRAY); binaryOids.add(Oid.FLOAT8_ARRAY); + binaryOids.add(Oid.NUMERIC_ARRAY); binaryOids.add(Oid.VARCHAR_ARRAY); binaryOids.add(Oid.TEXT_ARRAY); binaryOids.add(Oid.POINT); diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java index a8b76f1da2..4ef826fafd 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java @@ -35,6 +35,7 @@ import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.math.BigInteger; +import java.math.RoundingMode; import java.net.InetAddress; import java.net.UnknownHostException; import java.sql.Array; @@ -2343,6 +2344,13 @@ private Number getNumeric(int columnIndex, int scale, boolean allowNaN) throws S return res; } return toBigDecimal(trimMoney(String.valueOf(obj)), scale); + } else { + Number num = ByteConverter.numeric(thisRow[columnIndex - 1]); + if (allowNaN && Double.isNaN(num.doubleValue())) { + return Double.NaN; + } + + return num; } } @@ -3014,6 +3022,8 @@ private double readDoubleValue(byte[] bytes, int oid, String targetType) throws return ByteConverter.float4(bytes, 0); case Oid.FLOAT8: return ByteConverter.float8(bytes, 0); + case Oid.NUMERIC: + return ByteConverter.numeric(bytes).doubleValue(); } throw new PSQLException(GT.tr("Cannot convert the column of type {0} to requested type {1}.", Oid.toString(oid), targetType), PSQLState.DATA_TYPE_MISMATCH); @@ -3058,6 +3068,14 @@ private long readLongValue(byte[] bytes, int oid, long minVal, long maxVal, Stri case Oid.FLOAT8: val = (long) ByteConverter.float8(bytes, 0); break; + case Oid.NUMERIC: + Number num = ByteConverter.numeric(bytes); + if (num instanceof BigDecimal) { + val = ((BigDecimal) num).setScale(0 , RoundingMode.DOWN).longValueExact(); + } else { + val = num.longValue(); + } + break; default: throw new PSQLException( GT.tr("Cannot convert the column of type {0} to requested type {1}.", diff --git a/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java b/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java index f118897781..dd7eb737e3 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java +++ b/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java @@ -5,6 +5,9 @@ package org.postgresql.util; +import java.math.BigDecimal; +import java.nio.CharBuffer; + /** * Helper methods to parse java base types from byte arrays. * @@ -12,6 +15,16 @@ */ public class ByteConverter { + private static final int NBASE = 10000; + private static final int NUMERIC_DSCALE_MASK = 0x00003FFF; + private static final short NUMERIC_POS = 0x0000; + private static final short NUMERIC_NEG = 0x4000; + private static final short NUMERIC_NAN = (short) 0xC000; + private static final int DEC_DIGITS = 4; + private static final int[] round_powers = {0, 1000, 100, 10}; + private static final int SHORT_BYTES = 2; + private static final int LONG_BYTES = 4; + private ByteConverter() { // prevent instantiation of static helper class } @@ -25,16 +38,168 @@ public static int bytesToInt(byte []bytes) { if ( bytes.length == 1 ) { return (int)bytes[0]; } - if ( bytes.length == 2 ) { + if ( bytes.length == SHORT_BYTES ) { return int2(bytes, 0); } - if ( bytes.length == 4 ) { + if ( bytes.length == LONG_BYTES ) { return int4(bytes, 0); } else { throw new IllegalArgumentException("Argument bytes is empty"); } } + /** + * Convert a number from binary representation to text representation. + * @param idx index of the digit to be converted in the digits array + * @param digits array of shorts that can be decoded as the number String + * @param buffer the character buffer to put the text representation in + * @param alwaysPutIt a flag that indicate whether or not to put the digit char even if it is zero + * @return String the number as String + */ + private static void digitToString(int idx, short[] digits, CharBuffer buffer, boolean alwaysPutIt) { + short dig = (idx >= 0 && idx < digits.length) ? digits[idx] : 0; + for (int p = 1; p < round_powers.length; p++) { + int pow = round_powers[p]; + short d1 = (short)(dig / pow); + dig -= d1 * pow; + boolean putit = (d1 > 0); + if (putit || alwaysPutIt) { + buffer.put((char)(d1 + '0')); + } + } + + buffer.put((char)(dig + '0')); + } + + /** + * Convert a number from binary representation to text representation. + * @param digits array of shorts that can be decoded as the number String + * @param scale the scale of the number binary representation + * @param weight the weight of the number binary representation + * @param sign the sign of the number + * @return String the number as String + */ + private static String numberBytesToString(short[] digits, int scale, int weight, int sign) { + CharBuffer buffer; + int i; + int d; + + /* + * Allocate space for the result. + * + * i is set to the # of decimal digits before decimal point. dscale is the + * # of decimal digits we will print after decimal point. We may generate + * as many as DEC_DIGITS-1 excess digits at the end, and in addition we + * need room for sign, decimal point, null terminator. + */ + i = (weight + 1) * DEC_DIGITS; + if (i <= 0) { + i = 1; + } + + buffer = CharBuffer.allocate((i + scale + DEC_DIGITS + 2)); + + /* + * Output a dash for negative values + */ + if (sign == NUMERIC_NEG) { + buffer.put('-'); + } + + /* + * Output all digits before the decimal point + */ + if (weight < 0) { + d = weight + 1; + buffer.put('0'); + } else { + for (d = 0; d <= weight; d++) { + /* In the first digit, suppress extra leading decimal zeroes */ + digitToString(d, digits, buffer, d != 0); + } + } + + /* + * If requested, output a decimal point and all the digits that follow it. + * We initially put out a multiple of DEC_DIGITS digits, then truncate if + * needed. + */ + if (scale > 0) { + buffer.put('.'); + for (i = 0; i < scale; d++, i += DEC_DIGITS) { + digitToString(d, digits, buffer, true); + } + } + + /* + * terminate the string and return it + */ + int extra = (i - scale) % DEC_DIGITS; + return new String(buffer.array(), 0, buffer.position() - extra); + } + + /** + * Convert a variable length array of bytes to an integer + * @param bytes array of bytes that can be decoded as an integer + * @return integer + */ + public static Number numeric(byte [] bytes) { + return numeric(bytes, 0, bytes.length); + } + + /** + * Convert a variable length array of bytes to an integer + * @param bytes array of bytes that can be decoded as an integer + * @param pos index of the start position of the bytes array for number + * @param numBytes number of bytes to use, length is already encoded + * in the binary format but this is used for double checking + * @return integer + */ + public static Number numeric(byte [] bytes, int pos, int numBytes) { + if (numBytes < 8) { + throw new IllegalArgumentException("number of bytes should be at-least 8"); + } + + short len = ByteConverter.int2(bytes, pos); + short weight = ByteConverter.int2(bytes, pos + 2); + short sign = ByteConverter.int2(bytes, pos + 4); + short scale = ByteConverter.int2(bytes, pos + 6); + + if (numBytes != (len * SHORT_BYTES + 8)) { + throw new IllegalArgumentException("invalid length of bytes \"numeric\" value"); + } + + if (!(sign == NUMERIC_POS + || sign == NUMERIC_NEG + || sign == NUMERIC_NAN)) { + throw new IllegalArgumentException("invalid sign in \"numeric\" value"); + } + + if (sign == NUMERIC_NAN) { + return Double.NaN; + } + + if ((scale & NUMERIC_DSCALE_MASK) != scale) { + throw new IllegalArgumentException("invalid scale in \"numeric\" value"); + } + + short[] digits = new short[len]; + int idx = pos + 8; + for (int i = 0; i < len; i++) { + short d = ByteConverter.int2(bytes, idx); + idx += 2; + + if (d < 0 || d >= NBASE) { + throw new IllegalArgumentException("invalid digit in \"numeric\" value"); + } + + digits[i] = d; + } + + String numString = numberBytesToString(digits, scale, weight, sign); + return new BigDecimal(numString); + } + /** * Parses a long value from the byte array. * 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 a46abef568..2ac51e7c34 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/PreparedStatementTest.java @@ -51,6 +51,9 @@ @RunWith(Parameterized.class) public class PreparedStatementTest extends BaseTest4 { + private static final int NUMERIC_MAX_PRECISION = 1000; + private static final int NUMERIC_MAX_DISPLAY_SCALE = NUMERIC_MAX_PRECISION; + public PreparedStatementTest(BinaryMode binaryMode) { setBinaryMode(binaryMode); } @@ -513,6 +516,53 @@ public void testDoubleQuestionMark() throws SQLException { st.close(); } + @Test + public void testNumeric() throws SQLException { + PreparedStatement pstmt = con.prepareStatement( + "CREATE TEMP TABLE numeric_tab (max_numeric_positive numeric, min_numeric_positive numeric, max_numeric_negative numeric, min_numeric_negative numeric, null_value numeric)"); + pstmt.executeUpdate(); + pstmt.close(); + + char[] wholeDigits = new char[NUMERIC_MAX_DISPLAY_SCALE]; + for (int i = 0; i < NUMERIC_MAX_DISPLAY_SCALE; i++) { + wholeDigits[i] = '9'; + } + + char[] fractionDigits = new char[NUMERIC_MAX_PRECISION]; + for (int i = 0; i < NUMERIC_MAX_PRECISION; i++) { + fractionDigits[i] = '9'; + } + + String maxValueString = new String(wholeDigits); + String minValueString = new String(fractionDigits); + BigDecimal[] values = new BigDecimal[4]; + values[0] = new BigDecimal(maxValueString); + values[1] = new BigDecimal("-" + maxValueString); + values[2] = new BigDecimal(minValueString); + values[3] = new BigDecimal("-" + minValueString); + + pstmt = con.prepareStatement("insert into numeric_tab values (?,?,?,?,?)"); + for (int i = 1; i < 5 ; i++) { + pstmt.setBigDecimal(i, values[i - 1]); + } + + pstmt.setNull(5, Types.NUMERIC); + pstmt.executeUpdate(); + pstmt.close(); + + pstmt = con.prepareStatement("select * from numeric_tab"); + ResultSet rs = pstmt.executeQuery(); + assertTrue(rs.next()); + for (int i = 1; i < 5 ; i++) { + assertTrue(rs.getBigDecimal(i).compareTo(values[i - 1]) == 0); + } + rs.getDouble(5); + assertTrue(rs.wasNull()); + rs.close(); + pstmt.close(); + + } + @Test public void testDouble() throws SQLException { PreparedStatement pstmt = con.prepareStatement(