diff --git a/docs/_config.yml b/docs/_config.yml index fdb49ed6e0..58b50f7c91 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,5 +1,8 @@ name: PostgreSQL JDBC Driver website markdown: redcarpet +redcarpet: + extensions: + - tables highlighter: pygments excerpt_separator: exclude: diff --git a/docs/documentation/head/arrays.md b/docs/documentation/head/arrays.md new file mode 100644 index 0000000000..b74356a10b --- /dev/null +++ b/docs/documentation/head/arrays.md @@ -0,0 +1,29 @@ +--- +layout: default_docs +title: Arrays +header: Chapter 9. PostgreSQL™ Extensions to the JDBC API +resource: media +previoustitle: Physical and Logical replication API +previous: replication.html +nexttitle: Chapter 10. Using the Driver in a Multithreaded or a Servlet Environment +next: thread.html +--- + +PostgreSQL™ provides robust support for array data types as column types, function arguments +and criteria in where clauses. There are several ways to create arrays with pgjdbc. + +The [java.sql.Connection.createArrayOf(String, Object\[\])](https://docs.oracle.com/javase/8/docs/api/java/sql/Connection.html#createArrayOf-java.lang.String-java.lang.Object:A-) can be used to create an [java.sql.Array](https://docs.oracle.com/javase/8/docs/api/java/sql/Array.html) from `Object[]` instances (Note: this includes both primitive and object multi-dimensional arrays). +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: + +Java Type | Default PostgreSQL™ Type +--- | --- +`short[]` | `int2[]` +`int[]` | `int4[]` +`long[]` | `int8[]` +`float[]` | `float4[]` +`double[]` | `float8[]` +`boolean[]` | `bool[]` +`String[]` | `varchar[]` diff --git a/docs/documentation/head/ext.md b/docs/documentation/head/ext.md index 753a1ee4ab..ea7cee9b12 100644 --- a/docs/documentation/head/ext.md +++ b/docs/documentation/head/ext.md @@ -17,6 +17,7 @@ next: geometric.html * [Listen / Notify](listennotify.html) * [Server Prepared Statements](server-prepare.html) * [Physical and Logical replication API](replication.html) +* [Arrays](arrays.html) PostgreSQL™ is an extensible database system. You can add your own functions to the server, which can then be called from queries, or even add your own data types. diff --git a/docs/documentation/head/index.html b/docs/documentation/head/index.html index ec9162b0a0..70ef62fd0e 100644 --- a/docs/documentation/head/index.html +++ b/docs/documentation/head/index.html @@ -120,6 +120,7 @@

Table of Contents

Listen / Notify
Server Prepared Statements
Physical and Logical replication API
+
Arrays
diff --git a/docs/documentation/head/replication.md b/docs/documentation/head/replication.md index c90caeb173..3494b24a2e 100644 --- a/docs/documentation/head/replication.md +++ b/docs/documentation/head/replication.md @@ -5,8 +5,8 @@ header: Chapter 9. PostgreSQL™ Extensions to the JDBC API resource: media previoustitle: Server Prepared Statements previous: server-prepare.html -nexttitle: Chapter 10. Using the Driver in a Multithreaded or a Servlet Environment -next: thread.html +nexttitle: Arrays +next: arrays.html --- **Table of Contents** diff --git a/pgjdbc/src/main/java/org/postgresql/PGConnection.java b/pgjdbc/src/main/java/org/postgresql/PGConnection.java index 2456098104..ec849568e3 100644 --- a/pgjdbc/src/main/java/org/postgresql/PGConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/PGConnection.java @@ -13,6 +13,7 @@ import org.postgresql.replication.PGReplicationConnection; import org.postgresql.util.PGobject; +import java.sql.Array; import java.sql.SQLException; import java.sql.Statement; @@ -21,6 +22,25 @@ * returned by the PostgreSQL driver implement PGConnection. */ public interface PGConnection { + + /** + * Creates an {@link Array} wrapping elements. This is similar to + * {@link java.sql.Connection#createArrayOf(String, Object[])}, but also + * provides support for primitive arrays. + * + * @param typeName + * The SQL name of the type to map the elements to. + * Must not be {@code null}. + * @param elements + * The array of objects to map. A {@code null} value will result in + * an {@link Array} representing {@code null}. + * @return An {@link Array} wrapping elements. + * @throws SQLException + * If for some reason the array cannot be created. + * @see java.sql.Connection#createArrayOf(String, Object[]) + */ + Array createArrayOf(String typeName, Object elements) throws SQLException; + /** * This method returns any notifications that have been received since the last call to this * method. Returns null if there have been no notifications. diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java index 269ab2c7cc..3810081f3f 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgArray.java @@ -250,6 +250,9 @@ private int storeValues(final Object[] arr, int elementOid, final int[] dims, in Encoding encoding = connection.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) { @@ -392,6 +395,8 @@ private Class elementOidToClass(int oid) throws SQLException { case Oid.TEXT: case Oid.VARCHAR: return String.class; + case Oid.BOOL: + return Boolean.class; default: ArrayAssistant arrElemBuilder = ArrayAssistantRegistry.getAssistant(oid); if (arrElemBuilder != null) { @@ -900,11 +905,17 @@ public ResultSet getResultSetImpl(long index, int count, Map> m public String toString() { if (fieldString == null && fieldBytes != null) { try { - Object array = readBinaryArray(1,0); - java.sql.Array tmpArray = connection.createArrayOf(getBaseTypeName(), (Object[]) array); - fieldString = tmpArray.toString(); + Object array = readBinaryArray(1, 0); + + final PrimitiveArraySupport arraySupport = PrimitiveArraySupport.getArraySupport(array); + if (arraySupport != null) { + fieldString = arraySupport.toArrayString(connection.getTypeInfo().getArrayDelimiter(oid), array); + } else { + java.sql.Array tmpArray = connection.createArrayOf(getBaseTypeName(), (Object[]) array); + fieldString = tmpArray.toString(); + } } catch (SQLException e) { - fieldString = "NULL"; //punt + fieldString = "NULL"; // punt } } return fieldString; diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java index a0911908d7..9a0c508e6a 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java @@ -1156,7 +1156,12 @@ private static void appendArray(StringBuilder sb, Object elements, char delim) { if (o == null) { sb.append("NULL"); } else if (o.getClass().isArray()) { - appendArray(sb, o, delim); + 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); @@ -1271,6 +1276,49 @@ public Struct createStruct(String typeName, Object[] attributes) throws SQLExcep throw org.postgresql.Driver.notImplemented(this.getClass(), "createStruct(String, Object[])"); } + @Override + public Array createArrayOf(String typeName, Object elements) throws SQLException { + checkClosed(); + + final TypeInfo typeInfo = getTypeInfo(); + + final int oid = typeInfo.getPGArrayType(typeName); + final char delim = typeInfo.getArrayDelimiter(oid); + + 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); + } + + 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(); + } + + return makeArray(oid, arrayString); + } + @Override public Array createArrayOf(String typeName, Object[] elements) throws SQLException { checkClosed(); @@ -1283,6 +1331,10 @@ public Array createArrayOf(String typeName, Object[] elements) throws SQLExcepti PSQLState.INVALID_NAME); } + if (elements == null) { + return makeArray(oid, null); + } + char delim = getTypeInfo().getArrayDelimiter(oid); StringBuilder sb = new StringBuilder(); appendArray(sb, elements, delim); diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java index 28319be328..db0a3782d5 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java @@ -13,6 +13,7 @@ import org.postgresql.core.Query; import org.postgresql.core.QueryExecutor; import org.postgresql.core.ServerVersion; +import org.postgresql.core.TypeInfo; import org.postgresql.core.v3.BatchedQuery; import org.postgresql.largeobject.LargeObject; import org.postgresql.largeobject.LargeObjectManager; @@ -656,6 +657,8 @@ public void setObject(int parameterIndex, Object in, int targetSqlType, int scal 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}", @@ -681,6 +684,21 @@ public void setObject(int parameterIndex, Object in, int targetSqlType, int scal } } + private void setPrimitiveArray(int parameterIndex, A in) throws SQLException { + final PrimitiveArraySupport arrayToString = PrimitiveArraySupport.getArraySupport(in); + + final TypeInfo typeInfo = connection.getTypeInfo(); + + final int oid = arrayToString.getDefaultArrayTypeOid(typeInfo); + + if (arrayToString.supportBinaryRepresentation() && connection.getPreferQueryMode() != PreferQueryMode.SIMPLE) { + bindBytes(parameterIndex, arrayToString.toBinaryRepresentation(connection, in), oid); + } else { + final char delim = typeInfo.getArrayDelimiter(oid); + setString(parameterIndex, arrayToString.toArrayString(delim, in), oid); + } + } + private static String asString(final Clob in) throws SQLException { return in.getSubString(1, (int) in.length()); } @@ -942,6 +960,8 @@ public void setObject(int parameterIndex, Object x) throws SQLException { setMap(parameterIndex, (Map) x); } else if (x instanceof Number) { setNumber(parameterIndex, (Number) x); + } else if (PrimitiveArraySupport.isSupportedPrimitiveArray(x)) { + setPrimitiveArray(parameterIndex, x); } 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 new file mode 100644 index 0000000000..b92f01be01 --- /dev/null +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PrimitiveArraySupport.java @@ -0,0 +1,488 @@ +/* + * 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 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 PrimitiveArraySupport getArraySupport(A array) { + return ARRAY_CLASS_TO_SUPPORT.get(array.getClass()); + } +} diff --git a/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java b/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java index 73e56d616a..5464236cb9 100644 --- a/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java +++ b/pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java @@ -61,6 +61,19 @@ public static short int2(byte[] bytes, int idx) { return (short) (((bytes[idx] & 255) << 8) + ((bytes[idx + 1] & 255))); } + /** + * Parses a boolean value from the byte array. + * + * @param bytes + * The byte array to parse. + * @param idx + * The starting index to read from bytes. + * @return parsed boolean value. + */ + public static boolean bool(byte[] bytes, int idx) { + return bytes[idx] == 1; + } + /** * Parses a float value from the byte array. * @@ -127,6 +140,20 @@ public static void int2(byte[] target, int idx, int value) { target[idx + 1] = (byte) value; } + /** + * Encodes a boolean value to the byte array. + * + * @param target + * The byte array to encode to. + * @param idx + * The starting index in the byte array. + * @param value + * The value to encode. + */ + public static void bool(byte[] target, int idx, boolean value) { + target[idx] = value ? (byte) 1 : (byte) 0; + } + /** * Encodes a int value to the byte array. * diff --git a/pgjdbc/src/test/java/org/postgresql/jdbc/PrimitiveArraySupportTest.java b/pgjdbc/src/test/java/org/postgresql/jdbc/PrimitiveArraySupportTest.java new file mode 100644 index 0000000000..83b028bbce --- /dev/null +++ b/pgjdbc/src/test/java/org/postgresql/jdbc/PrimitiveArraySupportTest.java @@ -0,0 +1,253 @@ +/* + * 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/test/jdbc2/ArrayTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ArrayTest.java index 8aa30d0cd3..8e1874c949 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ArrayTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/ArrayTest.java @@ -5,12 +5,17 @@ package org.postgresql.test.jdbc2; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.postgresql.PGConnection; import org.postgresql.core.BaseConnection; import org.postgresql.geometric.PGbox; import org.postgresql.geometric.PGpoint; import org.postgresql.jdbc.PgArray; import org.postgresql.jdbc.PreferQueryMode; import org.postgresql.test.TestUtil; +import org.postgresql.util.PSQLException; import org.junit.Assert; import org.junit.Test; @@ -80,6 +85,122 @@ public void testSetNull() throws SQLException { pstmt.close(); } + @Test + public void testSetPrimitiveObjects() throws SQLException { + PreparedStatement pstmt = conn.prepareStatement("INSERT INTO arrtest VALUES (?,?,?)"); + pstmt.setObject(1, new int[]{1,2,3}, Types.ARRAY); + pstmt.setObject(2, new double[]{3.1d, 1.4d}, Types.ARRAY); + pstmt.setObject(3, new String[]{"abc", "f'a", "fa\"b"}, Types.ARRAY); + pstmt.executeUpdate(); + pstmt.close(); + + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT intarr, decarr, strarr FROM arrtest"); + Assert.assertTrue(rs.next()); + + Array arr = rs.getArray(1); + Assert.assertEquals(Types.INTEGER, arr.getBaseType()); + Integer[] intarr = (Integer[]) arr.getArray(); + assertEquals(3, intarr.length); + assertEquals(1, intarr[0].intValue()); + assertEquals(2, intarr[1].intValue()); + assertEquals(3, intarr[2].intValue()); + + arr = rs.getArray(2); + assertEquals(Types.NUMERIC, arr.getBaseType()); + BigDecimal[] decarr = (BigDecimal[]) arr.getArray(); + assertEquals(2, decarr.length); + assertEquals(new BigDecimal("3.1"), decarr[0]); + assertEquals(new BigDecimal("1.4"), decarr[1]); + + arr = rs.getArray(3); + assertEquals(Types.VARCHAR, arr.getBaseType()); + String[] strarr = (String[]) arr.getArray(2, 2); + assertEquals(2, strarr.length); + assertEquals("f'a", strarr[0]); + assertEquals("fa\"b", strarr[1]); + + rs.close(); + } + + @Test + public void testSetPrimitiveArraysObjects() throws SQLException { + PreparedStatement pstmt = conn.prepareStatement("INSERT INTO arrtest VALUES (?,?,?)"); + + final PGConnection arraySupport = conn.unwrap(PGConnection.class); + + pstmt.setArray(1, arraySupport.createArrayOf("int4", new int[] { 1, 2, 3 })); + pstmt.setObject(2, arraySupport.createArrayOf("float8", new double[] { 3.1d, 1.4d })); + pstmt.setObject(3, arraySupport.createArrayOf("varchar", new String[] { "abc", "f'a", "fa\"b" })); + + pstmt.executeUpdate(); + pstmt.close(); + + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT intarr, decarr, strarr FROM arrtest"); + Assert.assertTrue(rs.next()); + + Array arr = rs.getArray(1); + Assert.assertEquals(Types.INTEGER, arr.getBaseType()); + Integer[] intarr = (Integer[]) arr.getArray(); + Assert.assertEquals(3, intarr.length); + Assert.assertEquals(1, intarr[0].intValue()); + Assert.assertEquals(2, intarr[1].intValue()); + Assert.assertEquals(3, intarr[2].intValue()); + + arr = rs.getArray(2); + Assert.assertEquals(Types.NUMERIC, arr.getBaseType()); + BigDecimal[] decarr = (BigDecimal[]) arr.getArray(); + Assert.assertEquals(2, decarr.length); + Assert.assertEquals(new BigDecimal("3.1"), decarr[0]); + Assert.assertEquals(new BigDecimal("1.4"), decarr[1]); + + arr = rs.getArray(3); + Assert.assertEquals(Types.VARCHAR, arr.getBaseType()); + String[] strarr = (String[]) arr.getArray(2, 2); + Assert.assertEquals(2, strarr.length); + Assert.assertEquals("f'a", strarr[0]); + Assert.assertEquals("fa\"b", strarr[1]); + + try { + arraySupport.createArrayOf("int4", Integer.valueOf(1)); + fail("not an array"); + } catch (PSQLException e) { + + } + + rs.close(); + } + + @Test + public void testSetNullArrays() throws SQLException { + PreparedStatement pstmt = conn.prepareStatement("INSERT INTO arrtest VALUES (?,?,?)"); + + final PGConnection arraySupport = conn.unwrap(PGConnection.class); + + pstmt.setArray(1, arraySupport.createArrayOf("int4", null)); + pstmt.setObject(2, conn.createArrayOf("float8", null)); + pstmt.setObject(3, arraySupport.createArrayOf("varchar", null)); + + pstmt.executeUpdate(); + pstmt.close(); + + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT intarr, decarr, strarr FROM arrtest"); + Assert.assertTrue(rs.next()); + + Array arr = rs.getArray(1); + Assert.assertNull(arr); + + arr = rs.getArray(2); + Assert.assertNull(arr); + + arr = rs.getArray(3); + Assert.assertNull(arr); + + rs.close(); + } + @Test public void testRetrieveArrays() throws SQLException { Statement stmt = conn.createStatement(); 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 87a04e3ea3..e687cb61b5 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/Jdbc2TestSuite.java @@ -9,6 +9,7 @@ import org.postgresql.core.ReturningParserTest; import org.postgresql.core.v3.V3ParameterListTests; import org.postgresql.jdbc.DeepBatchedInsertStatementTest; +import org.postgresql.jdbc.PrimitiveArraySupportTest; import org.postgresql.test.core.JavaVersionTest; import org.postgresql.test.core.NativeQueryBindLengthTest; import org.postgresql.test.util.ExpressionPropertiesTest; @@ -61,6 +62,7 @@ ResultSetTest.class, ResultSetMetaDataTest.class, ArrayTest.class, + PrimitiveArraySupportTest.class, RefCursorTest.class, DateTest.class, 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 3d3640e3ae..dba8f899fd 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc4/ArrayTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc4/ArrayTest.java @@ -7,6 +7,7 @@ import org.postgresql.core.ServerVersion; import org.postgresql.geometric.PGbox; +import org.postgresql.jdbc.PgConnection; import org.postgresql.jdbc.PreferQueryMode; import org.postgresql.test.TestUtil; import org.postgresql.test.jdbc2.BaseTest4; @@ -27,6 +28,7 @@ import java.sql.Statement; import java.sql.Types; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.UUID; @@ -74,6 +76,22 @@ public void tearDown() throws SQLException { super.tearDown(); } + @Test + public void testCreateArrayOfBool() throws SQLException { + PreparedStatement pstmt = _conn.prepareStatement("SELECT ?::bool[]"); + pstmt.setArray(1, _conn.unwrap(PgConnection.class).createArrayOf("boolean", new boolean[] { true, true, false })); + + ResultSet rs = pstmt.executeQuery(); + Assert.assertTrue(rs.next()); + Array arr = rs.getArray(1); + Boolean[] out = (Boolean[]) arr.getArray(); + + Assert.assertEquals(3, out.length); + Assert.assertEquals(Boolean.TRUE, out[0]); + Assert.assertEquals(Boolean.TRUE, out[1]); + Assert.assertEquals(Boolean.FALSE, out[2]); + } + @Test public void testCreateArrayOfInt() throws SQLException { PreparedStatement pstmt = _conn.prepareStatement("SELECT ?::int[]"); @@ -150,7 +168,7 @@ public void testCreateArrayOfMultiJson() throws SQLException { PGobject p2 = new PGobject(); p2.setType("json"); p2.setValue("{\"x\": 20}"); - PGobject[] in = new PGobject[]{p1, p2}; + PGobject[] in = new PGobject[] { p1, p2 }; pstmt.setArray(1, _conn.createArrayOf("json", in)); ResultSet rs = pstmt.executeQuery(); @@ -315,27 +333,33 @@ public void testUUIDArray() throws SQLException { @Test public void testSetObjectFromJavaArray() throws SQLException { 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 (?)"); - // Incorrect, but commonly attempted by many ORMs: + //cannot handle generic Object[] try { - pstmt.setObject(1, strArray, Types.ARRAY); + pstmt.setObject(1, objCopy, Types.ARRAY); pstmt.executeUpdate(); Assert.fail("setObject() with a Java array parameter and Types.ARRAY shouldn't succeed"); } catch (org.postgresql.util.PSQLException ex) { // Expected failure. } - // Also incorrect, but commonly attempted by many ORMs: try { - pstmt.setObject(1, strArray); + pstmt.setObject(1, objCopy); pstmt.executeUpdate(); Assert.fail("setObject() with a Java array parameter and no Types argument shouldn't succeed"); } catch (org.postgresql.util.PSQLException ex) { // Expected failure. } + pstmt.setObject(1, strArray); + pstmt.executeUpdate(); + + pstmt.setObject(1, strArray, Types.ARRAY); + pstmt.executeUpdate(); + // Correct way, though the use of "text" as a type is non-portable. // Only supported for JDK 1.6 and JDBC4 Array sqlArray = _conn.createArrayOf("text", strArray); @@ -549,10 +573,11 @@ public void nullArray() throws SQLException { Assert.assertNull("null array should return null on getObject", getObject); } - @Test(expected = NullPointerException.class) + @Test public void createNullArray() throws SQLException { Array arr = con.createArrayOf("float8", null); - Assert.fail("createArrayOf(float8, null) should fail with NPE"); + Assert.assertNotNull(arr); + Assert.assertNull(arr.getArray()); } @Test