Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Binary Support for Oid.NUMERIC and Oid.NUMERIC_ARRAY #1636

Merged
merged 5 commits into from Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -332,6 +332,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 @@ -342,6 +343,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
155 changes: 155 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java
Expand Up @@ -5,13 +5,24 @@

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 ByteConverter() {
// prevent instantiation of static helper class
}
Expand All @@ -35,6 +46,150 @@ public static int bytesToInt(byte []bytes) {
}
}

/**
* 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);
}

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
* @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) {
mahmoudbahaa marked this conversation as resolved.
Show resolved Hide resolved
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