From 59589c39623b174e04a15516b7c05e0f85fd2359 Mon Sep 17 00:00:00 2001
From: Michael-A-McMahon
+ * Creates an {@link ArrayType} representing a user defined {@code ARRAY}
+ * type. The {@code name} passed to this method must identify the name of a
+ * user defined {@code ARRAY} type.
+ *
+ * Typically, the name passed to this method should be UPPER CASE, unless the
+ * {@code CREATE TYPE} command that created the type used an "enquoted" type
+ * name.
+ *
+ * The {@code ArrayType} object returned by this method may be used to a
+ * {@link Parameter} that binds an array value to a {@link Statement}.
+ * {@code
+ * Publisher
+ * @param name Name of a user defined ARRAY type. Not null.
+ * @return A {@code Type} object representing the user defined ARRAY type. Not
+ * null.
+ */
+ public static ArrayType arrayType(String name) {
+ return new ArrayTypeImpl(Objects.requireNonNull(name, "name is null"));
+ }
+
+ /**
+ * Extension of the standard {@link Type} interface used to represent user
+ * defined ARRAY types. An instance of {@code ArrayType} must be used when
+ * binding an array value to a {@link Statement} created by the Oracle R2DBC
+ * Driver.
+ *
+ * Oracle Database does not support an anonymous {@code ARRAY} type, which is + * what the standard {@link R2dbcType#COLLECTION} type represents. Oracle + * Database only supports {@code ARRAY} types which are declared as a user + * defined type, as in: + *
{@code + * CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER + * }+ * In order to bind an array, the name of a user defined ARRAY type must + * be known to Oracle R2DBC. Instances of {@code ArrayType} retain the name + * that is provided to the {@link #arrayType(String)} factory method. + */ + public interface ArrayType extends Type { + + /** + * {@inheritDoc} + * Returns {@code Object[].class}, which is the standard mapping for + * {@link R2dbcType#COLLECTION}. The true default type mapping is the array + * variant of the default mapping for the element type of the {@code ARRAY}. + * For instance, an {@code ARRAY} of {@code VARCHAR} maps to a + * {@code String[]} by default. + */ + @Override + Class> getJavaType(); + + /** + * {@inheritDoc} + * Returns the name of this user defined {@code ARRAY} type. For instance, + * this method returns "MY_ARRAY" if the type is declared as: + *
{@code + * CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER + * }+ */ + @Override + String getName(); + } + + /** Concrete implementation of the {@code ArrayType} interface */ + private static final class ArrayTypeImpl + extends TypeImpl implements ArrayType { + + /** + * Constructs an ARRAY type with the given {@code name}. The constructed + * {@code ArrayType} as a default Java type mapping of + * {@code Object[].class}. This is consistent with the standard + * {@link R2dbcType#COLLECTION} type. + * @param name User defined name of the type. Not null. + */ + ArrayTypeImpl(String name) { + super(Object[].class, name); + } + } + /** * Implementation of the {@link Type} SPI. */ - private static final class TypeImpl implements Type { + private static class TypeImpl implements Type { /** * The Java Language mapping of this SQL type. diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java index d00dbef..6ecdeb3 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java @@ -31,17 +31,41 @@ import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import io.r2dbc.spi.Type; +import oracle.jdbc.OracleArray; import oracle.r2dbc.OracleR2dbcTypes; import oracle.r2dbc.impl.ReactiveJdbcAdapter.JdbcReadable; import oracle.r2dbc.impl.ReadablesMetadata.OutParametersMetadataImpl; import oracle.r2dbc.impl.ReadablesMetadata.RowMetadataImpl; +import java.math.BigDecimal; import java.nio.ByteBuffer; +import java.sql.Array; +import java.sql.JDBCType; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.Time; import java.sql.Timestamp; +import java.sql.Types; +import java.time.Duration; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.Period; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Map; import java.util.NoSuchElementException; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.IntFunction; +import static java.lang.String.format; +import static oracle.r2dbc.impl.OracleR2dbcExceptions.fromJdbc; import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; +import static oracle.r2dbc.impl.OracleR2dbcExceptions.runJdbc; +import static oracle.r2dbc.impl.OracleR2dbcExceptions.toR2dbcException; /** *
@@ -208,6 +232,13 @@ else if (io.r2dbc.spi.Clob.class.equals(type)) { else if (LocalDateTime.class.equals(type)) { value = getLocalDateTime(index); } + else if (type.isArray() + && R2dbcType.COLLECTION.equals(readablesMetadata.get(index).getType())) { + // Note that a byte[] would be a valid mapping for a RAW column, so this + // branch is only taken if the target type is an array, and the column + // type is a SQL ARRAY (ie: COLLECTION). + value = getJavaArray(index, type.getComponentType()); + } else if (Object.class.equals(type)) { // Use the default type mapping if Object.class has been specified. // This method is invoked recursively with the default mapping, so long @@ -327,6 +358,292 @@ private LocalDateTime getLocalDateTime(int index) { } } + /** + *
+ * Converts the value of a column at the specified {@code index} to a Java + * array. + *
+ * JDBC drivers are not required to support conversion to Java arrays for any + * SQL type. However, an R2DBC driver must support conversions to Java arrays + * for ARRAY values. This method is implemented to handle that case. + *
+ * @param index 0 based column index + * @param javaType Type of elements stored in the array. Not null. + * @return A Java array, or {@code null} if the column value is null. + */ + private Object getJavaArray(int index, Class> javaType) { + final OracleArray jdbcArray = + jdbcReadable.getObject(index, OracleArray.class); + + if (jdbcArray == null) + return null; + + try { + // For arrays of primitive types, use API extensions on OracleArray. + if (boolean.class.equals(javaType)) { + // OracleArray does not support conversion to boolean[], so map shorts + // to booleans. + short[] shorts = jdbcArray.getShortArray(); + boolean[] booleans = new boolean[shorts.length]; + for (int i = 0; i < shorts.length; i++) + booleans[i] = shorts[i] != 0; + return booleans; + } + else if (byte.class.equals(javaType)) { + // OracleArray does not support conversion to byte[], so map shorts to + // bytes + short[] shorts = jdbcArray.getShortArray(); + byte[] bytes = new byte[shorts.length]; + for (int i = 0; i < shorts.length; i++) + bytes[i] = (byte)shorts[i]; + return bytes; + } + else if (short.class.equals(javaType)) { + return jdbcArray.getShortArray(); + } + else if (int.class.equals(javaType)) { + return jdbcArray.getIntArray(); + } + else if (long.class.equals(javaType)) { + return jdbcArray.getLongArray(); + } + else if (float.class.equals(javaType)) { + return jdbcArray.getFloatArray(); + } + else if (double.class.equals(javaType)) { + return jdbcArray.getDoubleArray(); + } + else { + // Check if Row.get(int/String) was called without a Class argument. + // In this case, the default mapping is declared as Object[] in + // SqlTypeMap, and this method gets called with Object.clas + if (Object.class.equals(javaType)) { + // The correct default mapping for the ARRAY is not actually Object[], + // it is the default mapping of the ARRAY's element type. + // If the element type is DATE, handle it as if it were TIMESTAMP. + // This is consistent with how DATE columns are usually handled, and + // reflects the fact that Oracle DATE values have a time component. + int jdbcType = jdbcArray.getBaseType(); + Type arrayType = SqlTypeMap.toR2dbcType( + jdbcType == Types.DATE ? Types.TIMESTAMP : jdbcType); + + // Use R2DBC's default type mapping, if the SQL type is recognized. + // Otherwise, leave the javaType as Object.class and Oracle JDBC's + // default type mapping will be used. + if (arrayType != null) + javaType = arrayType.getJavaType(); + } + + // Oracle JDBC seems to ignore the Map argument in many cases, and just + // maps values to their default JDBC type. The convertArray method + // should correct this by converting the array to the proper type. + return convertArray( + (Object[]) jdbcArray.getArray( + Map.of(jdbcArray.getBaseTypeName(), javaType)), + javaType); + } + } + catch (SQLException sqlException) { + throw toR2dbcException(sqlException); + } + finally { + runJdbc(jdbcArray::free); + } + } + + /** + * Converts a given {@code array} to an array of a specified + * {@code elementType}. This method handles arrays returned by + * {@link Array#getArray(Map)}, which contain objects that are the default + * type mapping for a JDBC driver. This method converts the default JDBC + * type mappings to the default R2DBC type mappings. + */ + @SuppressWarnings("unchecked") + private+ * Verifies the implementation of Java to SQL and SQL to Java type mappings + * for ARRAY types. The Oracle R2DBC Driver is expected to implement the type + * mapping listed in + * + * Table 9 of Section 14 of the R2DBC 1.0.0 Specification. + * + *
+ * An ARRAY is expected to map to the Java type mapping of it's element type. + * An ARRAY of VARCHAR should map to a Java array of String. + *
+ */ + @Test + public void testArrayCharacterTypeMappings() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE CHARACTER_ARRAY" + + " AS ARRAY(10) OF VARCHAR(100)") + .execute()); + Type characterArrayType = OracleR2dbcTypes.arrayType("CHARACTER_ARRAY"); + + // Expect ARRAY of VARCHAR and String[] to map + String[] greetings = {"Hello", "Bonjour", "你好", null}; + verifyTypeMapping( + connection, + Parameters.in(characterArrayType, greetings), + characterArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + greetings, + assertInstanceOf(String[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE CHARACTER_ARRAY")); + tryAwaitNone(connection.close()); + } + } + + /** + *+ * Verifies the implementation of Java to SQL and SQL to Java type mappings + * for ARRAY types. The Oracle R2DBC Driver is expected to implement the type + * mapping listed in + * + * Table 9 of Section 14 of the R2DBC 1.0.0 Specification. + * + *
+ * An ARRAY is expected to map to the Java type mapping of it's element type. + * An ARRAY of NUMBER should map to a Java array of BigDecimal, Byte, Short, + * Integer, Long, Float, and Double. + *
+ */ + @Test + public void testArrayNumericTypeMappings() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE NUMBER_ARRAY" + + " AS ARRAY(10) OF NUMBER") + .execute()); + Type numberArrayType = OracleR2dbcTypes.arrayType("NUMBER_ARRAY"); + + // Expect ARRAY of NUMBER and BigDecimal[] to map + BigDecimal[] bigDecimals = new BigDecimal[] { + BigDecimal.ZERO, + new BigDecimal("1.23"), + new BigDecimal("4.56"), + new BigDecimal("7.89"), + null + }; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, bigDecimals), + numberArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + bigDecimals, + assertInstanceOf(BigDecimal[].class, rowValue))); + + // Expect ARRAY of NUMBER and byte[] to map + byte[] bytes = new byte[]{1,2,3,4,5}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, bytes), + numberArrayType.getName(), + row -> row.get(0, byte[].class), + (ignored, rowValue) -> + assertArrayEquals( + bytes, + assertInstanceOf(byte[].class, rowValue))); + + // Expect ARRAY of NUMBER and short[] to map + short[] shorts = new short[]{1,2,3,4,5}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, shorts), + numberArrayType.getName(), + row -> row.get(0, short[].class), + (ignored, rowValue) -> + assertArrayEquals( + shorts, + assertInstanceOf(short[].class, rowValue))); + + // Expect ARRAY of NUMBER and int[] to map + int[] ints = {1,2,3,4,5}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, ints), + numberArrayType.getName(), + row -> row.get(0, int[].class), + (ignored, rowValue) -> + assertArrayEquals( + ints, + assertInstanceOf(int[].class, rowValue))); + + // Expect ARRAY of NUMBER and long[] to map + long[] longs = {1,2,3,4,5}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, longs), + numberArrayType.getName(), + row -> row.get(0, long[].class), + (ignored, rowValue) -> + assertArrayEquals( + longs, + assertInstanceOf(long[].class, rowValue))); + + // Expect ARRAY of NUMBER and float[] to map + float[] floats = {1.1f,2.2f,3.3f,4.4f,5.5f}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, floats), + numberArrayType.getName(), + row -> row.get(0, float[].class), + (ignored, rowValue) -> + assertArrayEquals( + floats, + assertInstanceOf(float[].class, rowValue))); + + // Expect ARRAY of NUMBER and double[] to map + double[] doubles = {1.1,2.2,3.3,4.4,5.5}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, doubles), + numberArrayType.getName(), + row -> row.get(0, double[].class), + (ignored, rowValue) -> + assertArrayEquals( + doubles, + assertInstanceOf(double[].class, rowValue))); + + // Expect ARRAY of NUMBER and Byte[] to map + Byte[] byteObjects = new Byte[]{1,2,3,4,5,null}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, byteObjects), + numberArrayType.getName(), + row -> row.get(0, Byte[].class), + (ignored, rowValue) -> + assertArrayEquals( + byteObjects, + assertInstanceOf(Byte[].class, rowValue))); + + // Expect ARRAY of NUMBER and Short[] to map + Short[] shortObjects = new Short[]{1,2,3,4,5,null}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, shortObjects), + numberArrayType.getName(), + row -> row.get(0, Short[].class), + (ignored, rowValue) -> + assertArrayEquals( + shortObjects, + assertInstanceOf(Short[].class, rowValue))); + + // Expect ARRAY of NUMBER and Integer[] to map + Integer[] intObjects = {1,2,3,4,5,null}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, intObjects), + numberArrayType.getName(), + row -> row.get(0, Integer[].class), + (ignored, rowValue) -> + assertArrayEquals( + intObjects, + assertInstanceOf(Integer[].class, rowValue))); + + // Expect ARRAY of NUMBER and Long[] to map + Long[] longObjects = {1L,2L,3L,4L,5L,null}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, longObjects), + numberArrayType.getName(), + row -> row.get(0, Long[].class), + (ignored, rowValue) -> + assertArrayEquals( + longObjects, + assertInstanceOf(Long[].class, rowValue))); + + // Expect ARRAY of NUMBER and Float[] to map + Float[] floatObjects = {1.1f,2.2f,3.3f,4.4f,5.5f,null}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, floatObjects), + numberArrayType.getName(), + row -> row.get(0, Float[].class), + (ignored, rowValue) -> + assertArrayEquals( + floatObjects, + assertInstanceOf(Float[].class, rowValue))); + + // Expect ARRAY of NUMBER and Double[] to map + Double[] doubleObjects = {1.1,2.2,3.3,4.4,5.5,null}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, doubleObjects), + numberArrayType.getName(), + row -> row.get(0, Double[].class), + (ignored, rowValue) -> + assertArrayEquals( + doubleObjects, + assertInstanceOf(Double[].class, rowValue))); + } + finally { + tryAwaitNone(connection.close()); + } + } + + /** + *+ * Verifies the implementation of Java to SQL and SQL to Java type mappings + * for ARRAY types. The Oracle R2DBC Driver is expected to implement the type + * mapping listed in + * + * Table 9 of Section 14 of the R2DBC 1.0.0 Specification. + * + *
+ * An ARRAY is expected to map to the Java type mapping of it's element type. + * An ARRAY of NUMBER should map to a Java array of boolean. + *
+ */ + @Test + public void testArrayBooleanTypeMappings() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE BOOLEAN_ARRAY" + + " AS ARRAY(10) OF NUMBER") + .execute()); + Type booleanArrayType = OracleR2dbcTypes.arrayType("BOOLEAN_ARRAY"); + + // Expect ARRAY of NUMBER and boolean[] to map + boolean[] booleans = {true, false, false, true}; + verifyTypeMapping( + connection, + Parameters.in(booleanArrayType, booleans), + booleanArrayType.getName(), + row -> row.get(0, boolean[].class), + (ignored, rowValue) -> + assertArrayEquals( + booleans, + assertInstanceOf(boolean[].class, rowValue))); + + // Expect ARRAY of NUMBER and Boolean[] to map + Boolean[] booleanObjects = {true, false, false, true, null}; + verifyTypeMapping( + connection, + Parameters.in(booleanArrayType, booleanObjects), + booleanArrayType.getName(), + row -> row.get(0, Boolean[].class), + (ignored, rowValue) -> + assertArrayEquals( + booleanObjects, + assertInstanceOf(Boolean[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE BOOLEAN_ARRAY")); + tryAwaitNone(connection.close()); + } + } + + /** + *+ * Verifies the implementation of Java to SQL and SQL to Java type mappings + * for ARRAY types. The Oracle R2DBC Driver is expected to implement the type + * mapping listed in + * + * Table 9 of Section 14 of the R2DBC 1.0.0 Specification. + * + *
+ * An ARRAY is expected to map to the Java type mapping of it's element type. + * An ARRAY of RAW should map to a Java array of ByteBuffer. + *
+ */ + @Test + public void testArrayBinaryTypeMappings() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE BINARY_ARRAY" + + " AS ARRAY(10) OF RAW(100)") + .execute()); + Type binaryArrayType = OracleR2dbcTypes.arrayType("BINARY_ARRAY"); + + // Expect ARRAY of RAW and ByteBuffer[] to map + ByteBuffer[] byteBuffers = { + ByteBuffer.allocate(3 * Integer.BYTES) + .putInt(0).putInt(1).putInt(2).clear(), + ByteBuffer.allocate(3 * Integer.BYTES) + .putInt(3).putInt(4).putInt(5).clear(), + ByteBuffer.allocate(3 * Integer.BYTES) + .putInt(6).putInt(7).putInt(8).clear(), + null + }; + verifyTypeMapping( + connection, + Parameters.in(binaryArrayType, byteBuffers), + binaryArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + byteBuffers, + assertInstanceOf(ByteBuffer[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE BINARY_ARRAY")); + tryAwaitNone(connection.close()); + } + } + + /** + *+ * Verifies the implementation of Java to SQL and SQL to Java type mappings + * for ARRAY types. The Oracle R2DBC Driver is expected to implement the type + * mapping listed in + * + * Table 9 of Section 14 of the R2DBC 1.0.0 Specification. + * + *
+ * An ARRAY is expected to map to the Java type mapping of it's element type. + * An ARRAY of DATE should map to a Java array of LocalDateTime (Oracle DATE + * values have a time component). + * An ARRAY of TIMESTAMP should map to a Java array of LocalDateTime + * An ARRAY of TIMESTAMP WITH TIME ZONE should map to a Java array of + * OffsetDateTime. + * An ARRAY of TIMESTAMP WITH LOCAL TIME ZONE should map to a Java array of + * LocalDateTime. + * An ARRAY of INTERVAL YEAR TO MONTH should map to a Java array of Period + * An ARRAY of INTERVAL DAY TO SECOND should map to a Java array of Duration + *
+ */ + @Test + public void testArrayDatetimeTypeMappings() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + OffsetDateTime dateTimeValue = + OffsetDateTime.of(2038, 10, 23, 9, 42, 1, 1, ZoneOffset.ofHours(-5)); + + // Expect ARRAY of DATE and LocalDateTime[] to map + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE DATE_ARRAY" + + " AS ARRAY(10) OF DATE") + .execute()); + try { + Type dateArrayType = OracleR2dbcTypes.arrayType("DATE_ARRAY"); + + LocalDateTime[] localDateTimes = { + dateTimeValue.minus(Period.ofDays(7)) + .toLocalDateTime() + .truncatedTo(ChronoUnit.SECONDS), + dateTimeValue.toLocalDateTime() + .truncatedTo(ChronoUnit.SECONDS), + dateTimeValue.plus(Period.ofDays(7)) + .toLocalDateTime() + .truncatedTo(ChronoUnit.SECONDS), + null + }; + verifyTypeMapping( + connection, + Parameters.in(dateArrayType, localDateTimes), + dateArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + localDateTimes, + assertInstanceOf(LocalDateTime[].class, rowValue))); + + LocalDate[] localDates = + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null ? null : localDateTime.toLocalDate()) + .toArray(LocalDate[]::new); + verifyTypeMapping( + connection, + Parameters.in(dateArrayType, localDateTimes), + dateArrayType.getName(), + row -> row.get(0, LocalDate[].class), + (ignored, rowValue) -> + assertArrayEquals( + localDates, + assertInstanceOf(LocalDate[].class, rowValue))); + + LocalTime[] localTimes = + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null ? null : localDateTime.toLocalTime()) + .toArray(LocalTime[]::new); + verifyTypeMapping( + connection, + Parameters.in(dateArrayType, localDateTimes), + dateArrayType.getName(), + row -> row.get(0, LocalTime[].class), + (ignored, rowValue) -> + assertArrayEquals( + localTimes, + assertInstanceOf(LocalTime[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE DATE_ARRAY")); + } + + + // Expect ARRAY of TIMESTAMP and LocalDateTime[] to map + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_ARRAY" + + " AS ARRAY(10) OF TIMESTAMP") + .execute()); + try { + Type timestampArrayType = OracleR2dbcTypes.arrayType("TIMESTAMP_ARRAY"); + + LocalDateTime[] localDateTimes = { + dateTimeValue.minus(Duration.ofMillis(100)) + .toLocalDateTime(), + dateTimeValue.toLocalDateTime(), + dateTimeValue.plus(Duration.ofMillis(100)) + .toLocalDateTime(), + null + }; + + verifyTypeMapping( + connection, + Parameters.in(timestampArrayType, localDateTimes), + timestampArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + localDateTimes, + assertInstanceOf(LocalDateTime[].class, rowValue))); + + LocalDate[] localDates = + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null ? null : localDateTime.toLocalDate()) + .toArray(LocalDate[]::new); + verifyTypeMapping( + connection, + Parameters.in(timestampArrayType, localDateTimes), + timestampArrayType.getName(), + row -> row.get(0, LocalDate[].class), + (ignored, rowValue) -> + assertArrayEquals( + localDates, + assertInstanceOf(LocalDate[].class, rowValue))); + + LocalTime[] localTimes = + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null ? null : localDateTime.toLocalTime()) + .toArray(LocalTime[]::new); + verifyTypeMapping( + connection, + Parameters.in(timestampArrayType, localDateTimes), + timestampArrayType.getName(), + row -> row.get(0, LocalTime[].class), + (ignored, rowValue) -> + assertArrayEquals( + localTimes, + assertInstanceOf(LocalTime[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_ARRAY")); + } + + // Expect ARRAY of TIMESTAMP WITH TIME ZONE and OffsetDateTime[] to map + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY" + + " AS ARRAY(10) OF TIMESTAMP WITH TIME ZONE") + .execute()); + try { + Type timestampWithTimeZoneArrayType = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_TIME_ZONE_ARRAY"); + + OffsetDateTime[] offsetDateTimes = { + dateTimeValue.minus(Duration.ofMillis(100)) + .toLocalDateTime() + .atOffset(ZoneOffset.ofHours(-8)), + dateTimeValue.toLocalDateTime() + .atOffset(ZoneOffset.ofHours(0)), + dateTimeValue.plus(Duration.ofMillis(100)) + .toLocalDateTime() + .atOffset(ZoneOffset.ofHours(8)), + null + }; + + verifyTypeMapping( + connection, + Parameters.in(timestampWithTimeZoneArrayType, offsetDateTimes), + timestampWithTimeZoneArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + offsetDateTimes, + assertInstanceOf(OffsetDateTime[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY")); + } + + // Expect ARRAY of TIMESTAMP WITH LOCAL TIME ZONE and LocalDateTime[] to + // map + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY" + + " AS ARRAY(10) OF TIMESTAMP WITH LOCAL TIME ZONE") + .execute()); + try { + Type timestampWithLocalTimeZoneArrayType = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY"); + LocalDateTime[] localDateTimes = { + dateTimeValue.minus(Duration.ofMillis(100)) + .toLocalDateTime(), + dateTimeValue.toLocalDateTime(), + dateTimeValue.plus(Duration.ofMillis(100)) + .toLocalDateTime(), + null + }; + + verifyTypeMapping( + connection, + Parameters.in(timestampWithLocalTimeZoneArrayType, localDateTimes), + timestampWithLocalTimeZoneArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + localDateTimes, + assertInstanceOf(LocalDateTime[].class, rowValue))); + + LocalDate[] localDates = + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null ? null : localDateTime.toLocalDate()) + .toArray(LocalDate[]::new); + verifyTypeMapping( + connection, + Parameters.in(timestampWithLocalTimeZoneArrayType, localDateTimes), + timestampWithLocalTimeZoneArrayType.getName(), + row -> row.get(0, LocalDate[].class), + (ignored, rowValue) -> + assertArrayEquals( + localDates, + assertInstanceOf(LocalDate[].class, rowValue))); + + LocalTime[] localTimes = + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null ? null : localDateTime.toLocalTime()) + .toArray(LocalTime[]::new); + verifyTypeMapping( + connection, + Parameters.in(timestampWithLocalTimeZoneArrayType, localDateTimes), + timestampWithLocalTimeZoneArrayType.getName(), + row -> row.get(0, LocalTime[].class), + (ignored, rowValue) -> + assertArrayEquals( + localTimes, + assertInstanceOf(LocalTime[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY")); + } + + // Expect ARRAY of INTERVAL YEAR TO MONTH and Period[] to map + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE INTERVAL_YEAR_TO_MONTH_ARRAY" + + " AS ARRAY(10) OF INTERVAL YEAR TO MONTH") + .execute()); + try { + Type intervalYearToMonthArrayType = + OracleR2dbcTypes.arrayType("INTERVAL_YEAR_TO_MONTH_ARRAY"); + + Period[] periods = { + Period.of(1, 2, 0), + Period.of(3, 4, 0), + Period.of(5, 6, 0), + null + }; + + verifyTypeMapping( + connection, + Parameters.in(intervalYearToMonthArrayType, periods), + intervalYearToMonthArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + periods, + assertInstanceOf(Period[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE INTERVAL_YEAR_TO_MONTH_ARRAY")); + } + + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE INTERVAL_DAY_TO_SECOND_ARRAY" + + " AS ARRAY(10) OF INTERVAL DAY TO SECOND") + .execute()); + try { + Type intervalDayToSecondArrayType = + OracleR2dbcTypes.arrayType("INTERVAL_DAY_TO_SECOND_ARRAY"); + Duration[] durations = { + Duration.ofDays(1), + Duration.ofHours(2), + Duration.ofMinutes(3), + Duration.ofSeconds(4), + null + }; + verifyTypeMapping( + connection, + Parameters.in(intervalDayToSecondArrayType, durations), + intervalDayToSecondArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + durations, + assertInstanceOf(Duration[].class, rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE INTERVAL_DAY_TO_SECOND_ARRAY")); + } + } + finally { + tryAwaitNone(connection.close()); + } + } + + /** + *+ * Verifies the implementation of Java to SQL and SQL to Java type mappings + * for STRUCT types. The Oracle R2DBC Driver is expected to allow OBJECT + * values to be mapped to JDBC's {@link java.sql.Struct} type. The current + * R2DBC specification does not define a default mapping for OBJECT values, so + * the Oracle R2DBC driver does not implement one. + *
+ */ + @Test + public void testObjectTypeMappings() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE CHARACTER_ARRAY" + + " AS ARRAY(10) OF VARCHAR(100)") + .execute()); + Type characterArrayType = OracleR2dbcTypes.arrayType("CHARACTER_ARRAY"); + + // Expect ARRAY of VARCHAR and String[] to map + String[] greetings = {"Hello", "Bonjour", "你好"}; + verifyTypeMapping( + connection, + Parameters.in(characterArrayType, greetings), + characterArrayType.getName(), + (ignored, rowValue) -> + assertArrayEquals( + greetings, + assertInstanceOf(String[].class, rowValue))); + + // Expect ARRAY of NUMBER and int[] to map + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE NUMBER_ARRAY" + + " AS ARRAY(10) OF NUMBER") + .execute()); + Type numberArrayType = OracleR2dbcTypes.arrayType("NUMBER_ARRAY"); + int[] numbers = {1, 2, 3, 4}; + verifyTypeMapping( + connection, + Parameters.in(numberArrayType, numbers), + numberArrayType.getName(), + row -> row.get(0, int[].class), + (ignored, rowValue) -> + assertArrayEquals( + numbers, + assertInstanceOf(int[].class, rowValue))); + } + finally { + tryAwaitNone(connection.close()); + } + } + // TODO: More tests for JDBC 4.3 mappings like BigInteger to BIGINT, // java.sql.Date to DATE, java.sql.Blob to BLOB? Oracle R2DBC exposes all // type mappings supported by Oracle JDBC. @@ -565,10 +1279,20 @@ private static- * Verifies the implementation of Java to SQL and SQL to Java type mappings - * for STRUCT types. The Oracle R2DBC Driver is expected to allow OBJECT - * values to be mapped to JDBC's {@link java.sql.Struct} type. The current - * R2DBC specification does not define a default mapping for OBJECT values, so - * the Oracle R2DBC driver does not implement one. - *
- */ - @Test - public void testObjectTypeMappings() { - Connection connection = - Mono.from(sharedConnection()).block(connectTimeout()); - try { - awaitOne(connection.createStatement( - "CREATE OR REPLACE TYPE CHARACTER_ARRAY" + - " AS ARRAY(10) OF VARCHAR(100)") - .execute()); - Type characterArrayType = OracleR2dbcTypes.arrayType("CHARACTER_ARRAY"); - - // Expect ARRAY of VARCHAR and String[] to map - String[] greetings = {"Hello", "Bonjour", "你好"}; - verifyTypeMapping( - connection, - Parameters.in(characterArrayType, greetings), - characterArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - greetings, - assertInstanceOf(String[].class, rowValue))); - - // Expect ARRAY of NUMBER and int[] to map - awaitOne(connection.createStatement( - "CREATE OR REPLACE TYPE NUMBER_ARRAY" + - " AS ARRAY(10) OF NUMBER") - .execute()); - Type numberArrayType = OracleR2dbcTypes.arrayType("NUMBER_ARRAY"); - int[] numbers = {1, 2, 3, 4}; - verifyTypeMapping( - connection, - Parameters.in(numberArrayType, numbers), - numberArrayType.getName(), - row -> row.get(0, int[].class), - (ignored, rowValue) -> - assertArrayEquals( - numbers, - assertInstanceOf(int[].class, rowValue))); - } - finally { - tryAwaitNone(connection.close()); - } - } - // TODO: More tests for JDBC 4.3 mappings like BigInteger to BIGINT, // java.sql.Date to DATE, java.sql.Blob to BLOB? Oracle R2DBC exposes all // type mappings supported by Oracle JDBC. From 6d64322be5a611fbd848a4e5d2d29db9aa6ba389 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon* Constructs a new {@code Readable} that supplies values of a * {@code jdbcReadable} and obtains metadata of the values from * {@code resultMetadata}. *
+ * @param jdbcConnection JDBC connection that created the + * {@code jdbcReadable}. Not null. * @param jdbcReadable Readable values from a JDBC Driver. Not null. * @param readablesMetadata Metadata of each value. Not null. * @param adapter Adapts JDBC calls into reactive streams. Not null. */ private OracleReadableImpl( - JdbcReadable jdbcReadable, ReadablesMetadata> readablesMetadata, + java.sql.Connection jdbcConnection, JdbcReadable jdbcReadable, + ReadablesMetadata> readablesMetadata, ReactiveJdbcAdapter adapter) { + this.jdbcConnection = jdbcConnection; this.jdbcReadable = jdbcReadable; this.readablesMetadata = readablesMetadata; this.adapter = adapter; @@ -113,6 +118,8 @@ private OracleReadableImpl( * provided {@code jdbcReadable} and {@code metadata}. The metadata * object is used to determine the default type mapping of column values. * + * @param jdbcConnection JDBC connection that created the + * {@code jdbcReadable}. Not null. * @param jdbcReadable Row data from the Oracle JDBC Driver. Not null. * @param metadata Meta-data for the specified row. Not null. * @param adapter Adapts JDBC calls into reactive streams. Not null. @@ -120,9 +127,9 @@ private OracleReadableImpl( * {@code metadata}. Not null. */ static Row createRow( - JdbcReadable jdbcReadable, RowMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { - return new RowImpl(jdbcReadable, metadata, adapter); + java.sql.Connection jdbcConnection, JdbcReadable jdbcReadable, + RowMetadataImpl metadata, ReactiveJdbcAdapter adapter) { + return new RowImpl(jdbcConnection, jdbcReadable, metadata, adapter); } /** *@@ -130,6 +137,8 @@ static Row createRow( * the provided {@code jdbcReadable} and {@code rowMetadata}. The metadata * object is used to determine the default type mapping of column values. *
+ * @param jdbcConnection JDBC connection that created the + * {@code jdbcReadable}. Not null. * @param jdbcReadable Row data from the Oracle JDBC Driver. Not null. * @param metadata Meta-data for the specified row. Not null. * @param adapter Adapts JDBC calls into reactive streams. Not null. @@ -137,9 +146,9 @@ static Row createRow( * {@code metadata}. Not null. */ static OutParameters createOutParameters( - JdbcReadable jdbcReadable, OutParametersMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { - return new OutParametersImpl(jdbcReadable, metadata, adapter); + java.sql.Connection jdbcConnection, JdbcReadable jdbcReadable, + OutParametersMetadataImpl metadata, ReactiveJdbcAdapter adapter) { + return new OutParametersImpl(jdbcConnection, jdbcReadable, metadata, adapter); } /** @@ -372,179 +381,255 @@ private LocalDateTime getLocalDateTime(int index) { * @return A Java array, or {@code null} if the column value is null. */ private Object getJavaArray(int index, Class> javaType) { - final OracleArray jdbcArray = - jdbcReadable.getObject(index, OracleArray.class); + OracleArray oracleArray = jdbcReadable.getObject(index, OracleArray.class); + + return oracleArray == null + ? null + : convertOracleArray(oracleArray, javaType); + } + + private Object convertOracleArray( + OracleArray oracleArray, Class> javaType) { + try { + // Convert to primitive array types using API extensions on OracleArray + if (javaType.isPrimitive()) + return convertPrimitiveArray(oracleArray, javaType); + + // Check if Row.get(int/String) was called without a Class argument. + // In this case, the default mapping is declared as Object[] in + // SqlTypeMap, and this method gets called with Object.class. A default + // mapping is used in this case. + Class> convertedType = getJavaArrayType(oracleArray, javaType); + + // Attempt to have Oracle JDBC convert to the desired type + Object[] javaArray = + (Object[]) oracleArray.getArray( + Map.of(oracleArray.getBaseTypeName(), convertedType)); + + // Oracle JDBC may ignore the Map argument in many cases, and just + // maps values to their default JDBC type. The convertArray method + // will correct this by converting the array to the desired type. + return convertArray(javaArray, convertedType); + } + catch (SQLException sqlException) { + throw toR2dbcException(sqlException); + } + finally { + runJdbc(oracleArray::free); + } + } - if (jdbcArray == null) - return null; + /** + * Returns the Java array type that an ARRAY will be mapped to. If the + * {@code javaType} argument is {@code Object.class}, then this method returns + * a default mapping based on the element type of the ARRAY. Otherwise, this + * method just returns the {@code javaType}. + */ + private Class> getJavaArrayType( + OracleArray oracleArray, Class> javaType) { + + if (!Object.class.equals(javaType)) + return javaType; + + // Determine a default Java type mapping for the element type of the ARRAY. + // If the element type is DATE, handle it as if it were TIMESTAMP. + // This is consistent with how DATE columns are usually handled, and + // reflects the fact that Oracle DATE values have a time component. + int jdbcType = fromJdbc(oracleArray::getBaseType); + if (jdbcType == Types.ARRAY) { + + Object[] oracleArrays = (Object[]) fromJdbc(oracleArray::getArray); + + // TODO: It should be possible to determine the type of next ARRAY + // dimension by calling + // OracleConnection.createArray( + // oracleArray.getSQLTypeName(), new Object[0]).getBaseType()) + final OracleArray oracleArrayElement; + if (oracleArrays.length > 0) { + oracleArrayElement = (OracleArray) oracleArrays[0]; + } + else { + // Need to create an instance of the OracleArray base type in order to + // know its own base type. The information for the base type ARRAY + // should already be cached by Oracle JDBC, as the type info was + // retrieved when value was returned from the database. If the + // information is cached, then createOracleArray should not perform a + // blocking call. + oracleArrayElement = (OracleArray) fromJdbc(() -> + jdbcConnection.unwrap(OracleConnection.class) + .createOracleArray(oracleArray.getBaseTypeName(), new Object[0])); + } + return java.lang.reflect.Array.newInstance( + getJavaArrayType(oracleArrayElement, Object.class), 0) + .getClass(); + } + + Type r2dbcType = SqlTypeMap.toR2dbcType( + jdbcType == Types.DATE ? Types.TIMESTAMP : jdbcType); + + // Use R2DBC's default type mapping, if the SQL type is recognized. + // Otherwise, leave the javaType as Object.class and Oracle JDBC's + // default type mapping will be used. + return r2dbcType != null + ? r2dbcType.getJavaType() + : javaType; + } + + private Object convertPrimitiveArray( + OracleArray oracleArray, Class> primitiveType) { try { - // For arrays of primitive types, use API extensions on OracleArray. - if (boolean.class.equals(javaType)) { + if (boolean.class.equals(primitiveType)) { // OracleArray does not support conversion to boolean[], so map shorts // to booleans. - short[] shorts = jdbcArray.getShortArray(); + short[] shorts = oracleArray.getShortArray(); boolean[] booleans = new boolean[shorts.length]; for (int i = 0; i < shorts.length; i++) booleans[i] = shorts[i] != 0; return booleans; } - else if (byte.class.equals(javaType)) { + else if (byte.class.equals(primitiveType)) { // OracleArray does not support conversion to byte[], so map shorts to // bytes - short[] shorts = jdbcArray.getShortArray(); + short[] shorts = oracleArray.getShortArray(); byte[] bytes = new byte[shorts.length]; for (int i = 0; i < shorts.length; i++) - bytes[i] = (byte)shorts[i]; + bytes[i] = (byte) shorts[i]; return bytes; } - else if (short.class.equals(javaType)) { - return jdbcArray.getShortArray(); + else if (short.class.equals(primitiveType)) { + return oracleArray.getShortArray(); } - else if (int.class.equals(javaType)) { - return jdbcArray.getIntArray(); + else if (int.class.equals(primitiveType)) { + return oracleArray.getIntArray(); } - else if (long.class.equals(javaType)) { - return jdbcArray.getLongArray(); + else if (long.class.equals(primitiveType)) { + return oracleArray.getLongArray(); } - else if (float.class.equals(javaType)) { - return jdbcArray.getFloatArray(); + else if (float.class.equals(primitiveType)) { + return oracleArray.getFloatArray(); } - else if (double.class.equals(javaType)) { - return jdbcArray.getDoubleArray(); + else if (double.class.equals(primitiveType)) { + return oracleArray.getDoubleArray(); } else { - // Check if Row.get(int/String) was called without a Class argument. - // In this case, the default mapping is declared as Object[] in - // SqlTypeMap, and this method gets called with Object.clas - if (Object.class.equals(javaType)) { - // The correct default mapping for the ARRAY is not actually Object[], - // it is the default mapping of the ARRAY's element type. - // If the element type is DATE, handle it as if it were TIMESTAMP. - // This is consistent with how DATE columns are usually handled, and - // reflects the fact that Oracle DATE values have a time component. - int jdbcType = jdbcArray.getBaseType(); - Type arrayType = SqlTypeMap.toR2dbcType( - jdbcType == Types.DATE ? Types.TIMESTAMP : jdbcType); - - // Use R2DBC's default type mapping, if the SQL type is recognized. - // Otherwise, leave the javaType as Object.class and Oracle JDBC's - // default type mapping will be used. - if (arrayType != null) - javaType = arrayType.getJavaType(); - } - - // Oracle JDBC seems to ignore the Map argument in many cases, and just - // maps values to their default JDBC type. The convertArray method - // should correct this by converting the array to the proper type. - return convertArray( - (Object[]) jdbcArray.getArray( - Map.of(jdbcArray.getBaseTypeName(), javaType)), - javaType); + // Attempt to have Oracle JDBC convert the array + Object javaArray = oracleArray.getArray( + Map.of(oracleArray.getSQLTypeName(), primitiveType)); + + // Check if Oracle JDBC ignored the Map argument or not + Class> javaArrayType = javaArray.getClass().getComponentType(); + if (primitiveType.equals(javaArrayType)) + return javaArray; + + throw unsupportedArrayConversion(javaArrayType, primitiveType); } } catch (SQLException sqlException) { throw toR2dbcException(sqlException); } - finally { - runJdbc(jdbcArray::free); - } } /** * Converts a given {@code array} to an array of a specified - * {@code elementType}. This method handles arrays returned by - * {@link Array#getArray(Map)}, which contain objects that are the default - * type mapping for a JDBC driver. This method converts the default JDBC - * type mappings to the default R2DBC type mappings. + * {@code desiredType}. This method handles arrays returned by + * {@link Array#getArray(Map)}, which may contain objects that are the default + * desiredType mapping for a JDBC driver. This method converts the default JDBC + * desiredType mappings to the default R2DBC desiredType mappings. */ @SuppressWarnings("unchecked") - private- * The {@code ArrayType} object returned by this method may be used to a - * {@link Parameter} that binds an array value to a {@link Statement}. + * The {@code ArrayType} object returned by this method may be used to create + * a {@link Parameter} that binds an array value to a {@link Statement}. *
{@code * PublisherarrayBindExample(Connection connection) { * Statement statement = diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java index 968935c..8a2f9b9 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java @@ -36,7 +36,6 @@ import oracle.r2dbc.impl.ReactiveJdbcAdapter.JdbcReadable; import oracle.r2dbc.impl.ReadablesMetadata.OutParametersMetadataImpl; import oracle.r2dbc.impl.ReadablesMetadata.RowMetadataImpl; -import oracle.sql.DatumWithConnection; import java.math.BigDecimal; import java.nio.ByteBuffer; @@ -388,6 +387,15 @@ private Object getJavaArray(int index, Class> javaType) { : convertOracleArray(oracleArray, javaType); } + /** + * Converts an {@code OracleArray} from Oracle JDBC into the Java array + * variant of the specified {@code javaType}. If the {@code javaType} is + * {@code Object.class}, this method converts to the R2DBC standard mapping + * for the SQL type of the ARRAY elements. + * @param oracleArray Array from Oracle JDBC. Not null. + * @param javaType Type to convert to. Not null. + * @return The converted array. + */ private Object convertOracleArray( OracleArray oracleArray, Class> javaType) { try { @@ -399,7 +407,7 @@ private Object convertOracleArray( // In this case, the default mapping is declared as Object[] in // SqlTypeMap, and this method gets called with Object.class. A default // mapping is used in this case. - Class> convertedType = getJavaArrayType(oracleArray, javaType); + Class> convertedType = getArrayTypeMapping(oracleArray, javaType); // Attempt to have Oracle JDBC convert to the desired type Object[] javaArray = @@ -420,51 +428,55 @@ private Object convertOracleArray( } /** - * Returns the Java array type that an ARRAY will be mapped to. If the - * {@code javaType} argument is {@code Object.class}, then this method returns - * a default mapping based on the element type of the ARRAY. Otherwise, this - * method just returns the {@code javaType}. + * Returns the Java array type that an ARRAY will be mapped to. This method + * is used to determine a default type mapping when {@link #get(int)} or + * {@link #get(String)} are called. In this case, the {@code javaType} + * argument is expected to be {@code Object.class} and this method returns + * a default mapping based on the element type of the ARRAY. Otherwise, if + * the {@code javaType} is something more specific, this method just returns + * it. */ - private Class> getJavaArrayType( + private Class> getArrayTypeMapping( OracleArray oracleArray, Class> javaType) { if (!Object.class.equals(javaType)) return javaType; - // Determine a default Java type mapping for the element type of the ARRAY. - // If the element type is DATE, handle it as if it were TIMESTAMP. - // This is consistent with how DATE columns are usually handled, and - // reflects the fact that Oracle DATE values have a time component. int jdbcType = fromJdbc(oracleArray::getBaseType); + + // Check if the array is multi-dimensional if (jdbcType == Types.ARRAY) { Object[] oracleArrays = (Object[]) fromJdbc(oracleArray::getArray); - // TODO: It should be possible to determine the type of next ARRAY - // dimension by calling - // OracleConnection.createArray( - // oracleArray.getSQLTypeName(), new Object[0]).getBaseType()) + // An instance of OracleArray representing base type is needed in order to + // know the base type of the next dimension. final OracleArray oracleArrayElement; if (oracleArrays.length > 0) { oracleArrayElement = (OracleArray) oracleArrays[0]; } else { - // Need to create an instance of the OracleArray base type in order to - // know its own base type. The information for the base type ARRAY - // should already be cached by Oracle JDBC, as the type info was - // retrieved when value was returned from the database. If the - // information is cached, then createOracleArray should not perform a - // blocking call. + // The array is empty, so an OracleArray will need to be created. The + // type information for the ARRAY should be cached by Oracle JDBC, and + // so createOracleArray should not perform a blocking call. oracleArrayElement = (OracleArray) fromJdbc(() -> jdbcConnection.unwrap(OracleConnection.class) .createOracleArray(oracleArray.getBaseTypeName(), new Object[0])); } + // Recursively call getJavaArrayType, creating a Java array at each level + // of recursion until a non-array SQL type is found. Returning back up the + // stack, the top level of recursion will then create an array with + // the right number of dimensions, and the class of this multi-dimensional + // array is returned. return java.lang.reflect.Array.newInstance( - getJavaArrayType(oracleArrayElement, Object.class), 0) + getArrayTypeMapping(oracleArrayElement, Object.class), 0) .getClass(); } + // If the element type is DATE, handle it as if it were TIMESTAMP. + // This is consistent with how DATE columns are usually handled, and + // reflects the fact that Oracle DATE values have a time component. Type r2dbcType = SqlTypeMap.toR2dbcType( jdbcType == Types.DATE ? Types.TIMESTAMP : jdbcType); @@ -476,6 +488,12 @@ private Class> getJavaArrayType( : javaType; } + /** + * Converts an array from Oracle JDBC into a Java array of a primitive type, + * such as int[], boolean[], etc. This method is handles the case where user + * code explicitly requests a primitive array by passing the class type + * to {@link #get(int, Class)} or {@link #get(String, Class)}. + */ private Object convertPrimitiveArray( OracleArray oracleArray, Class> primitiveType) { try { @@ -531,11 +549,11 @@ else if (double.class.equals(primitiveType)) { } /** - * Converts a given {@code array} to an array of a specified - * {@code desiredType}. This method handles arrays returned by - * {@link Array#getArray(Map)}, which may contain objects that are the default - * desiredType mapping for a JDBC driver. This method converts the default JDBC - * desiredType mappings to the default R2DBC desiredType mappings. + * Converts a given {@code array} to an array of a {@code desiredType}. This + * method handles arrays returned by {@link Array#getArray(Map)}, which may + * contain objects that are the default desiredType mapping for a JDBC driver. + * This method converts the default JDBC desiredType mappings to the default + * R2DBC desiredType mappings. */ @SuppressWarnings("unchecked") private T[] convertArray(Object[] array, Class desiredType) { @@ -685,6 +703,7 @@ else if (oracle.sql.INTERVALDS.class.isAssignableFrom(elementType) } else if (oracle.jdbc.OracleArray.class.isAssignableFrom(elementType) && desiredType.isArray()) { + // Recursively convert a multi-dimensional array. return (T[]) mapArray( array, length -> @@ -699,6 +718,10 @@ else if (oracle.jdbc.OracleArray.class.isAssignableFrom(elementType) throw unsupportedArrayConversion(elementType, desiredType); } + /** + * Returns an exception indicating a type of elements in a Java array can + * not be converted to a different type. + */ private static IllegalArgumentException unsupportedArrayConversion( Class> fromType, Class> toType) { return new IllegalArgumentException(format( @@ -706,6 +729,21 @@ private static IllegalArgumentException unsupportedArrayConversion( fromType.getName(), toType.getName())); } + /** + * + * Maps the elements of a given {@code array} using a {@code mappingFunction}, + * and returns an array generated by an {@code arrayAllocator} that stores the + * mapped elements. + *
+ * The {@code array} may contain {@code null} elements. A {@code null} element + * is automatically converted to {@code null}, and not supplied as input to + * the {@code mappingFunction}. + *
+ * @param array Array of elements to convert. Not null. + * @param arrayAllocator Allocates an array of an input length. Not null. + * @param mappingFunction Maps elements from the {@code array}. Not null. + * @return Array of mapped elements. + */ private staticU[] mapArray( T[] array, IntFunction arrayAllocator, Function mappingFunction) { @@ -722,49 +760,6 @@ private static U[] mapArray( return result; } - /** - * Converts an array of {@code BigDecimal} values to objects of a given - * type. This method handles the case where Oracle JDBC does not perform - * conversions specified by the {@code Map} argument to - * {@link Array#getArray(Map)} - */ - private T[] convertBigDecimalArray( - BigDecimal[] bigDecimals, Class type) { - - final Function mapFunction; - - if (type.equals(Byte.class)) { - mapFunction = bigDecimal -> type.cast(bigDecimal.byteValue()); - } - else if (type.equals(Short.class)) { - mapFunction = bigDecimal -> type.cast(bigDecimal.shortValue()); - } - else if (type.equals(Integer.class)) { - mapFunction = bigDecimal -> type.cast(bigDecimal.intValue()); - } - else if (type.equals(Long.class)) { - mapFunction = bigDecimal -> type.cast(bigDecimal.longValue()); - } - else if (type.equals(Float.class)) { - mapFunction = bigDecimal -> type.cast(bigDecimal.floatValue()); - } - else if (type.equals(Double.class)) { - mapFunction = bigDecimal -> type.cast(bigDecimal.doubleValue()); - } - else { - throw new IllegalArgumentException( - "Can not convert BigDecimal to " + type); - } - - return Arrays.stream(bigDecimals) - .map(mapFunction) - .toArray(length -> { - @SuppressWarnings("unchecked") - T[] array = (T[])java.lang.reflect.Array.newInstance(type, length); - return array; - }); - } - /** * Checks if the specified zero-based {@code index} is a valid column index * for this row. This method is used to verify index value parameters @@ -801,7 +796,7 @@ private static final class RowImpl * determine the default type mapping of column values. * * @param jdbcConnection JDBC connection that created the - * {@code jdbcReadable}. Not null.* + * {@code jdbcReadable}. Not null. * @param jdbcReadable Row data from the Oracle JDBC Driver. Not null. * @param metadata Meta-data for the specified row. Not null. * @param adapter Adapts JDBC calls into reactive streams. Not null. diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 10b8e50..71512a7 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -976,7 +976,20 @@ final Publisher execute() { /** * - * Sets {@link #binds} on the {@link #preparedStatement}. The + * Sets {@link #binds} on the {@link #preparedStatement}, as specified by + * {@link #bind(Object[])}. This method is called when this statement is + * executed. Subclasess override this method to perform additional actions. + *
+ * @return A {@code Publisher} that emits {@code onComplete} when all + * {@code binds} have been set. + */ + protected Publisherbind() { + return bind(binds); + } + + /** + * + * Sets the given {@code binds} on the {@link #preparedStatement}. The * returned {@code Publisher} completes after all bind values have * materialized and been set on the {@code preparedStatement}. *
@@ -988,10 +1001,6 @@ final Publisher
execute() { * @return A {@code Publisher} that emits {@code onComplete} when all * {@code binds} have been set. */ - protected Publisher bind() { - return bind(binds); - } - protected final Publisher bind(Object[] binds) { return adapter.getLock().flatMap(() -> { From d0694d5cd66c1ab7c33a9dae75a5e3a3c0087a66 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Thu, 27 Oct 2022 15:13:08 -0700 Subject: [PATCH 10/10] Increase timeouts for github action runs --- .github/workflows/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 4ba9e9b..d232f13 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -81,6 +81,6 @@ echo "HOST=localhost" >> src/test/resources/config.properties echo "PORT=1521" >> src/test/resources/config.properties echo "USER=test" >> src/test/resources/config.properties echo "PASSWORD=test" >> src/test/resources/config.properties -echo "CONNECT_TIMEOUT=120" >> src/test/resources/config.properties -echo "SQL_TIMEOUT=120" >> src/test/resources/config.properties +echo "CONNECT_TIMEOUT=180" >> src/test/resources/config.properties +echo "SQL_TIMEOUT=180" >> src/test/resources/config.properties mvn clean compile test