Skip to content

Commit

Permalink
Add Binary Support for Oid.NUMERIC and Oid.NUMERIC_ARRAY (#1636)
Browse files Browse the repository at this point in the history
* Add Binary Support for Oid.NUMERIC and Oid.NUMERIC_ARRAY

* Adding test cases for binary support for Oid.NUMERIC

* Fix Oid.NUMERIC_ARRAY binary format

* Fix java docs for numeric functions in ByteConverter

* remove Short.BYTES as it doesn't exist in jre6 and jre7
  • Loading branch information
mahmoudbahaa authored and davecramer committed Dec 6, 2019
1 parent 08bd46b commit c85b149
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 2 deletions.
5 changes: 5 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
Expand Up @@ -335,6 +335,7 @@ private static Set<Integer> 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);
Expand All @@ -345,6 +346,7 @@ private static Set<Integer> 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);
Expand Down
18 changes: 18 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/jdbc/PgResultSet.java
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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}.",
Expand Down
169 changes: 167 additions & 2 deletions pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java
Expand Up @@ -5,13 +5,26 @@

package org.postgresql.util;

import java.math.BigDecimal;
import java.nio.CharBuffer;

/**
* Helper methods to parse java base types from byte arrays.
*
* @author Mikko Tiihonen
*/
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
}
Expand All @@ -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.
*
Expand Down
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit c85b149

Please sign in to comment.