diff --git a/docs/documentation/head/arrays.md b/docs/documentation/head/arrays.md index b74356a10b..2b5353dac2 100644 --- a/docs/documentation/head/arrays.md +++ b/docs/documentation/head/arrays.md @@ -16,14 +16,15 @@ The [java.sql.Connection.createArrayOf(String, Object\[\])](https://docs.oracle. A similar method `org.postgresql.PGConnection.createArrayOf(String, Object)` provides support for primitive array types. The `java.sql.Array` object returned from these methods can be used in other methods, such as [PreparedStatement.setArray(int, Array)](https://docs.oracle.com/javase/8/docs/api/java/sql/PreparedStatement.html#setArray-int-java.sql.Array-). -Additionally, the following types of arrays can be used in `PreparedStatement.setObject` methods and will use the defined type mapping: +The following types of arrays support binary representation in requests and can be used in `PreparedStatement.setObject`: -Java Type | Default PostgreSQL™ Type ---- | --- -`short[]` | `int2[]` -`int[]` | `int4[]` -`long[]` | `int8[]` -`float[]` | `float4[]` -`double[]` | `float8[]` -`boolean[]` | `bool[]` -`String[]` | `varchar[]` +Java Type | Supported binary PostgreSQL™ Types | Default PostgreSQL™ Type +--- | --- | --- +`short[]`, `Short[]` | `int2[]` | `int2[]` +`int[]`, `Integer[]` | `int4[]` | `int4[]` +`long[]`, `Long[]` | `int8[]` | `int8[]` +`float[]`, `Float[]` | `float4[]` | `float4[]` +`double[]`, `Double[]` | `float8[]` | `float8[]` +`boolean[]`, `Boolean[]` | `bool[]` | `bool[]` +`String[]` | `varchar[]`, `text[]` | `varchar[]` +`byte[][]` | `bytea[]` | `bytea[]` \ No newline at end of file diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/ArrayDecoding.java b/pgjdbc/src/main/java/org/postgresql/jdbc/ArrayDecoding.java new file mode 100644 index 0000000000..60cba225c3 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/ArrayDecoding.java @@ -0,0 +1,791 @@ +/* + * Copyright (c) 2020, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.BaseConnection; +import org.postgresql.core.Oid; +import org.postgresql.jdbc2.ArrayAssistant; +import org.postgresql.jdbc2.ArrayAssistantRegistry; +import org.postgresql.util.GT; +import org.postgresql.util.PGbytea; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; + +import org.checkerframework.checker.index.qual.NonNegative; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Utility for decoding arrays. + * + *

+ * See {@code ArrayEncoding} for description of the binary format of arrays. + *

+ * + * @author Brett Okken + */ +final class ArrayDecoding { + + /** + * Array list implementation specific for storing PG array elements. If + * {@link PgArrayList#dimensionsCount} is {@code 1}, the contents will be + * {@link String}. For all larger dimensionsCount, the values will be + * {@link PgArrayList} instances. + */ + static final class PgArrayList extends ArrayList<@Nullable Object> { + + private static final long serialVersionUID = 1L; + + /** + * How many dimensions. + */ + int dimensionsCount = 1; + + } + + private interface ArrayDecoder { + + A createArray(@NonNegative int size); + + Object[] createMultiDimensionalArray(@NonNegative int[] sizes); + + boolean supportBinary(); + + void populateFromBinary(A array, @NonNegative int index, @NonNegative int count, ByteBuffer bytes, BaseConnection connection) + throws SQLException; + + void populateFromString(A array, List<@Nullable String> strings, BaseConnection connection) throws SQLException; + } + + private abstract static class AbstractObjectStringArrayDecoder implements ArrayDecoder { + final Class baseClazz; + + AbstractObjectStringArrayDecoder(Class baseClazz) { + this.baseClazz = baseClazz; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportBinary() { + return false; + } + + @SuppressWarnings("unchecked") + @Override + public A createArray(int size) { + return (A) Array.newInstance(baseClazz, size); + } + + /** + * {@inheritDoc} + */ + @Override + public Object[] createMultiDimensionalArray(int[] sizes) { + return (Object[]) Array.newInstance(baseClazz, sizes); + } + + @Override + public void populateFromBinary(A arr, int index, int count, ByteBuffer bytes, BaseConnection connection) + throws SQLException { + throw new SQLFeatureNotSupportedException(); + } + + /** + * {@inheritDoc} + */ + @Override + public void populateFromString(A arr, List<@Nullable String> strings, BaseConnection connection) throws SQLException { + final @Nullable Object[] array = (Object[]) arr; + + for (int i = 0, j = strings.size(); i < j; ++i) { + final String stringVal = strings.get(i); + array[i] = stringVal != null ? parseValue(stringVal, connection) : null; + } + } + + abstract Object parseValue(String stringVal, BaseConnection connection) throws SQLException; + } + + private abstract static class AbstractObjectArrayDecoder extends AbstractObjectStringArrayDecoder { + + AbstractObjectArrayDecoder(Class baseClazz) { + super(baseClazz); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportBinary() { + return true; + } + + @Override + public void populateFromBinary(A arr, @NonNegative int index, @NonNegative int count, ByteBuffer bytes, BaseConnection connection) + throws SQLException { + final @Nullable Object[] array = (Object[]) arr; + + // skip through to the requested index + for (int i = 0; i < index; ++i) { + final int length = bytes.getInt(); + if (length > 0) { + bytes.position(bytes.position() + length); + } + } + + for (int i = 0; i < count; ++i) { + final int length = bytes.getInt(); + if (length != -1) { + array[i] = parseValue(length, bytes, connection); + } else { + // explicitly set to null for reader's clarity + array[i] = null; + } + } + } + + abstract Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) throws SQLException; + } + + private static final ArrayDecoder LONG_OBJ_ARRAY = new AbstractObjectArrayDecoder(Long.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) { + return bytes.getLong(); + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return PgResultSet.toLong(stringVal); + } + }; + + private static final ArrayDecoder INT4_UNSIGNED_OBJ_ARRAY = new AbstractObjectArrayDecoder( + Long.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) { + final long value = bytes.getInt() & 0xFFFFFFFFL; + return value; + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return PgResultSet.toLong(stringVal); + } + }; + + private static final ArrayDecoder INTEGER_OBJ_ARRAY = new AbstractObjectArrayDecoder( + Integer.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) { + return bytes.getInt(); + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return PgResultSet.toInt(stringVal); + } + }; + + private static final ArrayDecoder SHORT_OBJ_ARRAY = new AbstractObjectArrayDecoder(Short.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) { + return bytes.getShort(); + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return PgResultSet.toShort(stringVal); + } + }; + + private static final ArrayDecoder DOUBLE_OBJ_ARRAY = new AbstractObjectArrayDecoder( + Double.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) { + return bytes.getDouble(); + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return PgResultSet.toDouble(stringVal); + } + }; + + private static final ArrayDecoder FLOAT_OBJ_ARRAY = new AbstractObjectArrayDecoder(Float.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) { + return bytes.getFloat(); + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return PgResultSet.toFloat(stringVal); + } + }; + + private static final ArrayDecoder BOOLEAN_OBJ_ARRAY = new AbstractObjectArrayDecoder( + Boolean.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) { + return bytes.get() == 1; + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return BooleanTypeUtil.fromString(stringVal); + } + }; + + private static final ArrayDecoder STRING_ARRAY = new AbstractObjectArrayDecoder(String.class) { + + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) throws SQLException { + assert bytes.hasArray(); + final byte[] byteArray = bytes.array(); + final int offset = bytes.arrayOffset() + bytes.position(); + + String val; + try { + val = connection.getEncoding().decode(byteArray, offset, length); + } catch (IOException e) { + throw new PSQLException(GT.tr( + "Invalid character data was found. This is most likely caused by stored data containing characters that are invalid for the character set the database was created in. The most common example of this is storing 8bit data in a SQL_ASCII database."), + PSQLState.DATA_ERROR, e); + } + bytes.position(bytes.position() + length); + return val; + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return stringVal; + } + }; + + private static final ArrayDecoder BYTE_ARRAY_ARRAY = new AbstractObjectArrayDecoder( + byte[].class) { + + /** + * {@inheritDoc} + */ + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) throws SQLException { + final byte[] array = new byte[length]; + bytes.get(array); + return array; + } + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + try { + return PGbytea.toBytes(stringVal.getBytes("ascii")); + } catch (UnsupportedEncodingException e) { + throw new java.lang.Error("ascii must be supported"); + } + } + }; + + private static final ArrayDecoder BIG_DECIMAL_STRING_DECODER = new AbstractObjectStringArrayDecoder( + BigDecimal.class) { + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return PgResultSet.toBigDecimal(stringVal); + } + }; + + private static final ArrayDecoder STRING_ONLY_DECODER = new AbstractObjectStringArrayDecoder( + String.class) { + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return stringVal; + } + }; + + private static final ArrayDecoder DATE_DECODER = new AbstractObjectStringArrayDecoder( + java.sql.Date.class) { + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return connection.getTimestampUtils().toDate(null, stringVal); + } + }; + + private static final ArrayDecoder TIME_DECODER = new AbstractObjectStringArrayDecoder( + java.sql.Time.class) { + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return connection.getTimestampUtils().toTime(null, stringVal); + } + }; + + private static final ArrayDecoder TIMESTAMP_DECODER = new AbstractObjectStringArrayDecoder( + java.sql.Timestamp.class) { + + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return connection.getTimestampUtils().toTimestamp(null, stringVal); + } + }; + + /** + * Maps from base type oid to {@link ArrayDecoder} capable of processing + * entries. + */ + @SuppressWarnings("rawtypes") + private static final Map OID_TO_DECODER = new HashMap( + (int) (21 / .75) + 1); + + static { + OID_TO_DECODER.put(Oid.OID, INT4_UNSIGNED_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.INT8, LONG_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.INT4, INTEGER_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.INT2, SHORT_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.MONEY, DOUBLE_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.FLOAT8, DOUBLE_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.FLOAT4, FLOAT_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.TEXT, STRING_ARRAY); + OID_TO_DECODER.put(Oid.VARCHAR, STRING_ARRAY); + OID_TO_DECODER.put(Oid.BIT, BOOLEAN_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.BOOL, BOOLEAN_OBJ_ARRAY); + OID_TO_DECODER.put(Oid.BYTEA, BYTE_ARRAY_ARRAY); + OID_TO_DECODER.put(Oid.NUMERIC, BIG_DECIMAL_STRING_DECODER); + OID_TO_DECODER.put(Oid.BPCHAR, STRING_ONLY_DECODER); + OID_TO_DECODER.put(Oid.CHAR, STRING_ONLY_DECODER); + OID_TO_DECODER.put(Oid.JSON, STRING_ONLY_DECODER); + OID_TO_DECODER.put(Oid.DATE, DATE_DECODER); + OID_TO_DECODER.put(Oid.TIME, TIME_DECODER); + OID_TO_DECODER.put(Oid.TIMETZ, TIME_DECODER); + OID_TO_DECODER.put(Oid.TIMESTAMP, TIMESTAMP_DECODER); + OID_TO_DECODER.put(Oid.TIMESTAMPTZ, TIMESTAMP_DECODER); + } + + @SuppressWarnings("rawtypes") + private static final class ArrayAssistantObjectArrayDecoder extends AbstractObjectArrayDecoder { + private final ArrayAssistant arrayAssistant; + + @SuppressWarnings("unchecked") + ArrayAssistantObjectArrayDecoder(ArrayAssistant arrayAssistant) { + super(arrayAssistant.baseType()); + this.arrayAssistant = arrayAssistant; + } + + /** + * {@inheritDoc} + */ + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) throws SQLException { + + assert bytes.hasArray(); + final byte[] byteArray = bytes.array(); + final int offset = bytes.arrayOffset() + bytes.position(); + + final Object val = arrayAssistant.buildElement(byteArray, offset, length); + + bytes.position(bytes.position() + length); + return val; + } + + /** + * {@inheritDoc} + */ + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return arrayAssistant.buildElement(stringVal); + } + } + + private static final class MappedTypeObjectArrayDecoder extends AbstractObjectArrayDecoder { + + private final String typeName; + + MappedTypeObjectArrayDecoder(String baseTypeName) { + super(Object.class); + this.typeName = baseTypeName; + } + + /** + * {@inheritDoc} + */ + @Override + Object parseValue(int length, ByteBuffer bytes, BaseConnection connection) throws SQLException { + final byte[] copy = new byte[length]; + bytes.get(copy); + return connection.getObject(typeName, null, copy); + } + + /** + * {@inheritDoc} + */ + @Override + Object parseValue(String stringVal, BaseConnection connection) throws SQLException { + return connection.getObject(typeName, stringVal, null); + } + } + + @SuppressWarnings("unchecked") + private static ArrayDecoder getDecoder(int oid, BaseConnection connection) throws SQLException { + final Integer key = oid; + @SuppressWarnings("rawtypes") + final ArrayDecoder decoder = OID_TO_DECODER.get(key); + if (decoder != null) { + return decoder; + } + + final ArrayAssistant assistant = ArrayAssistantRegistry.getAssistant(oid); + + if (assistant != null) { + return new ArrayAssistantObjectArrayDecoder(assistant); + } + + final String typeName = connection.getTypeInfo().getPGType(oid); + if (typeName == null) { + throw org.postgresql.Driver.notImplemented(PgArray.class, "readArray(data,oid)"); + } + + return (ArrayDecoder) new MappedTypeObjectArrayDecoder(typeName); + } + + /** + * Reads binary representation of array into object model. + * + * @param index + * 1 based index of where to start on outermost array. + * @param count + * The number of items to return from outermost array (beginning at + * index). + * @param bytes + * The binary representation of the array. + * @param connection + * The connection the bytes were retrieved from. + * @return The parsed array. + * @throws SQLException + * For failures encountered during parsing. + */ + @SuppressWarnings("unchecked") + public static Object readBinaryArray(int index, int count, byte[] bytes, BaseConnection connection) + throws SQLException { + final ByteBuffer buffer = ByteBuffer.wrap(bytes); + buffer.order(ByteOrder.BIG_ENDIAN); + final int dimensions = buffer.getInt(); + @SuppressWarnings("unused") + final boolean hasNulls = buffer.getInt() != 0; + final int elementOid = buffer.getInt(); + + @SuppressWarnings("rawtypes") + final ArrayDecoder decoder = getDecoder(elementOid, connection); + + if (!decoder.supportBinary()) { + throw org.postgresql.Driver.notImplemented(PgArray.class, "readBinaryArray(data,oid)"); + } + + if (dimensions == 0) { + return decoder.createArray(0); + } + + final int adjustedSkipIndex = index > 0 ? index - 1 : 0; + + // optimize for single dimension array + if (dimensions == 1) { + int length = buffer.getInt(); + buffer.position(buffer.position() + 4); + if (count > 0) { + length = Math.min(length, count); + } + final Object array = decoder.createArray(length); + decoder.populateFromBinary(array, adjustedSkipIndex, length, buffer, connection); + return array; + } + + final int[] dimensionLengths = new int[dimensions]; + for (int i = 0; i < dimensions; ++i) { + dimensionLengths[i] = buffer.getInt(); + buffer.position(buffer.position() + 4); + } + + if (count > 0) { + dimensionLengths[0] = Math.min(count, dimensionLengths[0]); + } + + final Object[] array = decoder.createMultiDimensionalArray(dimensionLengths); + + // TODO: in certain circumstances (no nulls, fixed size data types) + // if adjustedSkipIndex is > 0, we could advance through the buffer rather than + // parse our way through throwing away the results + + storeValues(array, decoder, buffer, adjustedSkipIndex, dimensionLengths, 0, connection); + + return array; + } + + @SuppressWarnings("unchecked") + private static void storeValues(A[] array, ArrayDecoder decoder, ByteBuffer bytes, + int skip, int[] dimensionLengths, int dim, BaseConnection connection) throws SQLException { + assert dim <= dimensionLengths.length - 2; + + for (int i = 0; i < skip; ++i) { + if (dim == dimensionLengths.length - 2) { + decoder.populateFromBinary(array[0], 0, dimensionLengths[dim + 1], bytes, connection); + } else { + storeValues((@NonNull A @NonNull[]) array[0], decoder, bytes, 0, dimensionLengths, dim + 1, connection); + } + } + + for (int i = 0; i < dimensionLengths[dim]; ++i) { + if (dim == dimensionLengths.length - 2) { + decoder.populateFromBinary(array[i], 0, dimensionLengths[dim + 1], bytes, connection); + } else { + storeValues((@NonNull A @NonNull[]) array[i], decoder, bytes, 0, dimensionLengths, dim + 1, connection); + } + } + } + + /** + * Parses the string representation of an array into a {@link PgArrayList}. + * + * @param fieldString + * The array value to parse. + * @param delim + * The delimiter character appropriate for the data type. + * @return A {@link PgArrayList} representing the parsed fieldString. + */ + static PgArrayList buildArrayList(String fieldString, char delim) { + + final PgArrayList arrayList = new PgArrayList(); + + if (fieldString == null) { + return arrayList; + } + + final char[] chars = fieldString.toCharArray(); + StringBuilder buffer = null; + boolean insideString = false; + + // needed for checking if NULL value occurred + boolean wasInsideString = false; + + // array dimension arrays + final List dims = new ArrayList(); + + // currently processed array + PgArrayList curArray = arrayList; + + // Starting with 8.0 non-standard (beginning index + // isn't 1) bounds the dimensions are returned in the + // data formatted like so "[0:3]={0,1,2,3,4}". + // Older versions simply do not return the bounds. + // + // Right now we ignore these bounds, but we could + // consider allowing these index values to be used + // even though the JDBC spec says 1 is the first + // index. I'm not sure what a client would like + // to see, so we just retain the old behavior. + int startOffset = 0; + { + if (chars[0] == '[') { + while (chars[startOffset] != '=') { + startOffset++; + } + startOffset++; // skip = + } + } + + for (int i = startOffset; i < chars.length; i++) { + + // escape character that we need to skip + if (chars[i] == '\\') { + i++; + } else if (!insideString && chars[i] == '{') { + // subarray start + if (dims.isEmpty()) { + dims.add(arrayList); + } else { + PgArrayList a = new PgArrayList(); + PgArrayList p = dims.get(dims.size() - 1); + p.add(a); + dims.add(a); + } + curArray = dims.get(dims.size() - 1); + + // number of dimensions + { + for (int t = i + 1; t < chars.length; t++) { + if (Character.isWhitespace(chars[t])) { + continue; + } else if (chars[t] == '{') { + curArray.dimensionsCount++; + } else { + break; + } + } + } + + buffer = new StringBuilder(); + continue; + } else if (chars[i] == '"') { + // quoted element + insideString = !insideString; + wasInsideString = true; + continue; + } else if (!insideString && Character.isWhitespace(chars[i])) { + // white space + continue; + } else if ((!insideString && (chars[i] == delim || chars[i] == '}')) || i == chars.length - 1) { + // array end or element end + // when character that is a part of array element + if (chars[i] != '"' && chars[i] != '}' && chars[i] != delim && buffer != null) { + buffer.append(chars[i]); + } + + String b = buffer == null ? null : buffer.toString(); + + // add element to current array + if (b != null && (!b.isEmpty() || wasInsideString)) { + curArray.add(!wasInsideString && b.equals("NULL") ? null : b); + } + + wasInsideString = false; + buffer = new StringBuilder(); + + // when end of an array + if (chars[i] == '}') { + dims.remove(dims.size() - 1); + + // when multi-dimension + if (!dims.isEmpty()) { + curArray = dims.get(dims.size() - 1); + } + + buffer = null; + } + + continue; + } + + if (buffer != null) { + buffer.append(chars[i]); + } + } + + return arrayList; + } + + /** + * Reads {@code String} representation of array into object model. + * + * @param index + * 1 based index of where to start on outermost array. + * @param count + * The number of items to return from outermost array (beginning at + * index). + * @param oid + * The oid of the base type of the array. + * @param list + * The {@code #buildArrayList(String, char) processed} string + * representation of an array. + * @param connection + * The connection the bytes were retrieved from. + * @return The parsed array. + * @throws SQLException + * For failures encountered during parsing. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Object readStringArray(int index, int count, int oid, PgArrayList list, BaseConnection connection) + throws SQLException { + + final ArrayDecoder decoder = getDecoder(oid, connection); + + final int dims = list.dimensionsCount; + + if (dims == 0) { + return decoder.createArray(0); + } + + boolean sublist = false; + + int adjustedSkipIndex = 0; + if (index > 1) { + sublist = true; + adjustedSkipIndex = index - 1; + } + + int adjustedCount = list.size(); + if (count > 0 && count != adjustedCount) { + sublist = true; + adjustedCount = Math.min(adjustedCount, count); + } + + final List adjustedList = sublist ? list.subList(adjustedSkipIndex, adjustedSkipIndex + adjustedCount) : list; + + if (dims == 1) { + int length = adjustedList.size(); + if (count > 0) { + length = Math.min(length, count); + } + final Object array = decoder.createArray(length); + decoder.populateFromString(array, adjustedList, connection); + return array; + } + + // dimensions length array (to be used with + // java.lang.reflect.Array.newInstance(Class, int[])) + final int[] dimensionLengths = new int[dims]; + dimensionLengths[0] = adjustedCount; + { + List tmpList = (List) adjustedList.get(0); + for (int i = 1; i < dims; i++) { + dimensionLengths[i] = tmpList.size(); + if (i != dims - 1) { + tmpList = (List) tmpList.get(0); + } + } + } + + final Object[] array = decoder.createMultiDimensionalArray(dimensionLengths); + + storeStringValues(array, decoder, adjustedList, dimensionLengths, 0, connection); + + return array; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void storeStringValues(A[] array, ArrayDecoder decoder, List list, int @NonNull[] dimensionLengths, + int dim, BaseConnection connection) throws SQLException { + assert dim <= dimensionLengths.length - 2; + + for (int i = 0; i < dimensionLengths[dim]; ++i) { + if (dim == dimensionLengths.length - 2) { + decoder.populateFromString(array[i], (List<@Nullable String>) list.get(i), connection); + } else { + storeStringValues((@NonNull A @NonNull[]) array[i], decoder, (List) list.get(i), dimensionLengths, dim + 1, connection); + } + } + } +} diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/ArrayEncoding.java b/pgjdbc/src/main/java/org/postgresql/jdbc/ArrayEncoding.java new file mode 100644 index 0000000000..9e14d7e91d --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/ArrayEncoding.java @@ -0,0 +1,1403 @@ +/* + * Copyright (c) 2020, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.BaseConnection; +import org.postgresql.core.Encoding; +import org.postgresql.core.Oid; +import org.postgresql.util.ByteConverter; +import org.postgresql.util.GT; +import org.postgresql.util.PSQLException; +import org.postgresql.util.PSQLState; + +import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Array; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.HashMap; +import java.util.Map; + +/** + * Utility for using arrays in requests. + * + *

+ * Binary format: + *

+ *

+ * + * @author Brett Okken + */ +final class ArrayEncoding { + + public interface ArrayEncoder
{ + + /** + * The default array type oid supported by this instance. + * + * @return The default array type oid supported by this instance. + */ + int getDefaultArrayTypeOid(); + + /** + * Creates {@code String} representation of the array. + * + * @param delim + * The character to use to delimit between elements. + * @param array + * The array to represent as a {@code String}. + * @return {@code String} representation of the array. + */ + String toArrayString(char delim, A array); + + /** + * Indicates if an array can be encoded in binary form to array oid. + * + * @param oid + * The array oid to see check for binary support. + * @return Indication of whether + * {@link #toBinaryRepresentation(BaseConnection, Object, int)} is + * supported for oid. + */ + boolean supportBinaryRepresentation(int oid); + + /** + * Creates binary representation of the array. + * + * @param connection + * The connection the binary representation will be used on. Attributes + * from the connection might impact how values are translated to + * binary. + * @param array + * The array to binary encode. Must not be {@code null}, but may + * contain {@code null} elements. + * @param oid + * The array type oid to use. Calls to + * {@link #supportBinaryRepresentation(int)} must have returned + * {@code true}. + * @return The binary representation of array. + * @throws SQLFeatureNotSupportedException + * If {@link #supportBinaryRepresentation(int)} is false for + * oid. + */ + byte[] toBinaryRepresentation(BaseConnection connection, A array, int oid) + throws SQLException, SQLFeatureNotSupportedException; + } + + /** + * Base class to implement {@link ArrayEncoding.ArrayEncoder} and provide + * multi-dimensional support. + * + * @param + * Base array type supported. + */ + private abstract static class AbstractArrayEncoder + implements ArrayEncoder { + + private final int oid; + + final int arrayOid; + + /** + * + * @param oid + * The default/primary base oid type. + * @param arrayOid + * The default/primary array oid type. + */ + AbstractArrayEncoder(int oid, int arrayOid) { + this.oid = oid; + this.arrayOid = arrayOid; + } + + /** + * + * @param arrayOid + * The array oid to get base oid type for. + * @return The base oid type for the given array oid type given to + * {@link #toBinaryRepresentation(BaseConnection, Object, int)}. + */ + int getTypeOID(@SuppressWarnings("unused") int arrayOid) { + return oid; + } + + /** + * By default returns the arrayOid this instance was instantiated with. + */ + @Override + public int getDefaultArrayTypeOid() { + return arrayOid; + } + + /** + * Counts the number of {@code null} elements in array. + * + * @param array + * The array to count {@code null} elements in. + * @return The number of {@code null} elements in array. + */ + int countNulls(A array) { + int nulls = 0; + final int arrayLength = Array.getLength(array); + for (int i = 0; i < arrayLength; ++i) { + if (Array.get(array, i) == null) { + ++nulls; + } + } + return nulls; + } + + /** + * Creates {@code byte[]} of just the raw data (no metadata). + * + * @param connection + * The connection the binary representation will be used on. + * @param array + * The array to create binary representation of. Will not be + * {@code null}, but may contain {@code null} elements. + * @return {@code byte[]} of just the raw data (no metadata). + * @throws SQLFeatureNotSupportedException + * If {@link #supportBinaryRepresentation(int)} is false for + * oid. + */ + abstract byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, A array) + throws SQLException, SQLFeatureNotSupportedException; + + /** + * {@inheritDoc} + */ + @Override + public String toArrayString(char delim, A array) { + final StringBuilder sb = new StringBuilder(1024); + appendArray(sb, delim, array); + return sb.toString(); + } + + /** + * Append {@code String} representation of array to sb. + * + * @param sb + * The {@link StringBuilder} to append to. + * @param delim + * The delimiter between elements. + * @param array + * The array to represent. Will not be {@code null}, but may contain + * {@code null} elements. + */ + abstract void appendArray(StringBuilder sb, char delim, A array); + + /** + * By default returns {@code true} if oid matches the arrayOid + * this instance was instantiated with. + */ + @Override + public boolean supportBinaryRepresentation(int oid) { + return oid == arrayOid; + } + } + + /** + * Base class to provide support for {@code Number} based arrays. + * + * @param + * The base type of array. + */ + private abstract static class NumberArrayEncoder extends AbstractArrayEncoder { + + private final int fieldSize; + + /** + * + * @param fieldSize + * The fixed size to represent each value in binary. + * @param oid + * The base type oid. + * @param arrayOid + * The array type oid. + */ + NumberArrayEncoder(int fieldSize, int oid, int arrayOid) { + super(oid, arrayOid); + this.fieldSize = fieldSize; + } + + /** + * {@inheritDoc} + */ + @Override + final int countNulls(N[] array) { + int count = 0; + for (int i = 0; i < array.length; ++i) { + if (array[i] == null) { + ++count; + } + } + return count; + } + + /** + * {@inheritDoc} + */ + @Override + public final byte[] toBinaryRepresentation(BaseConnection connection, N[] array, int oid) + throws SQLException, SQLFeatureNotSupportedException { + assert oid == this.arrayOid; + + final int nullCount = countNulls(array); + + final byte[] bytes = writeBytes(array, nullCount, 20); + + // 1 dimension + ByteConverter.int4(bytes, 0, 1); + // no null + ByteConverter.int4(bytes, 4, nullCount == 0 ? 0 : 1); + // oid + ByteConverter.int4(bytes, 8, getTypeOID(oid)); + // length + ByteConverter.int4(bytes, 12, array.length); + + return bytes; + } + + /** + * {@inheritDoc} + */ + @Override + final byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, N[] array) + throws SQLException, SQLFeatureNotSupportedException { + + final int nullCount = countNulls(array); + + return writeBytes(array, nullCount, 0); + } + + private byte[] writeBytes(final N[] array, final int nullCount, final int offset) { + final int length = offset + (4 * array.length) + (fieldSize * (array.length - nullCount)); + final byte[] bytes = new byte[length]; + + int idx = offset; + for (int i = 0; i < array.length; ++i) { + if (array[i] == null) { + ByteConverter.int4(bytes, idx, -1); + idx += 4; + } else { + ByteConverter.int4(bytes, idx, fieldSize); + idx += 4; + write(array[i], bytes, idx); + idx += fieldSize; + } + } + + return bytes; + } + + /** + * Write single value (number) to bytes beginning at + * offset. + * + * @param number + * The value to write to bytes. This will never be {@code null}. + * @param bytes + * The {@code byte[]} to write to. + * @param offset + * The offset into bytes to write the number value. + */ + protected abstract void write(N number, byte[] bytes, int offset); + + /** + * {@inheritDoc} + */ + @Override + final void appendArray(StringBuilder sb, char delim, N[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i != 0) { + sb.append(delim); + } + if (array[i] == null) { + sb.append('N').append('U').append('L').append('L'); + } else { + sb.append('"'); + sb.append(array[i].toString()); + sb.append('"'); + } + } + sb.append('}'); + } + } + + /** + * Base support for primitive arrays. + * + * @param + * The primitive array to support. + */ + private abstract static class FixedSizePrimitiveArrayEncoder + extends AbstractArrayEncoder { + + private final int fieldSize; + + FixedSizePrimitiveArrayEncoder(int fieldSize, int oid, int arrayOid) { + super(oid, arrayOid); + this.fieldSize = fieldSize; + } + + /** + * {@inheritDoc} + * + *

+ * Always returns {@code 0}. + *

+ */ + @Override + final int countNulls(A array) { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public final byte[] toBinaryRepresentation(BaseConnection connection, A array, int oid) + throws SQLException, SQLFeatureNotSupportedException { + assert oid == arrayOid; + + final int arrayLength = Array.getLength(array); + final int length = 20 + ((fieldSize + 4) * arrayLength); + final byte[] bytes = new byte[length]; + + // 1 dimension + ByteConverter.int4(bytes, 0, 1); + // no null + ByteConverter.int4(bytes, 4, 0); + // oid + ByteConverter.int4(bytes, 8, getTypeOID(oid)); + // length + ByteConverter.int4(bytes, 12, arrayLength); + + write(array, bytes, 20); + + return bytes; + } + + /** + * {@inheritDoc} + */ + @Override + final byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, A array) + throws SQLException, SQLFeatureNotSupportedException { + final int length = ((fieldSize + 4) * Array.getLength(array)); + final byte[] bytes = new byte[length]; + + write(array, bytes, 0); + return bytes; + } + + /** + * Write the entire contents of array to bytes starting at + * offset without metadata describing type or length. + * + * @param array + * The array to write. + * @param bytes + * The {@code byte[]} to write to. + * @param offset + * The offset into bytes to start writing. + */ + protected abstract void write(A array, byte[] bytes, int offset); + } + + private static final AbstractArrayEncoder LONG_ARRAY = new FixedSizePrimitiveArrayEncoder(8, Oid.INT8, + Oid.INT8_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + public void appendArray(StringBuilder sb, char delim, long[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + sb.append(array[i]); + } + sb.append('}'); + } + + /** + * {@inheritDoc} + */ + @Override + protected void write(long[] array, byte[] bytes, int offset) { + int idx = offset; + for (int i = 0; i < array.length; ++i) { + bytes[idx + 3] = 8; + ByteConverter.int8(bytes, idx + 4, array[i]); + idx += 12; + } + } + }; + + private static final AbstractArrayEncoder LONG_OBJ_ARRAY = new NumberArrayEncoder(8, Oid.INT8, + Oid.INT8_ARRAY) { + + @Override + protected void write(Long number, byte[] bytes, int offset) { + ByteConverter.int8(bytes, offset, number.longValue()); + } + }; + + private static final AbstractArrayEncoder INT_ARRAY = new FixedSizePrimitiveArrayEncoder(4, Oid.INT4, + Oid.INT4_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + public void appendArray(StringBuilder sb, char delim, int[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + sb.append(array[i]); + } + sb.append('}'); + } + + /** + * {@inheritDoc} + */ + @Override + protected void write(int[] array, byte[] bytes, int offset) { + int idx = offset; + for (int i = 0; i < array.length; ++i) { + bytes[idx + 3] = 4; + ByteConverter.int4(bytes, idx + 4, array[i]); + idx += 8; + } + } + }; + + private static final AbstractArrayEncoder INT_OBJ_ARRAY = new NumberArrayEncoder(4, Oid.INT4, + Oid.INT4_ARRAY) { + + @Override + protected void write(Integer number, byte[] bytes, int offset) { + ByteConverter.int4(bytes, offset, number.intValue()); + } + }; + + private static final AbstractArrayEncoder SHORT_ARRAY = new FixedSizePrimitiveArrayEncoder(2, + Oid.INT2, Oid.INT2_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + public void appendArray(StringBuilder sb, char delim, short[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + sb.append(array[i]); + } + sb.append('}'); + } + + /** + * {@inheritDoc} + */ + @Override + protected void write(short[] array, byte[] bytes, int offset) { + int idx = offset; + for (int i = 0; i < array.length; ++i) { + bytes[idx + 3] = 2; + ByteConverter.int2(bytes, idx + 4, array[i]); + idx += 6; + } + } + }; + + private static final AbstractArrayEncoder SHORT_OBJ_ARRAY = new NumberArrayEncoder(2, Oid.INT2, + Oid.INT2_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + protected void write(Short number, byte[] bytes, int offset) { + ByteConverter.int2(bytes, offset, number.shortValue()); + } + }; + + private static final AbstractArrayEncoder DOUBLE_ARRAY = new FixedSizePrimitiveArrayEncoder(8, + Oid.FLOAT8, Oid.FLOAT8_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + public void appendArray(StringBuilder sb, char delim, double[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + // use quotes to account for any issues with scientific notation + sb.append('"'); + sb.append(array[i]); + sb.append('"'); + } + sb.append('}'); + } + + /** + * {@inheritDoc} + */ + @Override + protected void write(double[] array, byte[] bytes, int offset) { + int idx = offset; + for (int i = 0; i < array.length; ++i) { + bytes[idx + 3] = 8; + ByteConverter.float8(bytes, idx + 4, array[i]); + idx += 12; + } + } + }; + + private static final AbstractArrayEncoder DOUBLE_OBJ_ARRAY = new NumberArrayEncoder(8, Oid.FLOAT8, + Oid.FLOAT8_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + protected void write(Double number, byte[] bytes, int offset) { + ByteConverter.float8(bytes, offset, number.doubleValue()); + } + }; + + private static final AbstractArrayEncoder FLOAT_ARRAY = new FixedSizePrimitiveArrayEncoder(4, + Oid.FLOAT4, Oid.FLOAT4_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + public void appendArray(StringBuilder sb, char delim, float[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + // use quotes to account for any issues with scientific notation + sb.append('"'); + sb.append(array[i]); + sb.append('"'); + } + sb.append('}'); + } + + /** + * {@inheritDoc} + */ + @Override + protected void write(float[] array, byte[] bytes, int offset) { + int idx = offset; + for (int i = 0; i < array.length; ++i) { + bytes[idx + 3] = 4; + ByteConverter.float4(bytes, idx + 4, array[i]); + idx += 8; + } + } + }; + + private static final AbstractArrayEncoder FLOAT_OBJ_ARRAY = new NumberArrayEncoder(4, Oid.FLOAT4, + Oid.FLOAT4_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + protected void write(Float number, byte[] bytes, int offset) { + ByteConverter.float4(bytes, offset, number.floatValue()); + } + }; + + private static final AbstractArrayEncoder BOOLEAN_ARRAY = new FixedSizePrimitiveArrayEncoder(1, + Oid.BOOL, Oid.BOOL_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + public void appendArray(StringBuilder sb, char delim, boolean[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + sb.append(array[i] ? '1' : '0'); + } + sb.append('}'); + } + + /** + * {@inheritDoc} + */ + @Override + protected void write(boolean[] array, byte[] bytes, int offset) { + int idx = offset; + for (int i = 0; i < array.length; ++i) { + bytes[idx + 3] = 1; + ByteConverter.bool(bytes, idx + 4, array[i]); + idx += 5; + } + } + }; + + private static final AbstractArrayEncoder BOOLEAN_OBJ_ARRAY = new AbstractArrayEncoder(Oid.BOOL, + Oid.BOOL_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + public byte[] toBinaryRepresentation(BaseConnection connection, Boolean[] array, int oid) + throws SQLException, SQLFeatureNotSupportedException { + assert oid == arrayOid; + + final int nullCount = countNulls(array); + + final byte[] bytes = writeBytes(array, nullCount, 20); + + // 1 dimension + ByteConverter.int4(bytes, 0, 1); + // no null + ByteConverter.int4(bytes, 4, nullCount == 0 ? 0 : 1); + // oid + ByteConverter.int4(bytes, 8, getTypeOID(oid)); + // length + ByteConverter.int4(bytes, 12, array.length); + + return bytes; + } + + private byte[] writeBytes(final Boolean[] array, final int nullCount, final int offset) { + final int length = offset + (4 * array.length) + (array.length - nullCount); + final byte[] bytes = new byte[length]; + + int idx = offset; + for (int i = 0; i < array.length; ++i) { + if (array[i] == null) { + ByteConverter.int4(bytes, idx, -1); + idx += 4; + } else { + ByteConverter.int4(bytes, idx, 1); + idx += 4; + write(array[i], bytes, idx); + ++idx; + } + } + + return bytes; + } + + private void write(Boolean bool, byte[] bytes, int idx) { + ByteConverter.bool(bytes, idx, bool.booleanValue()); + } + + /** + * {@inheritDoc} + */ + @Override + byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, Boolean[] array) + throws SQLException, SQLFeatureNotSupportedException { + final int nullCount = countNulls(array); + return writeBytes(array, nullCount, 0); + } + + /** + * {@inheritDoc} + */ + @Override + void appendArray(StringBuilder sb, char delim, Boolean[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i != 0) { + sb.append(delim); + } + if (array[i] == null) { + sb.append('N').append('U').append('L').append('L'); + } else { + sb.append(array[i].booleanValue() ? '1' : '0'); + } + } + sb.append('}'); + } + }; + + private static final AbstractArrayEncoder STRING_ARRAY = new AbstractArrayEncoder(Oid.VARCHAR, + Oid.VARCHAR_ARRAY) { + + /** + * {@inheritDoc} + */ + @Override + int countNulls(String[] array) { + int count = 0; + for (int i = 0; i < array.length; ++i) { + if (array[i] == null) { + ++count; + } + } + return count; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportBinaryRepresentation(int oid) { + return oid == Oid.VARCHAR_ARRAY || oid == Oid.TEXT_ARRAY; + } + + /** + * {@inheritDoc} + */ + @Override + int getTypeOID(int arrayOid) { + if (arrayOid == Oid.VARCHAR_ARRAY) { + return Oid.VARCHAR; + } + + if (arrayOid == Oid.TEXT_ARRAY) { + return Oid.TEXT; + } + + // this should not be possible based on supportBinaryRepresentation returning + // false for all other types + throw new IllegalStateException("Invalid array oid: " + arrayOid); + } + + /** + * {@inheritDoc} + */ + @Override + public void appendArray(StringBuilder sb, char delim, String[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + if (array[i] == null) { + sb.append('N').append('U').append('L').append('L'); + } else { + PgArray.escapeArrayElement(sb, array[i]); + } + } + sb.append('}'); + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] toBinaryRepresentation(BaseConnection connection, String[] array, int oid) throws SQLException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.min(1024, (array.length * 32) + 20)); + + assert supportBinaryRepresentation(oid); + + final byte[] buffer = new byte[4]; + + try { + // 1 dimension + ByteConverter.int4(buffer, 0, 1); + baos.write(buffer); + // null + ByteConverter.int4(buffer, 0, countNulls(array) > 0 ? 1 : 0); + baos.write(buffer); + // oid + ByteConverter.int4(buffer, 0, getTypeOID(oid)); + baos.write(buffer); + // length + ByteConverter.int4(buffer, 0, array.length); + baos.write(buffer); + + // write 4 empty bytes + java.util.Arrays.fill(buffer, (byte) 0); + baos.write(buffer); + + final Encoding encoding = connection.getEncoding(); + for (int i = 0; i < array.length; ++i) { + final String string = array[i]; + if (string != null) { + final byte[] encoded; + try { + encoded = encoding.encode(string); + } catch (IOException e) { + throw new PSQLException(GT.tr("Unable to translate data into the desired encoding."), + PSQLState.DATA_ERROR, e); + } + ByteConverter.int4(buffer, 0, encoded.length); + baos.write(buffer); + baos.write(encoded); + } else { + ByteConverter.int4(buffer, 0, -1); + baos.write(buffer); + } + } + + return baos.toByteArray(); + } catch (IOException e) { + // this IO exception is from writing to baos, which will never throw an + // IOException + throw new java.lang.AssertionError(e); + } + } + + /** + * {@inheritDoc} + */ + @Override + byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, String[] array) + throws SQLException, SQLFeatureNotSupportedException { + try { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.min(1024, (array.length * 32) + 20)); + final byte[] buffer = new byte[4]; + final Encoding encoding = connection.getEncoding(); + for (int i = 0; i < array.length; ++i) { + final String string = array[i]; + if (string != null) { + final byte[] encoded; + try { + encoded = encoding.encode(string); + } catch (IOException e) { + throw new PSQLException(GT.tr("Unable to translate data into the desired encoding."), + PSQLState.DATA_ERROR, e); + } + ByteConverter.int4(buffer, 0, encoded.length); + baos.write(buffer); + baos.write(encoded); + } else { + ByteConverter.int4(buffer, 0, -1); + baos.write(buffer); + } + } + + return baos.toByteArray(); + } catch (IOException e) { + // this IO exception is from writing to baos, which will never throw an + // IOException + throw new java.lang.AssertionError(e); + } + } + }; + + private static final AbstractArrayEncoder BYTEA_ARRAY = new AbstractArrayEncoder(Oid.BYTEA, + Oid.BYTEA_ARRAY) { + + /** + * The possible characters to use for representing hex binary data. + */ + private final char[] hexDigits = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', + 'e', 'f' }; + + /** + * {@inheritDoc} + */ + @Override + public byte[] toBinaryRepresentation(BaseConnection connection, byte[][] array, int oid) + throws SQLException, SQLFeatureNotSupportedException { + + assert oid == arrayOid; + + int length = 20; + for (int i = 0; i < array.length; ++i) { + length += 4; + if (array[i] != null) { + length += array[i].length; + } + } + final byte[] bytes = new byte[length]; + + // 1 dimension + ByteConverter.int4(bytes, 0, 1); + // no null + ByteConverter.int4(bytes, 4, 0); + // oid + ByteConverter.int4(bytes, 8, getTypeOID(oid)); + // length + ByteConverter.int4(bytes, 12, array.length); + + write(array, bytes, 20); + + return bytes; + } + + /** + * {@inheritDoc} + */ + @Override + byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, byte[][] array) + throws SQLException, SQLFeatureNotSupportedException { + int length = 0; + for (int i = 0; i < array.length; ++i) { + length += 4; + if (array[i] != null) { + length += array[i].length; + } + } + final byte[] bytes = new byte[length]; + + write(array, bytes, 0); + return bytes; + } + + /** + * {@inheritDoc} + */ + @Override + int countNulls(byte[][] array) { + int nulls = 0; + for (int i = 0; i < array.length; ++i) { + if (array[i] == null) { + ++nulls; + } + } + return nulls; + } + + private void write(byte[][] array, byte[] bytes, int offset) { + int idx = offset; + for (int i = 0; i < array.length; ++i) { + if (array[i] != null) { + ByteConverter.int4(bytes, idx, array[i].length); + idx += 4; + System.arraycopy(array[i], 0, bytes, idx, array[i].length); + idx += array[i].length; + } else { + ByteConverter.int4(bytes, idx, -1); + idx += 4; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + void appendArray(StringBuilder sb, char delim, byte[][] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + + if (array[i] != null) { + sb.append("\"\\\\x"); + for (int j = 0; j < array[i].length; ++j) { + byte b = array[i][j]; + + // get the value for the left 4 bits (drop sign) + sb.append(hexDigits[(b & 0xF0) >>> 4]); + // get the value for the right 4 bits + sb.append(hexDigits[b & 0x0F]); + } + sb.append('"'); + } else { + sb.append("NULL"); + } + } + sb.append('}'); + } + }; + + private static final AbstractArrayEncoder OBJECT_ARRAY = new AbstractArrayEncoder(0, 0) { + + @Override + public int getDefaultArrayTypeOid() { + return 0; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportBinaryRepresentation(int oid) { + return false; + } + + @Override + public byte[] toBinaryRepresentation(BaseConnection connection, Object[] array, int oid) + throws SQLException, SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + byte[] toSingleDimensionBinaryRepresentation(BaseConnection connection, Object[] array) + throws SQLException, SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException(); + } + + @Override + void appendArray(StringBuilder sb, char delim, Object[] array) { + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + if (array[i] == null) { + sb.append('N').append('U').append('L').append('L'); + } else { + PgArray.escapeArrayElement(sb, array[i].toString()); + } + } + sb.append('}'); + } + }; + + @SuppressWarnings("rawtypes") + private static final Map<@NonNull Class, @NonNull AbstractArrayEncoder> ARRAY_CLASS_TO_ENCODER = new HashMap<@NonNull Class, @NonNull AbstractArrayEncoder>( + (int) (14 / .75) + 1); + + static { + ARRAY_CLASS_TO_ENCODER.put(long.class, LONG_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(Long.class, LONG_OBJ_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(int.class, INT_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(Integer.class, INT_OBJ_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(short.class, SHORT_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(Short.class, SHORT_OBJ_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(double.class, DOUBLE_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(Double.class, DOUBLE_OBJ_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(float.class, FLOAT_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(Float.class, FLOAT_OBJ_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(boolean.class, BOOLEAN_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(Boolean.class, BOOLEAN_OBJ_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(byte[].class, BYTEA_ARRAY); + ARRAY_CLASS_TO_ENCODER.put(String.class, STRING_ARRAY); + } + + /** + * Returns support for encoding array. + * + * @param array + * The array to encode. Must not be {@code null}. + * @return An instance capable of encoding array as a {@code String} at + * minimum. Some types may support binary encoding. + * @throws PSQLException + * if array is not a supported type. + * @see ArrayEncoding.ArrayEncoder#supportBinaryRepresentation(int) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static ArrayEncoder getArrayEncoder(A array) throws PSQLException { + final Class arrayClazz = array.getClass(); + Class subClazz = arrayClazz.getComponentType(); + if (subClazz == null) { + throw new PSQLException(GT.tr("Invalid elements {0}", array), PSQLState.INVALID_PARAMETER_TYPE); + } + AbstractArrayEncoder support = ARRAY_CLASS_TO_ENCODER.get(subClazz); + if (support != null) { + return support; + } + Class subSubClazz = subClazz.getComponentType(); + if (subSubClazz == null) { + if (Object.class.isAssignableFrom(subClazz)) { + return (ArrayEncoder) OBJECT_ARRAY; + } + throw new PSQLException(GT.tr("Invalid elements {0}", array), PSQLState.INVALID_PARAMETER_TYPE); + } + + subClazz = subSubClazz; + int dimensions = 2; + while (subClazz != null) { + support = ARRAY_CLASS_TO_ENCODER.get(subClazz); + if (support != null) { + if (dimensions == 2) { + return new TwoDimensionPrimitiveArrayEncoder(support); + } + return new RecursiveArrayEncoder(support, dimensions); + } + subSubClazz = subClazz.getComponentType(); + if (subSubClazz == null) { + if (Object.class.isAssignableFrom(subClazz)) { + if (dimensions == 2) { + return new TwoDimensionPrimitiveArrayEncoder(OBJECT_ARRAY); + } + return new RecursiveArrayEncoder(OBJECT_ARRAY, dimensions); + } + } + ++dimensions; + subClazz = subSubClazz; + } + + throw new PSQLException(GT.tr("Invalid elements {0}", array), PSQLState.INVALID_PARAMETER_TYPE); + } + + /** + * Wraps an {@link AbstractArrayEncoder} implementation and provides optimized + * support for 2 dimensions. + */ + private static final class TwoDimensionPrimitiveArrayEncoder implements ArrayEncoder { + private final AbstractArrayEncoder support; + + /** + * @param support + * The instance providing support for the base array type. + */ + TwoDimensionPrimitiveArrayEncoder(AbstractArrayEncoder support) { + super(); + this.support = support; + } + + /** + * {@inheritDoc} + */ + @Override + public int getDefaultArrayTypeOid() { + return support.getDefaultArrayTypeOid(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toArrayString(char delim, A @NonNull[] array) { + final StringBuilder sb = new StringBuilder(1024); + sb.append('{'); + for (int i = 0; i < array.length; ++i) { + if (i > 0) { + sb.append(delim); + } + support.appendArray(sb, delim, array[i]); + } + sb.append('}'); + return sb.toString(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportBinaryRepresentation(int oid) { + return support.supportBinaryRepresentation(oid); + } + + /** + * {@inheritDoc} 4 bytes - dimension 4 bytes - oid 4 bytes - ? 8*d bytes - + * dimension length + */ + @Override + public byte[] toBinaryRepresentation(BaseConnection connection, A[] array, int oid) + throws SQLException, SQLFeatureNotSupportedException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.min(1024, (array.length * 32) + 20)); + final byte[] buffer = new byte[4]; + + boolean hasNulls = false; + for (int i = 0; !hasNulls && i < array.length; ++i) { + if (support.countNulls(array[i]) > 0) { + hasNulls = true; + } + } + + try { + // 2 dimension + ByteConverter.int4(buffer, 0, 2); + baos.write(buffer); + // nulls + ByteConverter.int4(buffer, 0, hasNulls ? 1 : 0); + baos.write(buffer); + // oid + ByteConverter.int4(buffer, 0, support.getTypeOID(oid)); + baos.write(buffer); + + // length + ByteConverter.int4(buffer, 0, array.length); + baos.write(buffer); + // write 4 empty bytes + java.util.Arrays.fill(buffer, (byte) 0); + baos.write(buffer); + + ByteConverter.int4(buffer, 0, array.length > 0 ? Array.getLength(array[0]) : 0); + baos.write(buffer); + // write 4 empty bytes + java.util.Arrays.fill(buffer, (byte) 0); + baos.write(buffer); + + for (int i = 0; i < array.length; ++i) { + baos.write(support.toSingleDimensionBinaryRepresentation(connection, array[i])); + } + + return baos.toByteArray(); + + } catch (IOException e) { + // this IO exception is from writing to baos, which will never throw an + // IOException + throw new java.lang.AssertionError(e); + } + } + } + + /** + * Wraps an {@link AbstractArrayEncoder} implementation and provides support for + * 2 or more dimensions using recursion. + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static final class RecursiveArrayEncoder implements ArrayEncoder { + + private final AbstractArrayEncoder support; + private final @Positive int dimensions; + + /** + * @param support + * The instance providing support for the base array type. + */ + RecursiveArrayEncoder(AbstractArrayEncoder support, @Positive int dimensions) { + super(); + this.support = support; + this.dimensions = dimensions; + assert dimensions >= 2; + } + + /** + * {@inheritDoc} + */ + @Override + public int getDefaultArrayTypeOid() { + return support.getDefaultArrayTypeOid(); + } + + /** + * {@inheritDoc} + */ + @Override + public String toArrayString(char delim, Object array) { + final StringBuilder sb = new StringBuilder(2048); + arrayString(sb, array, delim, dimensions); + return sb.toString(); + } + + private void arrayString(StringBuilder sb, Object array, char delim, int depth) { + + if (depth > 1) { + sb.append('{'); + for (int i = 0, j = Array.getLength(array); i < j; ++i) { + if (i > 0) { + sb.append(delim); + } + arrayString(sb, Array.get(array, i), delim, depth - 1); + } + sb.append('}'); + } else { + support.appendArray(sb, delim, array); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean supportBinaryRepresentation(int oid) { + return support.supportBinaryRepresentation(oid); + } + + private boolean hasNulls(Object array, int depth) { + if (depth > 1) { + for (int i = 0, j = Array.getLength(array); i < j; ++i) { + if (hasNulls(Array.get(array, i), depth - 1)) { + return true; + } + } + return false; + } + + return support.countNulls(array) > 0; + } + + /** + * {@inheritDoc} + */ + @Override + public byte[] toBinaryRepresentation(BaseConnection connection, Object array, int oid) + throws SQLException, SQLFeatureNotSupportedException { + + final boolean hasNulls = hasNulls(array, dimensions); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(1024 * dimensions); + final byte[] buffer = new byte[4]; + + try { + // dimensions + ByteConverter.int4(buffer, 0, dimensions); + baos.write(buffer); + // nulls + ByteConverter.int4(buffer, 0, hasNulls ? 1 : 0); + baos.write(buffer); + // oid + ByteConverter.int4(buffer, 0, support.getTypeOID(oid)); + baos.write(buffer); + + // length + ByteConverter.int4(buffer, 0, Array.getLength(array)); + baos.write(buffer); + // write 4 empty bytes for lower bounds value. this is + java.util.Arrays.fill(buffer, (byte) 0); + baos.write(buffer); + + writeArray(connection, buffer, baos, array, dimensions, true); + + return baos.toByteArray(); + + } catch (IOException e) { + // this IO exception is from writing to baos, which will never throw an + // IOException + throw new java.lang.AssertionError(e); + } + } + + private void writeArray(BaseConnection connection, byte[] buffer, ByteArrayOutputStream baos, + Object array, int depth, boolean first) throws IOException, SQLException { + final int length = Array.getLength(array); + + if (first) { + ByteConverter.int4(buffer, 0, length > 0 ? Array.getLength(Array.get(array, 0)) : 0); + baos.write(buffer); + // write 4 empty bytes + java.util.Arrays.fill(buffer, (byte) 0); + baos.write(buffer); + } + + for (int i = 0; i < length; ++i) { + final Object subArray = Array.get(array, i); + if (depth > 2) { + writeArray(connection, buffer, baos, subArray, depth - 1, i == 0); + } else { + baos.write(support.toSingleDimensionBinaryRepresentation(connection, subArray)); + } + } + } + + } +} diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java b/pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java index 84080db22e..b7b2225155 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/BooleanTypeUtil.java @@ -51,7 +51,7 @@ static boolean castToBoolean(final Object in) throws PSQLException { throw new PSQLException("Cannot cast to boolean", PSQLState.CANNOT_COERCE); } - private static boolean fromString(final String strval) throws PSQLException { + static boolean fromString(final String strval) throws PSQLException { // Leading or trailing whitespace is ignored, and case does not matter. final String val = strval.trim(); if ("1".equals(val) || "true".equalsIgnoreCase(val) diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java index 97c0731eac..15b011e8a3 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java @@ -9,11 +9,10 @@ import org.postgresql.core.BaseConnection; import org.postgresql.core.BaseStatement; -import org.postgresql.core.Encoding; import org.postgresql.core.Field; import org.postgresql.core.Oid; import org.postgresql.core.Tuple; -import org.postgresql.jdbc2.ArrayAssistant; +import org.postgresql.jdbc.ArrayDecoding.PgArrayList; import org.postgresql.jdbc2.ArrayAssistantRegistry; import org.postgresql.util.ByteConverter; import org.postgresql.util.GT; @@ -22,15 +21,11 @@ import org.checkerframework.checker.nullness.qual.Nullable; -import java.io.IOException; -import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.Types; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.logging.Level; /** *

Array is used collect one column of query result data.

@@ -51,20 +46,6 @@ public class PgArray implements java.sql.Array { ArrayAssistantRegistry.register(Oid.UUID_ARRAY, new UUIDArrayAssistant()); } - /** - * Array list implementation specific for storing PG array elements. - */ - private static class PgArrayList extends ArrayList<@Nullable Object> { - - private static final long serialVersionUID = 2052783752654562677L; - - /** - * How many dimensions. - */ - int dimensionsCount = 1; - - } - /** * A database connection. */ @@ -80,26 +61,17 @@ private static class PgArrayList extends ArrayList<@Nullable Object> { */ protected @Nullable String fieldString; - /** - * Whether Object[] should be used instead primitive arrays. Object[] can contain null elements. - * It should be set to true if - * {@link BaseConnection#haveMinimumCompatibleVersion(String)} returns true for - * argument "8.3". - */ - private final boolean useObjects; - /** * Value of field as {@link PgArrayList}. Will be initialized only once within - * {@link #buildArrayList()}. + * {@link #buildArrayList(String)}. */ - protected @Nullable PgArrayList arrayList; + protected ArrayDecoding.@Nullable PgArrayList arrayList; protected byte @Nullable [] fieldBytes; private PgArray(BaseConnection connection, int oid) throws SQLException { this.connection = connection; this.oid = oid; - this.useObjects = true; } /** @@ -182,14 +154,14 @@ public Object getArray(long index, int count, @Nullable Map> ma return null; } - PgArrayList arrayList = buildArrayList(); + final PgArrayList arrayList = buildArrayList(fieldString); if (count == 0) { count = arrayList.size(); } // array index out of range - if ((--index) + count > arrayList.size()) { + if ((index - 1) + count > arrayList.size()) { throw new PSQLException( GT.tr("The array index is out of range: {0}, number of elements: {1}.", index + count, (long) arrayList.size()), @@ -199,95 +171,8 @@ public Object getArray(long index, int count, @Nullable Map> ma return buildArray(arrayList, (int) index, count); } - private Object readBinaryArray(byte[] fieldBytes, int index, int count) - throws SQLException { - int dimensions = ByteConverter.int4(fieldBytes, 0); - // int flags = ByteConverter.int4(fieldBytes, 4); // bit 0: 0=no-nulls, 1=has-nulls - int elementOid = ByteConverter.int4(fieldBytes, 8); - int pos = 12; - int[] dims = new int[dimensions]; - for (int d = 0; d < dimensions; ++d) { - dims[d] = ByteConverter.int4(fieldBytes, pos); - pos += 4; - /* int lbound = ByteConverter.int4(fieldBytes, pos); */ - pos += 4; - } - if (dimensions == 0) { - return java.lang.reflect.Array.newInstance(elementOidToClass(elementOid), 0); - } - if (count > 0) { - dims[0] = Math.min(count, dims[0]); - } - Object arr = java.lang.reflect.Array.newInstance(elementOidToClass(elementOid), dims); - try { - storeValues(fieldBytes, (Object[]) arr, elementOid, dims, pos, 0, index); - } catch (IOException ioe) { - throw new PSQLException( - GT.tr( - "Invalid character data was found. This is most likely caused by stored data containing characters that are invalid for the character set the database was created in. The most common example of this is storing 8bit data in a SQL_ASCII database."), - PSQLState.DATA_ERROR, ioe); - } - return arr; - } - - private int storeValues(byte[] fieldBytes, - final Object[] arr, int elementOid, final int[] dims, int pos, - final int thisDimension, int index) throws SQLException, IOException { - if (thisDimension == dims.length - 1) { - for (int i = 1; i < index; ++i) { - int len = ByteConverter.int4(fieldBytes, pos); - pos += 4; - if (len != -1) { - pos += len; - } - } - for (int i = 0; i < dims[thisDimension]; ++i) { - int len = ByteConverter.int4(fieldBytes, pos); - pos += 4; - if (len == -1) { - continue; - } - switch (elementOid) { - case Oid.INT2: - arr[i] = ByteConverter.int2(fieldBytes, pos); - break; - case Oid.INT4: - arr[i] = ByteConverter.int4(fieldBytes, pos); - break; - case Oid.INT8: - arr[i] = ByteConverter.int8(fieldBytes, pos); - break; - case Oid.FLOAT4: - arr[i] = ByteConverter.float4(fieldBytes, pos); - break; - 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 = getConnection().getEncoding(); - arr[i] = encoding.decode(fieldBytes, pos, len); - break; - case Oid.BOOL: - arr[i] = ByteConverter.bool(fieldBytes, pos); - break; - default: - ArrayAssistant arrAssistant = ArrayAssistantRegistry.getAssistant(elementOid); - if (arrAssistant != null) { - arr[i] = arrAssistant.buildElement(fieldBytes, pos, len); - } - } - pos += len; - } - } else { - for (int i = 0; i < dims[thisDimension]; ++i) { - pos = storeValues(fieldBytes, (Object[]) arr[i], elementOid, dims, pos, thisDimension + 1, 0); - } - } - return pos; + private Object readBinaryArray(byte[] fieldBytes, int index, int count) throws SQLException { + return ArrayDecoding.readBinaryArray(index, count, fieldBytes, getConnection()); } private ResultSet readBinaryResultSet(byte[] fieldBytes, int index, int count) @@ -403,157 +288,14 @@ private int calcRemainingDataLength(byte[] fieldBytes, return pos; } - private Class elementOidToClass(int oid) throws SQLException { - switch (oid) { - case Oid.INT2: - return Short.class; - case Oid.INT4: - return Integer.class; - case Oid.INT8: - return Long.class; - case Oid.FLOAT4: - return Float.class; - case Oid.FLOAT8: - return Double.class; - case Oid.NUMERIC: - return BigDecimal.class; - case Oid.TEXT: - case Oid.VARCHAR: - return String.class; - case Oid.BOOL: - return Boolean.class; - default: - ArrayAssistant arrElemBuilder = ArrayAssistantRegistry.getAssistant(oid); - if (arrElemBuilder != null) { - return arrElemBuilder.baseType(); - } - - throw org.postgresql.Driver.notImplemented(this.getClass(), "readBinaryArray(data,oid)"); - } - } - /** * Build {@link ArrayList} from field's string input. As a result of this method * {@link #arrayList} is build. Method can be called many times in order to make sure that array * list is ready to use, however {@link #arrayList} will be set only once during first call. */ - private synchronized PgArrayList buildArrayList() throws SQLException { - PgArrayList arrayList = this.arrayList; - if (arrayList != null) { - return arrayList; - } - - this.arrayList = arrayList = new PgArrayList(); - - char delim = getConnection().getTypeInfo().getArrayDelimiter(oid); - - if (fieldString != null) { - - char[] chars = fieldString.toCharArray(); - StringBuilder buffer = null; - boolean insideString = false; - boolean wasInsideString = false; // needed for checking if NULL - // value occurred - List dims = new ArrayList(); // array dimension arrays - PgArrayList curArray = arrayList; // currently processed array - - // Starting with 8.0 non-standard (beginning index - // isn't 1) bounds the dimensions are returned in the - // data formatted like so "[0:3]={0,1,2,3,4}". - // Older versions simply do not return the bounds. - // - // Right now we ignore these bounds, but we could - // consider allowing these index values to be used - // even though the JDBC spec says 1 is the first - // index. I'm not sure what a client would like - // to see, so we just retain the old behavior. - int startOffset = 0; - { - if (chars[0] == '[') { - while (chars[startOffset] != '=') { - startOffset++; - } - startOffset++; // skip = - } - } - - for (int i = startOffset; i < chars.length; i++) { - - // escape character that we need to skip - if (chars[i] == '\\') { - i++; - } else if (!insideString && chars[i] == '{') { - // subarray start - if (dims.isEmpty()) { - dims.add(arrayList); - } else { - PgArrayList a = new PgArrayList(); - PgArrayList p = dims.get(dims.size() - 1); - p.add(a); - dims.add(a); - } - curArray = dims.get(dims.size() - 1); - - // number of dimensions - { - for (int t = i + 1; t < chars.length; t++) { - if (Character.isWhitespace(chars[t])) { - continue; - } else if (chars[t] == '{') { - curArray.dimensionsCount++; - } else { - break; - } - } - } - - buffer = new StringBuilder(); - continue; - } else if (chars[i] == '"') { - // quoted element - insideString = !insideString; - wasInsideString = true; - continue; - } else if (!insideString && Character.isWhitespace(chars[i])) { - // white space - continue; - } else if ((!insideString && (chars[i] == delim || chars[i] == '}')) - || i == chars.length - 1) { - // array end or element end - // when character that is a part of array element - if (chars[i] != '"' && chars[i] != '}' && chars[i] != delim && buffer != null) { - buffer.append(chars[i]); - } - - String b = buffer == null ? null : buffer.toString(); - - // add element to current array - if (b != null && (!b.isEmpty() || wasInsideString)) { - curArray.add(!wasInsideString && b.equals("NULL") ? null : b); - } - - wasInsideString = false; - buffer = new StringBuilder(); - - // when end of an array - if (chars[i] == '}') { - dims.remove(dims.size() - 1); - - // when multi-dimension - if (!dims.isEmpty()) { - curArray = dims.get(dims.size() - 1); - } - - buffer = null; - } - - continue; - } - - if (buffer != null) { - buffer.append(chars[i]); - } - } + private synchronized PgArrayList buildArrayList(String fieldString) throws SQLException { + if (arrayList == null) { + arrayList = ArrayDecoding.buildArrayList(fieldString, getConnection().getTypeInfo().getArrayDelimiter(oid)); } return arrayList; } @@ -563,269 +305,9 @@ private synchronized PgArrayList buildArrayList() throws SQLException { * * @param input list to be converted into array */ - private Object buildArray(PgArrayList input, int index, int count) throws SQLException { - - if (count < 0) { - count = input.size(); - } - - // array to be returned - Object ret = null; - - // how many dimensions - int dims = input.dimensionsCount; - - // dimensions length array (to be used with java.lang.reflect.Array.newInstance(Class, - // int[])) - int[] dimsLength = dims > 1 ? new int[dims] : null; - if (dimsLength != null) { - for (int i = 0; i < dims; i++) { - dimsLength[i] = (i == 0 ? count : 0); - } - } - - // array elements counter - int length = 0; - - // array elements type - final int type = - getConnection().getTypeInfo().getSQLType( - getConnection().getTypeInfo().getPGArrayElement(oid)); - - if (type == Types.BIT) { - boolean[] pa = null; // primitive array - @Nullable Object @Nullable [] oa = null; // objects array - - if (dimsLength != null || useObjects) { - ret = oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array - .newInstance(useObjects ? Boolean.class : boolean.class, dimsLength) - : new Boolean[count]); - } else { - ret = pa = new boolean[count]; - } - - // add elements - for (; count > 0; count--) { - Object o = input.get(index++); - - if (oa != null) { - oa[length++] = o == null ? null - : (dimsLength != null ? buildArray((PgArrayList) o, 0, -1) : BooleanTypeUtil.castToBoolean((String) o)); - } else if (pa != null) { - pa[length++] = o != null && BooleanTypeUtil.castToBoolean(o); - } - } - } else if (type == Types.SMALLINT) { - short[] pa = null; - @Nullable Object @Nullable [] oa = null; - - if (dimsLength != null || useObjects) { - ret = - oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array - .newInstance(useObjects ? Short.class : short.class, dimsLength) - : new Short[count]); - } else { - ret = pa = new short[count]; - } - - for (; count > 0; count--) { - Object o = input.get(index++); - - if (oa != null) { - oa[length++] = o == null ? null - : (dimsLength != null ? buildArray((PgArrayList) o, 0, -1) : PgResultSet.toShort((String) o)); - } else if (pa != null) { - pa[length++] = o == null ? 0 : PgResultSet.toShort((String) o); - } - } - } else if (type == Types.INTEGER) { - int[] pa = null; - @Nullable Object @Nullable [] oa = null; - - if (dimsLength != null || useObjects) { - ret = - oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array - .newInstance(useObjects ? Integer.class : int.class, dimsLength) - : new Integer[count]); - } else { - ret = pa = new int[count]; - } - - for (; count > 0; count--) { - Object o = input.get(index++); - - if (oa != null) { - oa[length++] = o == null ? null - : (dimsLength != null ? buildArray((PgArrayList) o, 0, -1) : PgResultSet.toInt((String) o)); - } else if (pa != null) { - pa[length++] = o == null ? 0 : PgResultSet.toInt((String) o); - } - } - } else if (type == Types.BIGINT) { - long[] pa = null; - @Nullable Object @Nullable [] oa = null; - - if (dimsLength != null || useObjects) { - ret = - oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array - .newInstance(useObjects ? Long.class : long.class, dimsLength) - : new Long[count]); - } else { - ret = pa = new long[count]; - } - - for (; count > 0; count--) { - Object o = input.get(index++); - - if (oa != null) { - oa[length++] = o == null ? null - : (dimsLength != null ? buildArray((PgArrayList) o, 0, -1) : PgResultSet.toLong((String) o)); - } else if (pa != null) { - pa[length++] = o == null ? 0L : PgResultSet.toLong((String) o); - } - } - } else if (type == Types.NUMERIC) { - @Nullable Object[] oa; - ret = oa = - (dimsLength != null ? (Object[]) java.lang.reflect.Array.newInstance(BigDecimal.class, dimsLength) - : new BigDecimal[count]); - - for (; count > 0; count--) { - Object v = input.get(index++); - oa[length++] = dimsLength != null && v != null ? buildArray((PgArrayList) v, 0, -1) - : (v == null ? null : PgResultSet.toBigDecimal((String) v)); - } - } else if (type == Types.REAL) { - float[] pa = null; - @Nullable Object @Nullable [] oa = null; - - if (dimsLength != null || useObjects) { - ret = - oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array - .newInstance(useObjects ? Float.class : float.class, dimsLength) - : new Float[count]); - } else { - ret = pa = new float[count]; - } - - for (; count > 0; count--) { - Object o = input.get(index++); - - if (oa != null) { - oa[length++] = o == null ? null - : (dimsLength != null ? buildArray((PgArrayList) o, 0, -1) : PgResultSet.toFloat((String) o)); - } else if (pa != null) { - pa[length++] = o == null ? 0f : PgResultSet.toFloat((String) o); - } - } - } else if (type == Types.DOUBLE) { - double[] pa = null; - @Nullable Object[] oa = null; - - if (dimsLength != null || useObjects) { - ret = oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array - .newInstance(useObjects ? Double.class : double.class, dimsLength) - : new Double[count]); - } else { - ret = pa = new double[count]; - } - - for (; count > 0; count--) { - Object o = input.get(index++); - - if (oa != null) { - oa[length++] = o == null ? null - : (dimsLength != null ? buildArray((PgArrayList) o, 0, -1) : PgResultSet.toDouble((String) o)); - } else if (pa != null) { - pa[length++] = o == null ? 0d : PgResultSet.toDouble((String) o); - } - } - } else if (type == Types.CHAR || type == Types.VARCHAR || oid == Oid.JSONB_ARRAY) { - @Nullable Object[] oa; - ret = - oa = (dimsLength != null ? (Object[]) java.lang.reflect.Array.newInstance(String.class, dimsLength) - : new String[count]); - - for (; count > 0; count--) { - Object v = input.get(index++); - oa[length++] = dimsLength != null && v != null ? buildArray((PgArrayList) v, 0, -1) : v; - } - } else if (type == Types.DATE) { - @Nullable Object[] oa; - ret = oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array.newInstance(java.sql.Date.class, dimsLength) - : new java.sql.Date[count]); - - for (; count > 0; count--) { - Object v = input.get(index++); - oa[length++] = dimsLength != null && v != null ? buildArray((PgArrayList) v, 0, -1) - : (v == null ? null : getConnection().getTimestampUtils().toDate(null, (String) v)); - } - } else if (type == Types.TIME) { - @Nullable Object[] oa; - ret = oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array.newInstance(java.sql.Time.class, dimsLength) - : new java.sql.Time[count]); - - for (; count > 0; count--) { - Object v = input.get(index++); - oa[length++] = dimsLength != null && v != null ? buildArray((PgArrayList) v, 0, -1) - : (v == null ? null : getConnection().getTimestampUtils().toTime(null, (String) v)); - } - } else if (type == Types.TIMESTAMP) { - @Nullable Object[] oa; - ret = oa = (dimsLength != null - ? (Object[]) java.lang.reflect.Array.newInstance(java.sql.Timestamp.class, dimsLength) - : new java.sql.Timestamp[count]); - - for (; count > 0; count--) { - Object v = input.get(index++); - oa[length++] = dimsLength != null && v != null ? buildArray((PgArrayList) v, 0, -1) - : (v == null ? null : getConnection().getTimestampUtils().toTimestamp(null, (String) v)); - } - } else if (ArrayAssistantRegistry.getAssistant(oid) != null) { - ArrayAssistant arrAssistant = castNonNull(ArrayAssistantRegistry.getAssistant(oid)); - - @Nullable Object[] oa; - ret = oa = (dimsLength != null) - ? (Object[]) java.lang.reflect.Array.newInstance(arrAssistant.baseType(), dimsLength) - : (Object[]) java.lang.reflect.Array.newInstance(arrAssistant.baseType(), count); - - for (; count > 0; count--) { - Object v = input.get(index++); - oa[length++] = (dimsLength != null && v != null) ? buildArray((PgArrayList) v, 0, -1) - : (v == null ? null : arrAssistant.buildElement((String) v)); - } - } else if (dims == 1) { - @Nullable Object[] oa = new Object[count]; - String typeName = getBaseTypeName(); - for (; count > 0; count--) { - Object v = input.get(index++); - if (v instanceof String) { - oa[length++] = getConnection().getObject(typeName, (String) v, null); - } else if (v instanceof byte[]) { - oa[length++] = getConnection().getObject(typeName, null, (byte[]) v); - } else if (v == null) { - oa[length++] = null; - } else { - throw org.postgresql.Driver.notImplemented(this.getClass(), "getArrayImpl(long,int,Map)"); - } - } - ret = oa; - } else { - // other datatypes not currently supported - getConnection().getLogger().log(Level.FINEST, "getArrayImpl(long,int,Map) with {0}", getBaseTypeName()); - - throw org.postgresql.Driver.notImplemented(this.getClass(), "getArrayImpl(long,int,Map)"); - } - - return ret; + private Object buildArray(ArrayDecoding.PgArrayList input, int index, int count) throws SQLException { + final BaseConnection connection = getConnection(); + return ArrayDecoding.readStringArray(index, count, connection.getTypeInfo().getPGArrayElement(oid), input, connection); } public int getBaseType() throws SQLException { @@ -833,7 +315,6 @@ public int getBaseType() throws SQLException { } public String getBaseTypeName() throws SQLException { - buildArrayList(); int elementOID = getConnection().getTypeInfo().getPGArrayElement(oid); return castNonNull(getConnection().getTypeInfo().getPGType(elementOID)); } @@ -877,7 +358,7 @@ public ResultSet getResultSetImpl(long index, int count, @Nullable Map getSupportedBinaryOids() { Oid.TIMETZ, Oid.TIMESTAMP, Oid.TIMESTAMPTZ, + Oid.BYTEA_ARRAY, Oid.INT2_ARRAY, Oid.INT4_ARRAY, Oid.INT8_ARRAY, + Oid.OID_ARRAY, Oid.FLOAT4_ARRAY, Oid.FLOAT8_ARRAY, Oid.VARCHAR_ARRAY, @@ -384,6 +386,7 @@ private static Set getBinaryOids(Properties info) throws PSQLException binaryOids.removeAll(getOidSet(oids)); } binaryOids.retainAll(SUPPORTED_BINARY_OIDS); + return binaryOids; } @@ -1266,33 +1269,6 @@ public PGReplicationConnection getReplicationAPI() { return new PGReplicationConnectionImpl(this); } - private static void appendArray(StringBuilder sb, Object elements, char delim) { - sb.append('{'); - - int nElements = java.lang.reflect.Array.getLength(elements); - for (int i = 0; i < nElements; i++) { - if (i > 0) { - sb.append(delim); - } - - Object o = java.lang.reflect.Array.get(elements, i); - if (o == null) { - sb.append("NULL"); - } else if (o.getClass().isArray()) { - final PrimitiveArraySupport arraySupport = PrimitiveArraySupport.getArraySupport(o); - if (arraySupport != null) { - arraySupport.appendArray(sb, delim, o); - } else { - appendArray(sb, o, delim); - } - } else { - String s = o.toString(); - PgArray.escapeArrayElement(sb, s); - } - } - sb.append('}'); - } - // Parse a "dirty" integer surrounded by non-numeric characters private static int integerPart(String dirtyString) { int start = 0; @@ -1397,6 +1373,7 @@ public Struct createStruct(String typeName, Object[] attributes) throws SQLExcep throw org.postgresql.Driver.notImplemented(this.getClass(), "createStruct(String, Object[])"); } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Override public Array createArrayOf(String typeName, @Nullable Object elements) throws SQLException { checkClosed(); @@ -1415,53 +1392,19 @@ public Array createArrayOf(String typeName, @Nullable Object elements) throws SQ return makeArray(oid, null); } - final String arrayString; - - final PrimitiveArraySupport arraySupport = PrimitiveArraySupport.getArraySupport(elements); - - if (arraySupport != null) { - // if the oid for the given type matches the default type, we might be - // able to go straight to binary representation - if (oid == arraySupport.getDefaultArrayTypeOid(typeInfo) && arraySupport.supportBinaryRepresentation() - && getPreferQueryMode() != PreferQueryMode.SIMPLE) { - return new PgArray(this, oid, arraySupport.toBinaryRepresentation(this, elements)); - } - arrayString = arraySupport.toArrayString(delim, elements); - } else { - final Class clazz = elements.getClass(); - if (!clazz.isArray()) { - throw new PSQLException(GT.tr("Invalid elements {0}", elements), PSQLState.INVALID_PARAMETER_TYPE); - } - StringBuilder sb = new StringBuilder(); - appendArray(sb, elements, delim); - arrayString = sb.toString(); + final ArrayEncoding.ArrayEncoder arraySupport = ArrayEncoding.getArrayEncoder(elements); + if (arraySupport.supportBinaryRepresentation(oid) && getPreferQueryMode() != PreferQueryMode.SIMPLE) { + return new PgArray(this, oid, arraySupport.toBinaryRepresentation(this, elements, oid)); } + final String arrayString = arraySupport.toArrayString(delim, elements); return makeArray(oid, arrayString); } @Override public Array createArrayOf(String typeName, @Nullable Object @Nullable [] elements) throws SQLException { - checkClosed(); - - int oid = getTypeInfo().getPGArrayType(typeName); - - if (oid == Oid.UNSPECIFIED) { - throw new PSQLException( - GT.tr("Unable to find server array type for provided name {0}.", typeName), - PSQLState.INVALID_NAME); - } - - if (elements == null) { - return makeArray(oid, null); - } - - char delim = getTypeInfo().getArrayDelimiter(oid); - StringBuilder sb = new StringBuilder(); - appendArray(sb, elements, delim); - - return makeArray(oid, sb.toString()); + return createArrayOf(typeName, (Object) elements); } @Override diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java index e265e2a0af..e78f700782 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java @@ -33,6 +33,7 @@ import org.checkerframework.checker.index.qual.NonNegative; import org.checkerframework.checker.index.qual.Positive; +import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.common.value.qual.IntRange; @@ -59,6 +60,7 @@ import java.sql.ResultSetMetaData; import java.sql.RowId; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.SQLXML; import java.sql.Time; import java.sql.Timestamp; @@ -70,6 +72,7 @@ import java.util.UUID; class PgPreparedStatement extends PgStatement implements PreparedStatement { + protected final CachedQuery preparedQuery; // Query fragments for prepared statement. protected final ParameterList preparedParameters; // Parameter values for prepared statement. @@ -677,13 +680,14 @@ public void setObject(@Positive int parameterIndex, @Nullable Object in, case Types.ARRAY: if (in instanceof Array) { setArray(parameterIndex, (Array) in); - } else if (PrimitiveArraySupport.isSupportedPrimitiveArray(in)) { - setPrimitiveArray(parameterIndex, in); } else { - throw new PSQLException( - GT.tr("Cannot cast an instance of {0} to type {1}", - in.getClass().getName(), "Types.ARRAY"), - PSQLState.INVALID_PARAMETER_TYPE); + try { + setObjectArray(parameterIndex, in); + } catch (Exception e) { + throw new PSQLException( + GT.tr("Cannot cast an instance of {0} to type {1}", in.getClass().getName(), "Types.ARRAY"), + PSQLState.INVALID_PARAMETER_TYPE, e); + } } break; case Types.DISTINCT: @@ -704,20 +708,24 @@ public void setObject(@Positive int parameterIndex, @Nullable Object in, } } - private
void setPrimitiveArray(@Positive int parameterIndex, A in) throws SQLException { - // TODO: move to method parameter? - final PrimitiveArraySupport arrayToString = - castNonNull(PrimitiveArraySupport.getArraySupport(in)); + private void setObjectArray(int parameterIndex, A in) throws SQLException { + final ArrayEncoding.ArrayEncoder arraySupport = ArrayEncoding.getArrayEncoder(in); final TypeInfo typeInfo = connection.getTypeInfo(); - final int oid = arrayToString.getDefaultArrayTypeOid(typeInfo); + final int oid = arraySupport.getDefaultArrayTypeOid(); - if (arrayToString.supportBinaryRepresentation() && connection.getPreferQueryMode() != PreferQueryMode.SIMPLE) { - bindBytes(parameterIndex, arrayToString.toBinaryRepresentation(connection, in), oid); + if (arraySupport.supportBinaryRepresentation(oid) && connection.getPreferQueryMode() != PreferQueryMode.SIMPLE) { + bindBytes(parameterIndex, arraySupport.toBinaryRepresentation(connection, in, oid), oid); } else { - final char delim = typeInfo.getArrayDelimiter(oid); - setString(parameterIndex, arrayToString.toArrayString(delim, in), oid); + if (oid == Oid.UNSPECIFIED) { + throw new SQLFeatureNotSupportedException(); + } + final int baseOid = typeInfo.getPGArrayElement(oid); + final String baseType = castNonNull(typeInfo.getPGType(baseOid)); + + final Array array = getPGConnection().createArrayOf(baseType, in); + this.setArray(parameterIndex, array); } } @@ -985,8 +993,14 @@ public void setObject(@Positive int parameterIndex, @Nullable Object x) throws S setMap(parameterIndex, (Map) x); } else if (x instanceof Number) { setNumber(parameterIndex, (Number) x); - } else if (PrimitiveArraySupport.isSupportedPrimitiveArray(x)) { - setPrimitiveArray(parameterIndex, x); + } else if (x.getClass().isArray()) { + try { + setObjectArray(parameterIndex, x); + } catch (Exception e) { + throw new PSQLException( + GT.tr("Cannot cast an instance of {0} to type {1}", x.getClass().getName(), "Types.ARRAY"), + PSQLState.INVALID_PARAMETER_TYPE, e); + } } else { // Can't infer a type. throw new PSQLException(GT.tr( diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PrimitiveArraySupport.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PrimitiveArraySupport.java deleted file mode 100644 index 060672bdb1..0000000000 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PrimitiveArraySupport.java +++ /dev/null @@ -1,492 +0,0 @@ -/* - * Copyright (c) 2004, PostgreSQL Global Development Group - * See the LICENSE file in the project root for more information. - */ - -package org.postgresql.jdbc; - -import org.postgresql.core.Oid; -import org.postgresql.core.TypeInfo; -import org.postgresql.util.ByteConverter; - -import org.checkerframework.checker.nullness.qual.NonNull; -import org.checkerframework.checker.nullness.qual.Nullable; - -import java.sql.Connection; -import java.sql.SQLFeatureNotSupportedException; -import java.util.HashMap; -import java.util.Map; - -abstract class PrimitiveArraySupport { - - public abstract int getDefaultArrayTypeOid(TypeInfo tiCache); - - public abstract String toArrayString(char delim, A array); - - public abstract void appendArray(StringBuilder sb, char delim, A array); - - public boolean supportBinaryRepresentation() { - return true; - } - - public abstract byte[] toBinaryRepresentation(Connection connection, A array) throws SQLFeatureNotSupportedException; - - private static final PrimitiveArraySupport LONG_ARRAY = new PrimitiveArraySupport() { - - /** - * {@inheritDoc} - */ - @Override - public int getDefaultArrayTypeOid(TypeInfo tiCache) { - return Oid.INT8_ARRAY; - } - - @Override - public String toArrayString(char delim, long[] array) { - final StringBuilder sb = new StringBuilder(Math.max(64, array.length * 8)); - appendArray(sb, delim, array); - return sb.toString(); - } - - /** - * {@inheritDoc} - */ - @Override - public void appendArray(StringBuilder sb, char delim, long[] array) { - sb.append('{'); - for (int i = 0; i < array.length; ++i) { - if (i > 0) { - sb.append(delim); - } - sb.append(array[i]); - } - sb.append('}'); - } - - /** - * {@inheritDoc} - */ - @Override - public byte[] toBinaryRepresentation(Connection connection, long[] array) { - - int length = 20 + (12 * array.length); - final byte[] bytes = new byte[length]; - - // 1 dimension - ByteConverter.int4(bytes, 0, 1); - // no null - ByteConverter.int4(bytes, 4, 0); - // oid - ByteConverter.int4(bytes, 8, Oid.INT8); - // length - ByteConverter.int4(bytes, 12, array.length); - - int idx = 20; - for (int i = 0; i < array.length; ++i) { - bytes[idx + 3] = 8; - ByteConverter.int8(bytes, idx + 4, array[i]); - idx += 12; - } - - return bytes; - } - }; - - private static final PrimitiveArraySupport INT_ARRAY = new PrimitiveArraySupport() { - - /** - * {@inheritDoc} - */ - @Override - public int getDefaultArrayTypeOid(TypeInfo tiCache) { - return Oid.INT4_ARRAY; - } - - @Override - public String toArrayString(char delim, int[] array) { - final StringBuilder sb = new StringBuilder(Math.max(32, array.length * 6)); - appendArray(sb, delim, array); - return sb.toString(); - } - - /** - * {@inheritDoc} - */ - @Override - public void appendArray(StringBuilder sb, char delim, int[] array) { - sb.append('{'); - for (int i = 0; i < array.length; ++i) { - if (i > 0) { - sb.append(delim); - } - sb.append(array[i]); - } - sb.append('}'); - } - - /** - * {@inheritDoc} - */ - @Override - public byte[] toBinaryRepresentation(Connection connection, int[] array) { - - int length = 20 + (8 * array.length); - final byte[] bytes = new byte[length]; - - // 1 dimension - ByteConverter.int4(bytes, 0, 1); - // no null - ByteConverter.int4(bytes, 4, 0); - // oid - ByteConverter.int4(bytes, 8, Oid.INT4); - // length - ByteConverter.int4(bytes, 12, array.length); - - int idx = 20; - for (int i = 0; i < array.length; ++i) { - bytes[idx + 3] = 4; - ByteConverter.int4(bytes, idx + 4, array[i]); - idx += 8; - } - - return bytes; - } - }; - - private static final PrimitiveArraySupport SHORT_ARRAY = new PrimitiveArraySupport() { - - /** - * {@inheritDoc} - */ - @Override - public int getDefaultArrayTypeOid(TypeInfo tiCache) { - return Oid.INT2_ARRAY; - } - - @Override - public String toArrayString(char delim, short[] array) { - final StringBuilder sb = new StringBuilder(Math.max(32, array.length * 4)); - appendArray(sb, delim, array); - return sb.toString(); - } - - /** - * {@inheritDoc} - */ - @Override - public void appendArray(StringBuilder sb, char delim, short[] array) { - sb.append('{'); - for (int i = 0; i < array.length; ++i) { - if (i > 0) { - sb.append(delim); - } - sb.append(array[i]); - } - sb.append('}'); - } - - /** - * {@inheritDoc} - */ - @Override - public byte[] toBinaryRepresentation(Connection connection, short[] array) { - - int length = 20 + (6 * array.length); - final byte[] bytes = new byte[length]; - - // 1 dimension - ByteConverter.int4(bytes, 0, 1); - // no null - ByteConverter.int4(bytes, 4, 0); - // oid - ByteConverter.int4(bytes, 8, Oid.INT2); - // length - ByteConverter.int4(bytes, 12, array.length); - - int idx = 20; - for (int i = 0; i < array.length; ++i) { - bytes[idx + 3] = 2; - ByteConverter.int2(bytes, idx + 4, array[i]); - idx += 6; - } - - return bytes; - } - - }; - - private static final PrimitiveArraySupport DOUBLE_ARRAY = new PrimitiveArraySupport() { - - /** - * {@inheritDoc} - */ - @Override - public int getDefaultArrayTypeOid(TypeInfo tiCache) { - return Oid.FLOAT8_ARRAY; - } - - @Override - public String toArrayString(char delim, double[] array) { - final StringBuilder sb = new StringBuilder(Math.max(64, array.length * 8)); - appendArray(sb, delim, array); - return sb.toString(); - } - - /** - * {@inheritDoc} - */ - @Override - public void appendArray(StringBuilder sb, char delim, double[] array) { - sb.append('{'); - for (int i = 0; i < array.length; ++i) { - if (i > 0) { - sb.append(delim); - } - // use quotes to account for any issues with scientific notation - sb.append('"'); - sb.append(array[i]); - sb.append('"'); - } - sb.append('}'); - } - - /** - * {@inheritDoc} - */ - @Override - public byte[] toBinaryRepresentation(Connection connection, double[] array) { - - int length = 20 + (12 * array.length); - final byte[] bytes = new byte[length]; - - // 1 dimension - ByteConverter.int4(bytes, 0, 1); - // no null - ByteConverter.int4(bytes, 4, 0); - // oid - ByteConverter.int4(bytes, 8, Oid.FLOAT8); - // length - ByteConverter.int4(bytes, 12, array.length); - - int idx = 20; - for (int i = 0; i < array.length; ++i) { - bytes[idx + 3] = 8; - ByteConverter.float8(bytes, idx + 4, array[i]); - idx += 12; - } - - return bytes; - } - - }; - - private static final PrimitiveArraySupport FLOAT_ARRAY = new PrimitiveArraySupport() { - - /** - * {@inheritDoc} - */ - @Override - public int getDefaultArrayTypeOid(TypeInfo tiCache) { - return Oid.FLOAT4_ARRAY; - } - - @Override - public String toArrayString(char delim, float[] array) { - final StringBuilder sb = new StringBuilder(Math.max(64, array.length * 8)); - appendArray(sb, delim, array); - return sb.toString(); - } - - /** - * {@inheritDoc} - */ - @Override - public void appendArray(StringBuilder sb, char delim, float[] array) { - sb.append('{'); - for (int i = 0; i < array.length; ++i) { - if (i > 0) { - sb.append(delim); - } - // use quotes to account for any issues with scientific notation - sb.append('"'); - sb.append(array[i]); - sb.append('"'); - } - sb.append('}'); - } - - /** - * {@inheritDoc} - */ - @Override - public byte[] toBinaryRepresentation(Connection connection, float[] array) { - - int length = 20 + (8 * array.length); - final byte[] bytes = new byte[length]; - - // 1 dimension - ByteConverter.int4(bytes, 0, 1); - // no null - ByteConverter.int4(bytes, 4, 0); - // oid - ByteConverter.int4(bytes, 8, Oid.FLOAT4); - // length - ByteConverter.int4(bytes, 12, array.length); - - int idx = 20; - for (int i = 0; i < array.length; ++i) { - bytes[idx + 3] = 4; - ByteConverter.float4(bytes, idx + 4, array[i]); - idx += 8; - } - - return bytes; - } - - }; - - private static final PrimitiveArraySupport BOOLEAN_ARRAY = new PrimitiveArraySupport() { - - /** - * {@inheritDoc} - */ - @Override - public int getDefaultArrayTypeOid(TypeInfo tiCache) { - return Oid.BOOL_ARRAY; - } - - @Override - public String toArrayString(char delim, boolean[] array) { - final StringBuilder sb = new StringBuilder(Math.max(64, array.length * 8)); - appendArray(sb, delim, array); - return sb.toString(); - } - - /** - * {@inheritDoc} - */ - @Override - public void appendArray(StringBuilder sb, char delim, boolean[] array) { - sb.append('{'); - for (int i = 0; i < array.length; ++i) { - if (i > 0) { - sb.append(delim); - } - sb.append(array[i] ? '1' : '0'); - } - sb.append('}'); - } - - /** - * {@inheritDoc} - * - * @throws SQLFeatureNotSupportedException - * Because this feature is not supported. - */ - @Override - public byte[] toBinaryRepresentation(Connection connection, boolean[] array) throws SQLFeatureNotSupportedException { - int length = 20 + (5 * array.length); - final byte[] bytes = new byte[length]; - - // 1 dimension - ByteConverter.int4(bytes, 0, 1); - // no null - ByteConverter.int4(bytes, 4, 0); - // oid - ByteConverter.int4(bytes, 8, Oid.BOOL); - // length - ByteConverter.int4(bytes, 12, array.length); - - int idx = 20; - for (int i = 0; i < array.length; ++i) { - bytes[idx + 3] = 1; - ByteConverter.bool(bytes, idx + 4, array[i]); - idx += 5; - } - - return bytes; - } - - }; - - private static final PrimitiveArraySupport STRING_ARRAY = new PrimitiveArraySupport() { - - /** - * {@inheritDoc} - */ - @Override - public int getDefaultArrayTypeOid(TypeInfo tiCache) { - return Oid.VARCHAR_ARRAY; - } - - @Override - public String toArrayString(char delim, String[] array) { - final StringBuilder sb = new StringBuilder(Math.max(64, array.length * 8)); - appendArray(sb, delim, array); - return sb.toString(); - } - - /** - * {@inheritDoc} - */ - @Override - public void appendArray(StringBuilder sb, char delim, String[] array) { - sb.append('{'); - for (int i = 0; i < array.length; ++i) { - if (i > 0) { - sb.append(delim); - } - if (array[i] == null) { - sb.append('N'); - sb.append('U'); - sb.append('L'); - sb.append('L'); - } else { - PgArray.escapeArrayElement(sb, array[i]); - } - } - sb.append('}'); - } - - /** - * {@inheritDoc} - */ - @Override - public boolean supportBinaryRepresentation() { - return false; - } - - /** - * {@inheritDoc} - * - * @throws SQLFeatureNotSupportedException - * Because this feature is not supported. - */ - @Override - public byte[] toBinaryRepresentation(Connection connection, String[] array) throws SQLFeatureNotSupportedException { - throw new SQLFeatureNotSupportedException(); - } - - }; - - private static final Map ARRAY_CLASS_TO_SUPPORT = new HashMap((int) (7 / .75) + 1); - - static { - ARRAY_CLASS_TO_SUPPORT.put(long[].class, LONG_ARRAY); - ARRAY_CLASS_TO_SUPPORT.put(int[].class, INT_ARRAY); - ARRAY_CLASS_TO_SUPPORT.put(short[].class, SHORT_ARRAY); - ARRAY_CLASS_TO_SUPPORT.put(double[].class, DOUBLE_ARRAY); - ARRAY_CLASS_TO_SUPPORT.put(float[].class, FLOAT_ARRAY); - ARRAY_CLASS_TO_SUPPORT.put(boolean[].class, BOOLEAN_ARRAY); - ARRAY_CLASS_TO_SUPPORT.put(String[].class, STRING_ARRAY); - } - - public static boolean isSupportedPrimitiveArray(Object obj) { - return obj != null && ARRAY_CLASS_TO_SUPPORT.containsKey(obj.getClass()); - } - - public static @Nullable PrimitiveArraySupport getArraySupport( - A array) { - return ARRAY_CLASS_TO_SUPPORT.get(array.getClass()); - } -} diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc2/ArrayAssistantRegistry.java b/pgjdbc/src/main/java/org/postgresql/jdbc2/ArrayAssistantRegistry.java index 9f8e607d49..9500436f3f 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc2/ArrayAssistantRegistry.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc2/ArrayAssistantRegistry.java @@ -7,8 +7,8 @@ import org.checkerframework.checker.nullness.qual.Nullable; -import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; /** * Array assistants register here. @@ -16,7 +16,7 @@ * @author Minglei Tu */ public class ArrayAssistantRegistry { - private static final Map ARRAY_ASSISTANT_MAP = + private static final ConcurrentMap ARRAY_ASSISTANT_MAP = new ConcurrentHashMap(); public static @Nullable ArrayAssistant getAssistant(int oid) { diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/AbstractArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/AbstractArraysTest.java new file mode 100644 index 0000000000..f892712b28 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/AbstractArraysTest.java @@ -0,0 +1,946 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import static org.junit.Assert.assertEquals; + +import org.postgresql.PGNotification; +import org.postgresql.copy.CopyManager; +import org.postgresql.core.BaseConnection; +import org.postgresql.core.CachedQuery; +import org.postgresql.core.Encoding; +import org.postgresql.core.QueryExecutor; +import org.postgresql.core.ReplicationProtocol; +import org.postgresql.core.TransactionState; +import org.postgresql.core.TypeInfo; +import org.postgresql.core.Version; +import org.postgresql.fastpath.Fastpath; +import org.postgresql.jdbc.FieldMetadata.Key; +import org.postgresql.largeobject.LargeObjectManager; +import org.postgresql.replication.PGReplicationConnection; +import org.postgresql.util.LruCache; +import org.postgresql.util.PGobject; +import org.postgresql.xml.PGXmlFactoryFactory; + +import org.junit.Test; + +import java.lang.reflect.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.TimerTask; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +public abstract class AbstractArraysTest { + + private static final BaseConnection ENCODING_CONNECTION = new EncodingConnection(Encoding.getJVMEncoding("utf-8")); + + private final A[][] testData; + + private final boolean binarySupported; + + private final int arrayTypeOid; + + /** + * + * @param testData + * 3 dimensional array to use for testing. + * @param binarySupported + * Indicates if binary support is epxected for the type. + */ + public AbstractArraysTest(A[][] testData, boolean binarySupported, int arrayTypeOid) { + super(); + this.testData = testData; + this.binarySupported = binarySupported; + this.arrayTypeOid = arrayTypeOid; + } + + protected void assertArraysEquals(String message, A expected, Object actual) { + final int expectedLength = Array.getLength(expected); + assertEquals(message + " size", expectedLength, Array.getLength(actual)); + for (int i = 0; i < expectedLength; ++i) { + assertEquals(message + " value at " + i, Array.get(expected, i), Array.get(actual, i)); + } + } + + @Test + public void testBinary() throws Exception { + + A data = testData[0][0]; + + ArrayEncoding.ArrayEncoder support = ArrayEncoding.getArrayEncoder(data); + + final int defaultArrayTypeOid = support.getDefaultArrayTypeOid(); + + assertEquals(binarySupported, support.supportBinaryRepresentation(defaultArrayTypeOid)); + + if (binarySupported) { + + final PgArray pgArray = new PgArray(ENCODING_CONNECTION, defaultArrayTypeOid, + support.toBinaryRepresentation(ENCODING_CONNECTION, data, defaultArrayTypeOid)); + + Object actual = pgArray.getArray(); + + assertArraysEquals("", data, actual); + } + } + + @Test + public void testString() throws Exception { + + A data = testData[0][0]; + + ArrayEncoding.ArrayEncoder support = ArrayEncoding.getArrayEncoder(data); + + final String arrayString = support.toArrayString(',', data); + + final PgArray pgArray = new PgArray(ENCODING_CONNECTION, arrayTypeOid, arrayString); + + Object actual = pgArray.getArray(); + + assertArraysEquals("", data, actual); + } + + @Test + public void test2dBinary() throws Exception { + + A[] data = testData[0]; + + ArrayEncoding.ArrayEncoder support = ArrayEncoding.getArrayEncoder(data); + + final int defaultArrayTypeOid = support.getDefaultArrayTypeOid(); + + assertEquals(binarySupported, support.supportBinaryRepresentation(defaultArrayTypeOid)); + + if (binarySupported) { + + final PgArray pgArray = new PgArray(ENCODING_CONNECTION, support.getDefaultArrayTypeOid(), + support.toBinaryRepresentation(ENCODING_CONNECTION, data, defaultArrayTypeOid)); + + Object[] actual = (Object[]) pgArray.getArray(); + + assertEquals(data.length, actual.length); + + for (int i = 0; i < data.length; ++i) { + assertArraysEquals("array at position " + i, data[i], actual[i]); + } + } + } + + @Test + public void test2dString() throws Exception { + + final A[] data = testData[0]; + + final ArrayEncoding.ArrayEncoder support = ArrayEncoding.getArrayEncoder(data); + + final String arrayString = support.toArrayString(',', data); + + final PgArray pgArray = new PgArray(ENCODING_CONNECTION, arrayTypeOid, arrayString); + + Object[] actual = (Object[]) pgArray.getArray(); + + assertEquals(data.length, actual.length); + + for (int i = 0; i < data.length; ++i) { + assertArraysEquals("array at position " + i, data[i], actual[i]); + } + } + + @Test + public void test3dBinary() throws Exception { + + ArrayEncoding.ArrayEncoder support = ArrayEncoding.getArrayEncoder(testData); + + final int defaultArrayTypeOid = support.getDefaultArrayTypeOid(); + + assertEquals(binarySupported, support.supportBinaryRepresentation(defaultArrayTypeOid)); + + if (binarySupported) { + + final PgArray pgArray = new PgArray(ENCODING_CONNECTION, support.getDefaultArrayTypeOid(), + support.toBinaryRepresentation(ENCODING_CONNECTION, testData, defaultArrayTypeOid)); + + Object[][] actual = (Object[][]) pgArray.getArray(); + + assertEquals(testData.length, actual.length); + + for (int i = 0; i < testData.length; ++i) { + assertEquals("array length at " + i, testData[i].length, actual[i].length); + for (int j = 0; j < testData[i].length; ++j) { + assertArraysEquals("array at " + i + ',' + j, testData[i][j], actual[i][j]); + } + } + } + } + + @Test + public void test3dString() throws Exception { + + final ArrayEncoding.ArrayEncoder support = ArrayEncoding.getArrayEncoder(testData); + + final String arrayString = support.toArrayString(',', testData); + + final PgArray pgArray = new PgArray(ENCODING_CONNECTION, arrayTypeOid, arrayString); + + Object[][] actual = (Object[][]) pgArray.getArray(); + + assertEquals(testData.length, actual.length); + + for (int i = 0; i < testData.length; ++i) { + assertEquals("array length at " + i, testData[i].length, actual[i].length); + for (int j = 0; j < testData[i].length; ++j) { + assertArraysEquals("array at " + i + ',' + j, testData[i][j], actual[i][j]); + } + } + } + + private static final class EncodingConnection implements BaseConnection { + private final Encoding encoding; + private final TypeInfo typeInfo = new TypeInfoCache(this, -1); + + EncodingConnection(Encoding encoding) { + this.encoding = encoding; + } + + /** + * {@inheritDoc} + */ + public Encoding getEncoding() throws SQLException { + return encoding; + } + + /** + * {@inheritDoc} + */ + public TypeInfo getTypeInfo() { + return typeInfo; + } + + /** + * {@inheritDoc} + */ + public void cancelQuery() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public ResultSet execSQLQuery(String s) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public ResultSet execSQLQuery(String s, int resultSetType, int resultSetConcurrency) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void execSQLUpdate(String s) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public QueryExecutor getQueryExecutor() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public ReplicationProtocol getReplicationProtocol() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Object getObject(String type, String value, byte[] byteValue) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean haveMinimumServerVersion(int ver) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean haveMinimumServerVersion(Version ver) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public byte[] encodeString(String str) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public String escapeString(String str) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean getStandardConformingStrings() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public TimestampUtils getTimestampUtils() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Logger getLogger() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean getStringVarcharFlag() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public TransactionState getTransactionState() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean binaryTransferSend(int oid) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean isColumnSanitiserDisabled() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void addTimerTask(TimerTask timerTask, long milliSeconds) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void purgeTimerTasks() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public LruCache getFieldMetadataCache() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public CachedQuery createQuery(String sql, boolean escapeProcessing, boolean isParameterized, String... columnNames) + throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Statement createStatement() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PreparedStatement prepareStatement(String sql) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public CallableStatement prepareCall(String sql) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public String nativeSQL(String sql) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setAutoCommit(boolean autoCommit) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean getAutoCommit() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void commit() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void rollback() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void close() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean isClosed() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public DatabaseMetaData getMetaData() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setReadOnly(boolean readOnly) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean isReadOnly() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setCatalog(String catalog) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public String getCatalog() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setTransactionIsolation(int level) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int getTransactionIsolation() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public SQLWarning getWarnings() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void clearWarnings() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Map> getTypeMap() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setTypeMap(Map> map) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setHoldability(int holdability) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int getHoldability() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Savepoint setSavepoint() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Savepoint setSavepoint(String name) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void rollback(Savepoint savepoint) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Clob createClob() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Blob createBlob() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public NClob createNClob() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public SQLXML createSQLXML() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean isValid(int timeout) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setClientInfo(String name, String value) throws SQLClientInfoException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setClientInfo(Properties properties) throws SQLClientInfoException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public String getClientInfo(String name) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Properties getClientInfo() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public java.sql.Array createArrayOf(String typeName, Object[] elements) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setSchema(String schema) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public String getSchema() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void abort(Executor executor) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int getNetworkTimeout() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public T unwrap(Class iface) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public boolean isWrapperFor(Class iface) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public java.sql.Array createArrayOf(String typeName, Object elements) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PGNotification[] getNotifications() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PGNotification[] getNotifications(int timeoutMillis) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public CopyManager getCopyAPI() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public LargeObjectManager getLargeObjectAPI() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public Fastpath getFastpathAPI() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void addDataType(String type, String className) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void addDataType(String type, Class klass) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setPrepareThreshold(int threshold) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int getPrepareThreshold() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setDefaultFetchSize(int fetchSize) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int getDefaultFetchSize() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public int getBackendPID() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public String escapeIdentifier(String identifier) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public String escapeLiteral(String literal) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PreferQueryMode getPreferQueryMode() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public AutoSave getAutosave() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public void setAutosave(AutoSave autoSave) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + public PGReplicationConnection getReplicationAPI() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public Map getParameterStatuses() { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public String getParameterStatus(String parameterName) { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public PGXmlFactoryFactory getXmlFactoryFactory() throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean hintReadOnly() { + return false; + } + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/ArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/ArraysTest.java new file mode 100644 index 0000000000..bb93a3f4a4 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/ArraysTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import static org.junit.Assert.assertFalse; + +import org.postgresql.core.Oid; +import org.postgresql.util.PSQLException; + +import org.junit.Test; + +import java.math.BigDecimal; +import java.sql.SQLFeatureNotSupportedException; + +public class ArraysTest { + + @Test(expected = PSQLException.class) + public void testNonArrayNotSupported() throws Exception { + ArrayEncoding.getArrayEncoder("asdflkj"); + } + + @Test(expected = PSQLException.class) + public void testNoByteArray() throws Exception { + ArrayEncoding.getArrayEncoder(new byte[] {}); + } + + @Test(expected = SQLFeatureNotSupportedException.class) + public void testBinaryNotSupported() throws Exception { + final ArrayEncoding.ArrayEncoder support = ArrayEncoding.getArrayEncoder(new BigDecimal[] {}); + + assertFalse(support.supportBinaryRepresentation(Oid.FLOAT8_ARRAY)); + + support.toBinaryRepresentation(null, new BigDecimal[] { BigDecimal.valueOf(3) }, Oid.FLOAT8_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/ArraysTestSuite.java b/pgjdbc/src/test/java/org/postgresql/jdbc/ArraysTestSuite.java new file mode 100644 index 0000000000..a34c8e8ef6 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/ArraysTestSuite.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.junit.runner.RunWith; +import org.junit.runners.Suite; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ + BigDecimalObjectArraysTest.class, + BooleanArraysTest.class, + BooleanObjectArraysTest.class, + ByteaArraysTest.class, + DoubleArraysTest.class, + DoubleObjectArraysTest.class, + FloatArraysTest.class, + FloatObjectArraysTest.class, + IntArraysTest.class, + IntegerObjectArraysTest.class, + LongArraysTest.class, + LongObjectArraysTest.class, + ShortArraysTest.class, + ShortObjectArraysTest.class, + StringArraysTest.class +}) +public class ArraysTestSuite { +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/BigDecimalObjectArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/BigDecimalObjectArraysTest.java new file mode 100644 index 0000000000..901da10626 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/BigDecimalObjectArraysTest.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import static java.math.BigDecimal.valueOf; + +import org.postgresql.core.Oid; + +import java.math.BigDecimal; + +public class BigDecimalObjectArraysTest extends AbstractArraysTest { + + private static final BigDecimal[][][] doubles = new BigDecimal[][][] { + { { valueOf(1.3), valueOf(2.4), valueOf(3.1), valueOf(4.2) }, + { valueOf(5D), valueOf(6D), valueOf(7D), valueOf(8D) }, + { valueOf(9D), valueOf(10D), valueOf(11D), valueOf(12D) } }, + { { valueOf(13D), valueOf(14D), valueOf(15D), valueOf(16D) }, { valueOf(17D), valueOf(18D), valueOf(19D), null }, + { valueOf(21D), valueOf(22D), valueOf(23D), valueOf(24D) } } }; + + public BigDecimalObjectArraysTest() { + super(doubles, false, Oid.NUMERIC_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/BooleanArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/BooleanArraysTest.java new file mode 100644 index 0000000000..c3717b5f4a --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/BooleanArraysTest.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class BooleanArraysTest extends AbstractArraysTest { + private static final boolean[][][] booleans = new boolean[][][] { + { { true, false, false, true }, { false, false, true, true }, { true, true, false, false } }, + { { false, true, true, false }, { true, false, true, false }, { false, true, false, true } } }; + + public BooleanArraysTest() { + super(booleans, true, Oid.BOOL_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/BooleanObjectArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/BooleanObjectArraysTest.java new file mode 100644 index 0000000000..6e33d1cd62 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/BooleanObjectArraysTest.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class BooleanObjectArraysTest extends AbstractArraysTest { + private static final Boolean[][][] booleans = new Boolean[][][] { + { { true, false, null, true }, { false, false, true, true }, { true, true, false, false } }, + { { false, true, true, false }, { true, false, true, null }, { false, true, false, true } } }; + + public BooleanObjectArraysTest() { + super(booleans, true, Oid.BOOL_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/ByteaArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/ByteaArraysTest.java new file mode 100644 index 0000000000..3a8141fa39 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/ByteaArraysTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import static org.junit.Assert.assertEquals; + +import org.postgresql.core.Oid; + +import org.junit.Assert; + +import java.lang.reflect.Array; + +public class ByteaArraysTest extends AbstractArraysTest { + + private static final byte[][][][] longs = new byte[][][][] { + { { { 0x1, 0x2, (byte) 0xFF, 0x4 }, { 0x5, 0x6, 0x7, (byte) 0xFF }, null, { 0x9, 0x10, 0x11, 0x12 } }, + { null, { 0x13, 0x14, 0x15, 0x16 }, { 0x17, 0x18, (byte) 0xFF, 0x20 }, { 0x1, 0x2, (byte) 0xFF, 0x4 } }, + { { 0x1, 0x2, (byte) 0xFF, 0x4 }, { 0x1, 0x2, (byte) 0xFF, 0x4 }, { 0x1, 0x2, (byte) 0xFF, 0x4 }, + { 0x1, 0x2, (byte) 0xFF, 0x4 } } }, + { { { 0x1, 0x2, (byte) 0xFF, 0x4 }, { 0x1, 0x2, (byte) 0xFF, 0x4 }, { 0x1, 0x2, (byte) 0xFF, 0x4 }, + { 0x1, 0x2, (byte) 0xFE, 0x4 } }, + { { 0x1, 0x2, (byte) 0xCD, 0x4 }, { 0x1, 0x2, (byte) 0xFF, 0x4 }, { 0x1, 0x2, (byte) 0xFF, 0x4 }, + { 0x1, 0x2, (byte) 0xFF, 0x4 } }, + { { 0x1, 0x2, (byte) 0xFF, 0x4 }, { 0x1, 0x2, (byte) 0xFE, 0x10 }, { 0x1, 0x2, (byte) 0xFF, 0x4 }, + { 0x1, 0x2, (byte) 0xFF, 0x4 } } } }; + + public ByteaArraysTest() { + super(longs, true, Oid.BYTEA_ARRAY); + } + + /** + * {@inheritDoc} + */ + @Override + protected void assertArraysEquals(String message, byte[][] expected, Object actual) { + final int expectedLength = Array.getLength(expected); + assertEquals(message + " size", expectedLength, Array.getLength(actual)); + for (int i = 0; i < expectedLength; ++i) { + Assert.assertArrayEquals(message + " value at " + i, expected[i], (byte[]) Array.get(actual, i)); + } + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/DoubleArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/DoubleArraysTest.java new file mode 100644 index 0000000000..1f21f547d6 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/DoubleArraysTest.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class DoubleArraysTest extends AbstractArraysTest { + + private static final double[][][] doubles = new double[][][] { + { { 1.2, 2.3, 3.7, 4.9 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }, + { { 13, 14, 15, 16 }, { 17, 18, 19, 20 }, { 21, 22, 23, 24 } } }; + + public DoubleArraysTest() { + super(doubles, true, Oid.FLOAT8_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/DoubleObjectArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/DoubleObjectArraysTest.java new file mode 100644 index 0000000000..90bc82036f --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/DoubleObjectArraysTest.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class DoubleObjectArraysTest extends AbstractArraysTest { + + private static final Double[][][] doubles = new Double[][][] { + { { 1.3, 2.4, 3.1, 4.2 }, { 5D, 6D, 7D, 8D }, { 9D, 10D, 11D, 12D } }, + { { 13D, 14D, 15D, 16D }, { 17D, 18D, 19D, null }, { 21D, 22D, 23D, 24D } } }; + + public DoubleObjectArraysTest() { + super(doubles, true, Oid.FLOAT8_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/FloatArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/FloatArraysTest.java new file mode 100644 index 0000000000..ef9ed26ad7 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/FloatArraysTest.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class FloatArraysTest extends AbstractArraysTest { + + private static final float[][][] floats = new float[][][] { + { { 1.2f, 2.3f, 3.7f, 4.9f }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }, + { { 13, 14, 15, 16 }, { 17, 18, 19, 20 }, { 21, 22, 23, 24 } } }; + + public FloatArraysTest() { + super(floats, true, Oid.FLOAT4_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/FloatObjectArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/FloatObjectArraysTest.java new file mode 100644 index 0000000000..c9a4a5f989 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/FloatObjectArraysTest.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class FloatObjectArraysTest extends AbstractArraysTest { + + private static final Float[][][] floats = new Float[][][] { + { { 1.3f, 2.4f, 3.1f, 4.2f }, { 5f, 6f, 7f, 8f }, { 9f, 10f, 11f, 12f } }, + { { 13f, 14f, 15f, 16f }, { 17f, 18f, 19f, null }, { 21f, 22f, 23f, 24f } } }; + + public FloatObjectArraysTest() { + super(floats, true, Oid.FLOAT4_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/IntArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/IntArraysTest.java new file mode 100644 index 0000000000..01ce25a32a --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/IntArraysTest.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class IntArraysTest extends AbstractArraysTest { + + private static final int[][][] ints = new int[][][] { { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }, + { { 13, 14, 15, 16 }, { 17, 18, 19, 20 }, { 21, 22, 23, 24 } } }; + + public IntArraysTest() { + super(ints, true, Oid.INT4_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/IntegerObjectArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/IntegerObjectArraysTest.java new file mode 100644 index 0000000000..026659592c --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/IntegerObjectArraysTest.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class IntegerObjectArraysTest extends AbstractArraysTest { + + private static final Integer[][][] ints = new Integer[][][] { + { { 1, 2, 3, 4 }, { 5, null, 7, 8 }, { 9, 10, 11, 12 } }, + { { 13, 14, 15, 16 }, { 17, 18, 19, 20 }, { 21, 22, 23, 24 } } }; + + public IntegerObjectArraysTest() { + super(ints, true, Oid.INT4_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/LongArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/LongArraysTest.java new file mode 100644 index 0000000000..db2c6c4428 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/LongArraysTest.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class LongArraysTest extends AbstractArraysTest { + + private static final long[][][] longs = new long[][][] { { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }, + { { 13, 14, 15, 16 }, { 17, 18, 19, 20 }, { 21, 22, 23, 24 } } }; + + public LongArraysTest() { + super(longs, true, Oid.INT8_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/LongObjectArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/LongObjectArraysTest.java new file mode 100644 index 0000000000..5625fabb75 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/LongObjectArraysTest.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class LongObjectArraysTest extends AbstractArraysTest { + + private static final Long[][][] longs = new Long[][][] { + { { 1L, 2L, null, 4L }, { 5L, 6L, 7L, 8L }, { 9L, 10L, 11L, 12L } }, + { { 13L, 14L, 15L, 16L }, { 17L, 18L, 19L, 20L }, { 21L, 22L, 23L, 24L } } }; + + public LongObjectArraysTest() { + super(longs, true, Oid.INT8_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/PrimitiveArraySupportTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/PrimitiveArraySupportTest.java deleted file mode 100644 index 83b028bbce..0000000000 --- a/pgjdbc/src/test/java/org/postgresql/jdbc/PrimitiveArraySupportTest.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * Copyright (c) 2003, PostgreSQL Global Development Group - * See the LICENSE file in the project root for more information. - */ - -package org.postgresql.jdbc; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; - -import org.postgresql.core.Oid; - -import org.junit.Test; - -import java.sql.SQLFeatureNotSupportedException; - -public class PrimitiveArraySupportTest { - - public PrimitiveArraySupport longArrays = PrimitiveArraySupport.getArraySupport(new long[] {}); - public PrimitiveArraySupport intArrays = PrimitiveArraySupport.getArraySupport(new int[] {}); - public PrimitiveArraySupport shortArrays = PrimitiveArraySupport.getArraySupport(new short[] {}); - public PrimitiveArraySupport doubleArrays = PrimitiveArraySupport.getArraySupport(new double[] {}); - public PrimitiveArraySupport floatArrays = PrimitiveArraySupport.getArraySupport(new float[] {}); - public PrimitiveArraySupport booleanArrays = PrimitiveArraySupport.getArraySupport(new boolean[] {}); - - @Test - public void testLongBinary() throws Exception { - final long[] longs = new long[84]; - for (int i = 0; i < 84; ++i) { - longs[i] = i - 3; - } - - final PgArray pgArray = new PgArray(null, Oid.INT8_ARRAY, longArrays.toBinaryRepresentation(null, longs)); - - Object arrayObj = pgArray.getArray(); - - assertThat(arrayObj, instanceOf(Long[].class)); - - final Long[] actual = (Long[]) arrayObj; - - assertEquals(longs.length, actual.length); - - for (int i = 0; i < longs.length; ++i) { - assertEquals(Long.valueOf(longs[i]), actual[i]); - } - } - - @Test - public void testLongToString() throws Exception { - final long[] longs = new long[] { 12367890987L, 987664198234L, -2982470923874L }; - - final String arrayString = longArrays.toArrayString(',', longs); - - assertEquals("{12367890987,987664198234,-2982470923874}", arrayString); - - final String altArrayString = longArrays.toArrayString(';', longs); - - assertEquals("{12367890987;987664198234;-2982470923874}", altArrayString); - } - - @Test - public void testIntBinary() throws Exception { - final int[] ints = new int[13]; - for (int i = 0; i < 13; ++i) { - ints[i] = i - 3; - } - - final PgArray pgArray = new PgArray(null, Oid.INT4_ARRAY, intArrays.toBinaryRepresentation(null, ints)); - - Object arrayObj = pgArray.getArray(); - - assertThat(arrayObj, instanceOf(Integer[].class)); - - final Integer[] actual = (Integer[]) arrayObj; - - assertEquals(ints.length, actual.length); - - for (int i = 0; i < ints.length; ++i) { - assertEquals(Integer.valueOf(ints[i]), actual[i]); - } - } - - @Test - public void testIntToString() throws Exception { - final int[] ints = new int[] { 12367890, 987664198, -298247092 }; - - final String arrayString = intArrays.toArrayString(',', ints); - - assertEquals("{12367890,987664198,-298247092}", arrayString); - - final String altArrayString = intArrays.toArrayString(';', ints); - - assertEquals("{12367890;987664198;-298247092}", altArrayString); - - } - - @Test - public void testShortToBinary() throws Exception { - final short[] shorts = new short[13]; - for (int i = 0; i < 13; ++i) { - shorts[i] = (short) (i - 3); - } - - final PgArray pgArray = new PgArray(null, Oid.INT4_ARRAY, shortArrays.toBinaryRepresentation(null, shorts)); - - Object arrayObj = pgArray.getArray(); - - assertThat(arrayObj, instanceOf(Short[].class)); - - final Short[] actual = (Short[]) arrayObj; - - assertEquals(shorts.length, actual.length); - - for (int i = 0; i < shorts.length; ++i) { - assertEquals(Short.valueOf(shorts[i]), actual[i]); - } - } - - @Test - public void testShortToString() throws Exception { - final short[] shorts = new short[] { 123, 34, -57 }; - - final String arrayString = shortArrays.toArrayString(',', shorts); - - assertEquals("{123,34,-57}", arrayString); - - final String altArrayString = shortArrays.toArrayString(';', shorts); - - assertEquals("{123;34;-57}", altArrayString); - - } - - @Test - public void testDoubleBinary() throws Exception { - final double[] doubles = new double[13]; - for (int i = 0; i < 13; ++i) { - doubles[i] = i - 3.1; - } - - final PgArray pgArray = new PgArray(null, Oid.FLOAT8_ARRAY, doubleArrays.toBinaryRepresentation(null, doubles)); - - Object arrayObj = pgArray.getArray(); - - assertThat(arrayObj, instanceOf(Double[].class)); - - final Double[] actual = (Double[]) arrayObj; - - assertEquals(doubles.length, actual.length); - - for (int i = 0; i < doubles.length; ++i) { - assertEquals(Double.valueOf(doubles[i]), actual[i]); - } - } - - @Test - public void testdoubleToString() throws Exception { - final double[] doubles = new double[] { 122353.345, 923487.235987, -23.239486 }; - - final String arrayString = doubleArrays.toArrayString(',', doubles); - - assertEquals("{\"122353.345\",\"923487.235987\",\"-23.239486\"}", arrayString); - - final String altArrayString = doubleArrays.toArrayString(';', doubles); - - assertEquals("{\"122353.345\";\"923487.235987\";\"-23.239486\"}", altArrayString); - - } - - @Test - public void testFloatBinary() throws Exception { - final float[] floats = new float[13]; - for (int i = 0; i < 13; ++i) { - floats[i] = (float) (i - 3.1); - } - - final PgArray pgArray = new PgArray(null, Oid.FLOAT4_ARRAY, floatArrays.toBinaryRepresentation(null, floats)); - - Object arrayObj = pgArray.getArray(); - - assertThat(arrayObj, instanceOf(Float[].class)); - - final Float[] actual = (Float[]) arrayObj; - - assertEquals(floats.length, actual.length); - - for (int i = 0; i < floats.length; ++i) { - assertEquals(Float.valueOf(floats[i]), actual[i]); - } - } - - @Test - public void testfloatToString() throws Exception { - final float[] floats = new float[] { 122353.34f, 923487.25f, -23.2394f }; - - final String arrayString = floatArrays.toArrayString(',', floats); - - assertEquals("{\"122353.34\",\"923487.25\",\"-23.2394\"}", arrayString); - - final String altArrayString = floatArrays.toArrayString(';', floats); - - assertEquals("{\"122353.34\";\"923487.25\";\"-23.2394\"}", altArrayString); - - } - - @Test - public void testBooleanBinary() throws Exception { - final boolean[] bools = new boolean[] { true, true, false }; - - final PgArray pgArray = new PgArray(null, Oid.BIT, booleanArrays.toBinaryRepresentation(null, bools)); - - Object arrayObj = pgArray.getArray(); - - assertThat(arrayObj, instanceOf(Boolean[].class)); - - final Boolean[] actual = (Boolean[]) arrayObj; - - assertEquals(bools.length, actual.length); - - for (int i = 0; i < bools.length; ++i) { - assertEquals(Boolean.valueOf(bools[i]), actual[i]); - } - } - - @Test - public void testBooleanToString() throws Exception { - final boolean[] bools = new boolean[] { true, true, false }; - - final String arrayString = booleanArrays.toArrayString(',', bools); - - assertEquals("{1,1,0}", arrayString); - - final String altArrayString = booleanArrays.toArrayString(';', bools); - - assertEquals("{1;1;0}", altArrayString); - } - - @Test - public void testStringNotSupportBinary() { - PrimitiveArraySupport stringArrays = PrimitiveArraySupport.getArraySupport(new String[] {}); - assertNotNull(stringArrays); - assertFalse(stringArrays.supportBinaryRepresentation()); - try { - stringArrays.toBinaryRepresentation(null, new String[] { "1.2" }); - fail("no sql exception thrown"); - } catch (SQLFeatureNotSupportedException e) { - - } - } -} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/ShortArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/ShortArraysTest.java new file mode 100644 index 0000000000..ed2779297f --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/ShortArraysTest.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class ShortArraysTest extends AbstractArraysTest { + + private static final short[][][] shorts = new short[][][] { { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }, + { { 13, 14, 15, 16 }, { 17, 18, 19, 20 }, { 21, 22, 23, 24 } } }; + + public ShortArraysTest() { + super(shorts, true, Oid.INT2_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/ShortObjectArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/ShortObjectArraysTest.java new file mode 100644 index 0000000000..856da90475 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/ShortObjectArraysTest.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class ShortObjectArraysTest extends AbstractArraysTest { + + private static final Short[][][] shorts = new Short[][][] { { { 1, 2, 3, 4 }, { 5, 6, 7, 8 }, { 9, 10, 11, 12 } }, + { { 13, 14, 15, 16 }, { 17, 18, null, 20 }, { 21, 22, 23, 24 } } }; + + public ShortObjectArraysTest() { + super(shorts, true, Oid.INT2_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/StringArraysTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/StringArraysTest.java new file mode 100644 index 0000000000..d9e131ac73 --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/StringArraysTest.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.jdbc; + +import org.postgresql.core.Oid; + +public class StringArraysTest extends AbstractArraysTest { + + private static final String[][][] strings = new String[][][] { + { { "some", "String", "haVE some \u03C0", "another" }, { null, "6L", "7L", "8L" }, //unicode escape for pi character + { "asdf", " asdf ", "11L", null } }, + { { "13L", null, "asasde4wtq", "16L" }, { "17L", "", "19L", "20L" }, { "21L", "22L", "23L", "24L" } } }; + + public StringArraysTest() { + super(strings, true, Oid.VARCHAR_ARRAY); + } +} diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java index b4075d6919..afcb832608 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/DatabaseMetaDataTest.java @@ -11,13 +11,17 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import org.postgresql.PGProperty; import org.postgresql.core.ServerVersion; import org.postgresql.test.TestUtil; +import org.postgresql.test.jdbc2.BaseTest4.BinaryMode; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.sql.Connection; import java.sql.DatabaseMetaData; @@ -29,21 +33,44 @@ import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; +import java.util.Properties; import java.util.Set; /* * TestCase to test the internal functionality of org.postgresql.jdbc2.DatabaseMetaData * */ +@RunWith(Parameterized.class) public class DatabaseMetaDataTest { private Connection con; + private final BinaryMode binaryMode; + + public DatabaseMetaDataTest(BinaryMode binaryMode) { + this.binaryMode = binaryMode; + } + + @Parameterized.Parameters(name = "binary = {0}") + public static Iterable data() { + Collection ids = new ArrayList(); + for (BinaryMode binaryMode : BinaryMode.values()) { + ids.add(new Object[]{binaryMode}); + } + return ids; + } @Before public void setUp() throws Exception { - con = TestUtil.openDB(); + if (binaryMode == BinaryMode.FORCE) { + final Properties props = new Properties(); + PGProperty.PREPARE_THRESHOLD.set(props, -1); + con = TestUtil.openDB(props); + } else { + con = TestUtil.openDB(); + } TestUtil.createTable(con, "metadatatest", "id int4, name text, updated timestamptz, colour text, quest text"); TestUtil.dropSequence(con, "sercoltest_b_seq"); diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java index 653512f820..842cd0db44 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java @@ -13,10 +13,11 @@ import org.postgresql.core.ReturningParserTest; import org.postgresql.core.UTF8EncodingTest; import org.postgresql.core.v3.V3ParameterListTests; +import org.postgresql.jdbc.ArraysTest; +import org.postgresql.jdbc.ArraysTestSuite; import org.postgresql.jdbc.DeepBatchedInsertStatementTest; import org.postgresql.jdbc.NoColumnMetadataIssue1613Test; import org.postgresql.jdbc.PgSQLXMLTest; -import org.postgresql.jdbc.PrimitiveArraySupportTest; import org.postgresql.test.core.FixedLengthOutputStreamTest; import org.postgresql.test.core.JavaVersionTest; import org.postgresql.test.core.LogServerMessagePropertyTest; @@ -42,6 +43,8 @@ @Suite.SuiteClasses({ ANTTest.class, ArrayTest.class, + ArraysTest.class, + ArraysTestSuite.class, BatchedInsertReWriteEnabledTest.class, BatchExecuteTest.class, BatchFailureTest.class, @@ -97,7 +100,6 @@ PGTimeTest.class, PgSQLXMLTest.class, PreparedStatementTest.class, - PrimitiveArraySupportTest.class, QuotationTest.class, ReaderInputStreamTest.class, RefCursorTest.class, @@ -122,7 +124,7 @@ UpdateableResultTest.class, UpsertTest.class, UTF8EncodingTest.class, - V3ParameterListTests.class, + V3ParameterListTests.class }) public class Jdbc2TestSuite { } diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/ArrayTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/ArrayTest.java index aa7ebd3b39..4803d70ed2 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/ArrayTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/ArrayTest.java @@ -11,6 +11,7 @@ import org.postgresql.jdbc.PreferQueryMode; import org.postgresql.test.TestUtil; import org.postgresql.test.jdbc2.BaseTest4; +import org.postgresql.test.util.RegexMatcher; import org.postgresql.util.PGobject; import org.postgresql.util.PGtokenizer; @@ -112,6 +113,66 @@ public void testCreateArrayOfInt() throws SQLException { Assert.assertEquals(2, out[2].intValue()); } + @Test + public void testCreateArrayOfBytes() throws SQLException { + + PreparedStatement pstmt = conn.prepareStatement("SELECT ?::bytea[]"); + final byte[][] in = new byte[][] { { 0x01, (byte) 0xFF, (byte) 0x12 }, {}, { (byte) 0xAC, (byte) 0xE4 }, null }; + final Array createdArray = conn.createArrayOf("bytea", in); + + byte[][] inCopy = (byte[][]) createdArray.getArray(); + + Assert.assertEquals(4, inCopy.length); + + Assert.assertArrayEquals(in[0], inCopy[0]); + Assert.assertArrayEquals(in[1], inCopy[1]); + Assert.assertArrayEquals(in[2], inCopy[2]); + Assert.assertArrayEquals(in[3], inCopy[3]); + Assert.assertNull(inCopy[3]); + + pstmt.setArray(1, createdArray); + + ResultSet rs = pstmt.executeQuery(); + Assert.assertTrue(rs.next()); + Array arr = rs.getArray(1); + + byte[][] out = (byte[][]) arr.getArray(); + + Assert.assertEquals(4, out.length); + + Assert.assertArrayEquals(in[0], out[0]); + Assert.assertArrayEquals(in[1], out[1]); + Assert.assertArrayEquals(in[2], out[2]); + Assert.assertArrayEquals(in[3], out[3]); + Assert.assertNull(out[3]); + } + + @Test + public void testCreateArrayOfBytesFromString() throws SQLException { + + assumeMinimumServerVersion("support for bytea[] as string requires hex string support from 9.0", + ServerVersion.v9_0); + + PreparedStatement pstmt = conn.prepareStatement("SELECT ?::bytea[]"); + final byte[][] in = new byte[][] { { 0x01, (byte) 0xFF, (byte) 0x12 }, {}, { (byte) 0xAC, (byte) 0xE4 }, null }; + + pstmt.setString(1, "{\"\\\\x01ff12\",\"\\\\x\",\"\\\\xace4\",NULL}"); + + ResultSet rs = pstmt.executeQuery(); + Assert.assertTrue(rs.next()); + Array arr = rs.getArray(1); + + byte[][] out = (byte[][]) arr.getArray(); + + Assert.assertEquals(4, out.length); + + Assert.assertArrayEquals(in[0], out[0]); + Assert.assertArrayEquals(in[1], out[1]); + Assert.assertArrayEquals(in[2], out[2]); + Assert.assertArrayEquals(in[3], out[3]); + Assert.assertNull(out[3]); + } + @Test public void testCreateArrayOfSmallInt() throws SQLException { PreparedStatement pstmt = conn.prepareStatement("SELECT ?::smallint[]"); @@ -332,12 +393,12 @@ public void testUUIDArray() throws SQLException { @Test public void testSetObjectFromJavaArray() throws SQLException { - String[] strArray = new String[]{"a", "b", "c"}; + String[] strArray = new String[] { "a", "b", "c" }; Object[] objCopy = Arrays.copyOf(strArray, strArray.length, Object[].class); PreparedStatement pstmt = conn.prepareStatement("INSERT INTO arrtest(strarr) VALUES (?)"); - //cannot handle generic Object[] + // cannot handle generic Object[] try { pstmt.setObject(1, objCopy, Types.ARRAY); pstmt.executeUpdate(); @@ -541,13 +602,16 @@ public void testToString() throws SQLException { Array doubles = rs.getArray(1); String actual = doubles.toString(); if (actual != null) { + // if a binary array is provided, the string representation looks like [0:1][0:1]={{1,2},{3,4}} + int idx = actual.indexOf('='); + if (idx > 0) { + actual = actual.substring(idx + 1); + } // Remove all double quotes. They do not make a difference here. actual = actual.replaceAll("\"", ""); - // Replace X.0 with just X - actual = actual.replaceAll("\\.0+([^0-9])", "$1"); } - Assert.assertEquals("Array.toString should use square braces", - "{3.5,-4.5,NULL,77}", actual); + //the string format may vary based on how data stored + Assert.assertThat(actual, RegexMatcher.matchesPattern("\\{3\\.5,-4\\.5,NULL,77(.0)?\\}")); } } finally { @@ -589,10 +653,34 @@ public void multiDimIntArray() throws SQLException { rs.next(); Array resArray = rs.getArray(1); String stringValue = resArray.toString(); + // if a binary array is provided, the string representation looks like [0:1][0:1]={{1,2},{3,4}} + int idx = stringValue.indexOf('='); + if (idx > 0) { + stringValue = stringValue.substring(idx + 1); + } // Both {{"1","2"},{"3","4"}} and {{1,2},{3,4}} are the same array representation stringValue = stringValue.replaceAll("\"", ""); Assert.assertEquals("{{1,2},{3,4}}", stringValue); TestUtil.closeQuietly(rs); TestUtil.closeQuietly(ps); } + + @Test + public void insertAndQueryMultiDimArray() throws SQLException { + Array arr = con.createArrayOf("int4", new int[][] { { 1, 2 }, { 3, 4 } }); + PreparedStatement insertPs = con.prepareStatement("INSERT INTO arrtest(intarr2) VALUES (?)"); + insertPs.setArray(1, arr); + insertPs.execute(); + insertPs.close(); + + PreparedStatement selectPs = con.prepareStatement("SELECT intarr2 FROM arrtest"); + ResultSet rs = selectPs.executeQuery(); + rs.next(); + + Array array = rs.getArray(1); + Integer[][] secondRowValues = (Integer[][]) array.getArray(2, 1); + + Assert.assertEquals(3, secondRowValues[0][0].intValue()); + Assert.assertEquals(4, secondRowValues[0][1].intValue()); + } } diff --git a/pgjdbc/src/test/java/org/postgresql/test/util/RegexMatcher.java b/pgjdbc/src/test/java/org/postgresql/test/util/RegexMatcher.java new file mode 100644 index 0000000000..382eb4cb2d --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/test/util/RegexMatcher.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2018, PostgreSQL Global Development Group + * See the LICENSE file in the project root for more information. + */ + +package org.postgresql.test.util; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +import java.util.regex.Pattern; + +/** + * Provides a matcher for String objects which does a regex comparison. + */ +public final class RegexMatcher extends TypeSafeMatcher { + + private final Pattern pattern; + + /** + * @param pattern + * The pattern to match items on. + */ + private RegexMatcher(Pattern pattern) { + this.pattern = pattern; + } + + public static Matcher matchesPattern(String pattern) { + return new RegexMatcher(Pattern.compile(pattern)); + } + + /** + * {@inheritDoc} + */ + @Override + public void describeTo(Description description) { + description.appendText("matches regex=" + pattern.toString()); + } + + /** + * {@inheritDoc} + */ + @Override + protected boolean matchesSafely(String item) { + return pattern.matcher(item).matches(); + } + +}