From 59589c39623b174e04a15516b7c05e0f85fd2359 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Tue, 25 Oct 2022 13:03:17 -0700 Subject: [PATCH 01/10] Support ARRAY binds and results --- .../java/oracle/r2dbc/OracleR2dbcTypes.java | 98 ++- .../oracle/r2dbc/impl/OracleReadableImpl.java | 317 ++++++++ .../r2dbc/impl/OracleStatementImpl.java | 226 +++++- .../java/oracle/r2dbc/impl/SqlTypeMap.java | 23 +- .../oracle/r2dbc/impl/TypeMappingTest.java | 732 +++++++++++++++++- 5 files changed, 1356 insertions(+), 40 deletions(-) diff --git a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java index 6f88622..1124a3a 100644 --- a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java +++ b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java @@ -20,6 +20,9 @@ */ package oracle.r2dbc; +import io.r2dbc.spi.Parameter; +import io.r2dbc.spi.R2dbcType; +import io.r2dbc.spi.Statement; import io.r2dbc.spi.Type; import oracle.sql.json.OracleJsonObject; @@ -28,6 +31,7 @@ import java.time.Duration; import java.time.LocalDateTime; import java.time.Period; +import java.util.Objects; /** * SQL types supported by Oracle Database that are not defined as standard types @@ -92,10 +96,102 @@ private OracleR2dbcTypes() {} public static final Type TIMESTAMP_WITH_LOCAL_TIME_ZONE = new TypeImpl(LocalDateTime.class, "TIMESTAMP WITH LOCAL TIME ZONE"); + /** + *

+ * 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 arrayBindExample(Connection connection) {
+   *   Statement statement =
+   *     connection.createStatement("INSERT INTO example VALUES (:array_bind)");
+   *
+   *   // Use the name defined for an ARRAY type:
+   *   // CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER
+   *   ArrayType arrayType = OracleR2dbcTypes.arrayType("MY_ARRAY");
+   *   Integer[] arrayValues = {1, 2, 3};
+   *   statement.bind("arrayBind", Parameters.in(arrayType, arrayValues));
+   *
+   *   return statement.execute();
+   * }
+   * }
+ * @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 T[] convertArray(Object[] array, Class type) { + + Class arrayType = array.getClass().getComponentType(); + + if (type.isAssignableFrom(arrayType)) { + return (T[])array; + } + else if (arrayType.equals(BigDecimal.class)) { + if (type.equals(Boolean.class)) { + return (T[]) mapArray( + (BigDecimal[])array, Boolean[]::new, + bigDecimal -> bigDecimal.shortValue() != 0); + } + else if (type.equals(Byte.class)) { + return (T[]) mapArray( + (BigDecimal[])array, Byte[]::new, BigDecimal::byteValue); + } + else if (type.equals(Short.class)) { + return (T[]) mapArray( + (BigDecimal[])array, Short[]::new, BigDecimal::shortValue); + } + else if (type.equals(Integer.class)) { + return (T[]) mapArray( + (BigDecimal[])array, Integer[]::new, BigDecimal::intValue); + } + else if (type.equals(Long.class)) { + return (T[]) mapArray( + (BigDecimal[])array, Long[]::new, BigDecimal::longValue); + } + else if (type.equals(Float.class)) { + return (T[]) mapArray( + (BigDecimal[])array, Float[]::new, BigDecimal::floatValue); + } + else if (type.equals(Double.class)) { + return (T[]) mapArray( + (BigDecimal[])array, Double[]::new, BigDecimal::doubleValue); + } + } + else if (byte[].class.equals(arrayType) + && type.isAssignableFrom(ByteBuffer.class)) { + return (T[]) mapArray( + (byte[][])array, ByteBuffer[]::new, ByteBuffer::wrap); + } + else if (java.sql.Timestamp.class.isAssignableFrom(arrayType)) { + // Note that DATE values are represented as Timestamp by OracleArray. + // For this reason, there is no branch for java.sql.Date conversions in + // this method. + if (type.isAssignableFrom(LocalDateTime.class)) { + return (T[]) mapArray( + (java.sql.Timestamp[]) array, LocalDateTime[]::new, + Timestamp::toLocalDateTime); + } + else if (type.isAssignableFrom(LocalDate.class)) { + return (T[]) mapArray( + (java.sql.Timestamp[]) array, LocalDate[]::new, + timestamp -> timestamp.toLocalDateTime().toLocalDate()); + } + else if (type.isAssignableFrom(LocalTime.class)) { + return (T[]) mapArray( + (java.sql.Timestamp[]) array, LocalTime[]::new, + timestamp -> timestamp.toLocalDateTime().toLocalTime()); + } + } + else if (java.time.OffsetDateTime.class.isAssignableFrom(arrayType)) { + // This branch handles mapping from TIMESTAMP WITH LOCAL TIME ZONE values. + // OracleArray maps these to OffsetDateTime, regardless of the Map + // argument to OracleArray.getArray(Map). Oracle R2DBC defines + // LocalDateTime as their default mapping. + if (type.isAssignableFrom(LocalDateTime.class)) { + return (T[]) mapArray( + (java.time.OffsetDateTime[]) array, LocalDateTime[]::new, + OffsetDateTime::toLocalDateTime); + } + else if (type.isAssignableFrom(LocalDate.class)) { + return (T[]) mapArray( + (java.time.OffsetDateTime[]) array, LocalDate[]::new, + OffsetDateTime::toLocalDate); + } + else if (type.isAssignableFrom(LocalTime.class)) { + return (T[]) mapArray( + (java.time.OffsetDateTime[]) array, LocalTime[]::new, + OffsetDateTime::toLocalTime); + } + } + else if (oracle.sql.INTERVALYM.class.isAssignableFrom(arrayType) + && type.isAssignableFrom(java.time.Period.class)) { + return (T[]) mapArray( + (oracle.sql.INTERVALYM[]) array, java.time.Period[]::new, + intervalym -> { + // The binary representation is specified in the JavaDoc of + // oracle.sql.INTERVALYM. In 21.x, the JavaDoc has bug: It neglects + // to mention that the year value is offset by 0x80000000 + ByteBuffer byteBuffer = ByteBuffer.wrap(intervalym.shareBytes()); + return Period.of( + byteBuffer.getInt() - 0x80000000, // 4 byte year + (byte)(byteBuffer.get() - 60), // 1 byte month + 0); // day + }); + } + else if (oracle.sql.INTERVALDS.class.isAssignableFrom(arrayType) + && type.isAssignableFrom(java.time.Duration.class)) { + return (T[]) mapArray( + (oracle.sql.INTERVALDS[]) array, java.time.Duration[]::new, + intervalds -> { + // The binary representation is specified in the JavaDoc of + // oracle.sql.INTERVALDS. In 21.x, the JavaDoc has bug: It neglects + // to mention that the day and fractional second values are offset by + // 0x80000000 + ByteBuffer byteBuffer = ByteBuffer.wrap(intervalds.shareBytes()); + return Duration.of( + TimeUnit.DAYS.toNanos(byteBuffer.getInt() - 0x80000000)// 4 byte day + + TimeUnit.HOURS.toNanos(byteBuffer.get() - 60) // 1 byte hour + + TimeUnit.MINUTES.toNanos(byteBuffer.get() - 60) // 1 byte minute + + TimeUnit.SECONDS.toNanos(byteBuffer.get() - 60) // 1 byte second + + byteBuffer.getInt() - 0x80000000, // 4 byte fractional second + ChronoUnit.NANOS); + }); + } + // OracleArray seems to support mapping TIMESTAMP WITH TIME ZONE to + // OffsetDateTime, so that case is not handled in this method + + throw new IllegalArgumentException(format( + "Conversion from array of %s to array of %s is not supported", + arrayType.getName(), type)); + } + + private static U[] mapArray( + T[] array, IntFunction arrayAllocator, + Function mappingFunction) { + + U[] result = arrayAllocator.apply(array.length); + + for (int i = 0; i < array.length; i++) { + T arrayValue = array[i]; + result[i] = arrayValue == null + ? null + : mappingFunction.apply(array[i]); + } + + 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 diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 8b76052..8cf21fc 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -27,13 +27,18 @@ import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; import io.r2dbc.spi.Type; +import oracle.jdbc.OracleConnection; +import oracle.r2dbc.OracleR2dbcTypes; import oracle.r2dbc.impl.ReactiveJdbcAdapter.JdbcReadable; import oracle.r2dbc.impl.ReadablesMetadata.OutParametersMetadataImpl; +import oracle.sql.ArrayDescriptor; +import oracle.sql.INTERVALYM; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; +import java.sql.Array; import java.sql.BatchUpdateException; import java.sql.CallableStatement; import java.sql.Connection; @@ -43,6 +48,7 @@ import java.sql.SQLType; import java.sql.SQLWarning; import java.time.Duration; +import java.time.Period; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -727,21 +733,15 @@ private void bindParameter(int index, Parameter parameter) { throw outParameterWithGeneratedValues(); } + requireSupportedSqlType(requireNonNull( + parameter.getType(), "Parameter type is null")); + requireSupportedJavaType(parameter.getValue()); + // TODO: This method should check if Java type can be converted to the // specified SQL type. If the conversion is unsupported, then JDBC // setObject(...) will throw when this statement is executed. The correct // behavior is to throw IllegalArgumentException here, and not from // execute() - Type r2dbcType = - requireNonNull(parameter.getType(), "Parameter type is null"); - SQLType jdbcType = toJdbcType(r2dbcType); - - if (jdbcType == null) { - throw new IllegalArgumentException( - "Unsupported SQL type: " + r2dbcType); - } - - requireSupportedJavaType(parameter.getValue()); bindValues[index] = parameter; } @@ -853,6 +853,19 @@ private static void requireSupportedJavaType(Object object) { } } + /** + * Checks that the SQL type identified by an {@code r2dbcType} is supported + * as a bind parameter. + * @param r2dbcType SQL type. Not null. + * @throws IllegalArgumentException If the SQL type is not supported. + */ + private static void requireSupportedSqlType(Type r2dbcType) { + SQLType jdbcType = toJdbcType(r2dbcType); + + if (jdbcType == null) + throw new IllegalArgumentException("Unsupported SQL type: " + r2dbcType); + } + /** * Returns an exception indicating that it is not possible to execute a * statement that returns both out-parameters and generated values. There @@ -982,35 +995,25 @@ protected Publisher bind() { protected final Publisher bind(Object[] binds) { return adapter.getLock().flatMap(() -> { + List> bindPublishers = null; + for (int i = 0; i < binds.length; i++) { + // Out binds are handled in the registerOutParameters method of the + // JdbcCall subclass. if (binds[i] instanceof Parameter.Out && !(binds[i] instanceof Parameter.In)) continue; - Object jdbcValue = convertBind(binds[i]); - SQLType jdbcType = - binds[i] instanceof Parameter - ? toJdbcType(((Parameter) binds[i]).getType()) - : null; // JDBC infers the type - - if (jdbcValue instanceof Publisher) { - int indexFinal = i; - Publisher bindPublisher = - Mono.from((Publisher) jdbcValue) - .doOnSuccess(allocatedValue -> - setBind(indexFinal, allocatedValue, jdbcType)) - .then(); + Publisher bindPublisher = bind(i, binds[i]); + if (bindPublisher != null) { if (bindPublishers == null) bindPublishers = new LinkedList<>(); bindPublishers.add(bindPublisher); } - else { - setBind(i, jdbcValue, jdbcType); - } } return bindPublishers == null @@ -1019,6 +1022,72 @@ protected final Publisher bind(Object[] binds) { }); } + /** + * Binds a value to the {@link #preparedStatement}. This method may convert + * the given {@code value} into an object type that is accepted by JDBC. If + * value is materialized asynchronously, such as a Blob or Clob, then this + * method returns a publisher that completes when the materialized value is + * bound. + * @param index zero-based bind index + * @param value value to bind. May be null. + * @return A publisher that completes when the value is bound, or null if + * the value is bound synchronously. + * @implNote The decision to return a null publisher rather than an empty + * publisher is motivated by reducing object allocation and overhead from + * subscribe/onSubscribe/onComplete. It is thought that this overhead would + * be substantial if it were incurred for each bind, of each statement, of + * each connection. + */ + private Publisher bind(int index, Object value) { + + final Object jdbcValue = convertBind(value); + + final SQLType jdbcType; + final String typeName; + + if (value instanceof Parameter) { + // Convert the parameter's R2DBC type to a JDBC type. Get the type name + // by calling getName() on the R2DBC type, not the JDBC type; This + // ensures that a user defined name will be used, such as one from + // OracleR2dbcTypes.ArrayType.getName() + Type type = ((Parameter)value).getType(); + jdbcType = toJdbcType(type); + typeName = type.getName(); + } + else { + // JDBC will infer the type from the class of jdbcValue + jdbcType = null; + typeName = null; + } + + if (jdbcValue instanceof Publisher) { + return setPublishedBind(index, (Publisher) jdbcValue); + } + else { + setBind(index, jdbcValue, jdbcType, typeName); + return null; + } + } + + /** + * Binds a published value to the {@link #preparedStatement}. The binding + * happens asynchronously. The returned publisher that completes after the + * value is published and bound + * @param index zero based bind index + * @param publisher publisher that emits a bound value. + * @return A publisher that completes after the published value is bound. + */ + private Publisher setPublishedBind( + int index, Publisher publisher) { + return Mono.from(publisher) + .flatMap(value -> { + Publisher bindPublisher = bind(index, value); + return bindPublisher == null + ? Mono.empty() + : Mono.from(bindPublisher); + }); + } + /** * Executes the JDBC {@link #preparedStatement} and maps the * results into R2DBC {@link Result} objects. The base class implements @@ -1219,14 +1288,23 @@ private void closeStatement() throws SQLException { * @param index 0-based parameter index * @param value Value. May be null. * @param type SQL type. May be null. + * @param typeName Name of a user defined type. May be null. */ - private void setBind(int index, Object value, SQLType type) { + private void setBind( + int index, Object value, SQLType type, String typeName) { runJdbc(() -> { int jdbcIndex = index + 1; - if (type != null) - preparedStatement.setObject(jdbcIndex, value, type); - else + + if (type == null) { preparedStatement.setObject(jdbcIndex, value); + } + else if (value == null) { + preparedStatement.setNull( + jdbcIndex, type.getVendorTypeNumber(), typeName); + } + else { + preparedStatement.setObject(jdbcIndex, value, type); + } }); } @@ -1256,7 +1334,7 @@ private Object convertBind(Object value) { return null; } else if (value instanceof Parameter) { - return convertBind(((Parameter) value).getValue()); + return convertParameterBind((Parameter) value); } else if (value instanceof io.r2dbc.spi.Blob) { return convertBlobBind((io.r2dbc.spi.Blob) value); @@ -1272,6 +1350,92 @@ else if (value instanceof ByteBuffer) { } } + /** Converts a {@code Parameter} bind value to a JDBC bind value */ + private Object convertParameterBind(Parameter parameter) { + Object value = parameter.getValue(); + + if (value == null) + return null; + + Type type = parameter.getType(); + + if (type instanceof OracleR2dbcTypes.ArrayType) { + return convertArrayBind((OracleR2dbcTypes.ArrayType) type, value); + } + else { + return value; + } + } + + /** + * Converts a given {@code value} to a JDBC array of the given {@code type}. + */ + private Array convertArrayBind( + OracleR2dbcTypes.ArrayType type, Object value) { + + final Object jdbcValue; + + if (value instanceof byte[]) { + // Oracle JDBC does not support creating SQL arrays from a byte[], so + // convert it to an short[]. + byte[] bytes = (byte[])value; + short[] shorts = new short[bytes.length]; + for (int i = 0; i < bytes.length; i++) + shorts[i] = (short)(0xFF & bytes[i]); + + jdbcValue = shorts; + } + else if (value instanceof ByteBuffer[]) { + // Convert from R2DBC's ByteBuffer representation of binary data into + // JDBC's byte[] representation + ByteBuffer[] byteBuffers = (ByteBuffer[]) value; + byte[][] byteArrays = new byte[byteBuffers.length][]; + for (int i = 0; i < byteBuffers.length; i++) { + ByteBuffer byteBuffer = byteBuffers[i]; + byteArrays[i] = byteBuffer == null + ? null + : convertByteBufferBind(byteBuffers[i]); + } + + jdbcValue = byteArrays; + } + else if (value instanceof Period[]) { + // Convert from Oracle R2DBC's Period representation of INTERVAL YEAR TO + // MONTH to Oracle JDBC's INTERVALYM representation. + Period[] periods = (Period[]) value; + INTERVALYM[] intervalYearToMonths = new INTERVALYM[periods.length]; + for (int i = 0; i < periods.length; i++) { + Period period = periods[i]; + if (period == null) { + intervalYearToMonths[i] = null; + } + else { + // The binary representation is specified in the JavaDoc of + // oracle.sql.INTERVALYM. In 21.x, the JavaDoc has bug: It neglects + // to mention that the year value is offset by 0x80000000 + byte[] bytes = new byte[5]; + ByteBuffer.wrap(bytes) + .putInt(period.getYears() + 0x80000000) // 4 byte year + .put((byte)(period.getMonths() + 60)); // 1 byte month + intervalYearToMonths[i] = new INTERVALYM(bytes); + } + } + + jdbcValue = intervalYearToMonths; + } + else { + jdbcValue = value; + } + + // TODO: createOracleArray executes a blocking database call the first + // time an OracleArray is created for a given type name. Subsequent + // creations of the same type avoid the database call using a cached type + // descriptor. If possible, rewrite this use a non-blocking call. + return fromJdbc(() -> + jdbcConnection.unwrap(OracleConnection.class) + .createOracleArray(type.getName(), jdbcValue)); + } + /** * Converts an R2DBC Blob to a JDBC Blob. The returned {@code Publisher} * asynchronously writes the {@code r2dbcBlob's} content to a JDBC Blob and diff --git a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java index 231c1fd..985442e 100644 --- a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java +++ b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java @@ -132,6 +132,7 @@ final class SqlTypeMap { entry(OffsetDateTime.class, JDBCType.TIMESTAMP_WITH_TIMEZONE), entry(io.r2dbc.spi.Blob.class, JDBCType.BLOB), entry(io.r2dbc.spi.Clob.class, JDBCType.CLOB), + entry(Object[].class, JDBCType.ARRAY), // JDBC 4.3 mappings not included in R2DBC Specification. Types like // java.sql.Blob/Clob/NClob/Array can be accessed from Row.get(...) @@ -159,8 +160,19 @@ final class SqlTypeMap { // Extended mappings supported by Oracle entry(Duration.class, OracleType.INTERVAL_DAY_TO_SECOND), entry(Period.class, OracleType.INTERVAL_YEAR_TO_MONTH), - entry(OracleJsonObject.class, OracleType.JSON) + entry(OracleJsonObject.class, OracleType.JSON), + // Primitive array mappings supported by OracleArray. Primitive arrays are + // not subtypes of Object[], which is listed for SQLType.ARRAY above. The + // primitive array types must be explicitly listed here. + entry(boolean[].class, OracleType.VARRAY), + // byte[] is mapped to RAW by default + // entry(byte[].class, OracleType.VARRAY), + entry(short[].class, OracleType.VARRAY), + entry(int[].class, OracleType.VARRAY), + entry(long[].class, OracleType.VARRAY), + entry(float[].class, OracleType.VARRAY), + entry(double[].class, OracleType.VARRAY) ); /** @@ -215,9 +227,12 @@ static Type toR2dbcType(int jdbcTypeNumber) { * @return A JDBC SQL type */ static SQLType toJdbcType(Type r2dbcType) { - return r2dbcType instanceof Type.InferredType - ? toJdbcType(r2dbcType.getJavaType()) - : R2DBC_TO_JDBC_TYPE_MAP.get(r2dbcType); + if (r2dbcType instanceof Type.InferredType) + return toJdbcType(r2dbcType.getJavaType()); + else if (r2dbcType instanceof OracleR2dbcTypes.ArrayType) + return JDBCType.ARRAY; + else + return R2DBC_TO_JDBC_TYPE_MAP.get(r2dbcType); } /** diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index 1f8907b..e2a8eb6 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -24,9 +24,12 @@ import io.r2dbc.spi.Blob; import io.r2dbc.spi.Clob; import io.r2dbc.spi.Connection; +import io.r2dbc.spi.Parameter; import io.r2dbc.spi.Parameters; import io.r2dbc.spi.Row; import io.r2dbc.spi.Statement; +import io.r2dbc.spi.Type; +import oracle.r2dbc.OracleR2dbcTypes; import oracle.sql.json.OracleJsonFactory; import oracle.sql.json.OracleJsonObject; import org.junit.jupiter.api.Assertions; @@ -40,12 +43,15 @@ import java.sql.RowId; import java.time.Duration; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; import java.time.OffsetDateTime; import java.time.Period; import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Objects; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -60,12 +66,15 @@ import static oracle.r2dbc.test.DatabaseConfig.sharedConnection; import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout; import static oracle.r2dbc.util.Awaits.awaitExecution; +import static oracle.r2dbc.util.Awaits.awaitNone; import static oracle.r2dbc.util.Awaits.awaitOne; import static oracle.r2dbc.util.Awaits.awaitUpdate; +import static oracle.r2dbc.util.Awaits.tryAwaitExecution; import static oracle.r2dbc.util.Awaits.tryAwaitNone; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -482,6 +491,711 @@ public void testByteArrayMapping() { } } + /** + *

+ * 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 void verifyTypeMapping( awaitExecution(connection.createStatement(String.format( "CREATE TABLE "+table+" (javaValue %s)", sqlTypeDdl))); - awaitUpdate(asList(1,1), connection.createStatement( - "INSERT INTO "+table+"(javaValue) VALUES(:javaValue)") - .bind("javaValue", javaValue).add() - .bindNull("javaValue", javaValue.getClass())); + Statement insert = connection.createStatement( + "INSERT INTO "+table+"(javaValue) VALUES(:javaValue)") + .bind("javaValue", javaValue) + .add(); + + if (javaValue instanceof Parameter) { + Type type = ((Parameter) javaValue).getType(); + insert.bind("javaValue", Parameters.in(type)); + } + else { + insert.bindNull("javaValue", javaValue.getClass()); + } + + awaitUpdate(asList(1,1), insert); verifyEquals.accept(javaValue, awaitOne(Flux.from(connection.createStatement( From 524178b8b2f13fe362f4ccaa67a5cf41ceeacd94 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Tue, 25 Oct 2022 13:09:51 -0700 Subject: [PATCH 02/10] Remove OBJECT test --- .../oracle/r2dbc/impl/TypeMappingTest.java | 53 ------------------- 1 file changed, 53 deletions(-) diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index e2a8eb6..9a26859 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -1143,59 +1143,6 @@ public void testArrayDatetimeTypeMappings() { } } - /** - *

- * 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 Date: Tue, 25 Oct 2022 13:26:37 -0700 Subject: [PATCH 03/10] Add README section --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index fa9299a..146bf20 100644 --- a/README.md +++ b/README.md @@ -553,6 +553,29 @@ prefetched entirely, a smaller prefetch size can be configured using the option, and the LOB can be consumed as a stream. By mapping LOB columns to `Blob` or `Clob` objects, the content can be consumed as a reactive stream. +### ARRAY +Oracle Database supports `ARRAY` as a user defined type only. A `CREATE TYPE` +command is used to define an `ARRAY` type: +```sql +CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER +``` +Oracle R2DBC defines `oracle.r2dbc.OracleR2dbcType.ArrayType` as a `Type` for +representing user defined `ARRAY` types. A `Parameter` with an type of +`ArrayType` must be used when binding array values to a `Statement` +```java +Publisher arrayBindExample(Connection connection) { + Statement statement = + connection.createStatement("INSERT INTO example VALUES (:array_bind)"); + + // Use the name defined for an ARRAY type: + // CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER + ArrayType arrayType = OracleR2dbcTypes.arrayType("MY_ARRAY"); + Integer[] arrayValues = {1, 2, 3}; + statement.bind("arrayBind", Parameters.in(arrayType, arrayValues)); + + return statement.execute(); +} +``` ## Secure Programming Guidelines The following security related guidelines should be adhered to when programming with the Oracle R2DBC Driver. From 155b0a752e0e0d7144cd690825ac64b11903e87f Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Tue, 25 Oct 2022 17:37:47 -0700 Subject: [PATCH 04/10] Support ARRAY out parameters --- README.md | 28 +- .../impl/OracleReadableMetadataImpl.java | 5 + .../r2dbc/impl/OracleStatementImpl.java | 14 +- .../r2dbc/impl/OracleStatementImplTest.java | 341 ++++++++++++++++-- 4 files changed, 350 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 146bf20..f55e89f 100644 --- a/README.md +++ b/README.md @@ -560,8 +560,8 @@ command is used to define an `ARRAY` type: CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER ``` Oracle R2DBC defines `oracle.r2dbc.OracleR2dbcType.ArrayType` as a `Type` for -representing user defined `ARRAY` types. A `Parameter` with an type of -`ArrayType` must be used when binding array values to a `Statement` +representing user defined `ARRAY` types. A `Parameter` with a type of +`ArrayType` must be used when binding array values to a `Statement`. ```java Publisher arrayBindExample(Connection connection) { Statement statement = @@ -576,6 +576,30 @@ Publisher arrayBindExample(Connection connection) { return statement.execute(); } ``` +A `Parameter` with a type of `ArrayType` must also be used when binding OUT +parameters of a PL/SQL call. +```java +Publisher arrayOutBindExample(Connection connection) { + Statement statement = + connection.createStatement("BEGIN; exampleCall(:array_bind); END;"); + + // Use the name defined for an ARRAY type: + // CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER + ArrayType arrayType = OracleR2dbcTypes.arrayType("MY_ARRAY"); + statement.bind("arrayBind", Parameters.out(arrayType)); + + return statement.execute(); +} +``` +`ARRAY` values may be consumed from a `Row` or `OutParameter` as a Java array. +The element type of the Java array may be any Java type that is supported as +a mapping for the SQL type of the `ARRAY`. For instance, if the `ARRAY` type is +`NUMBER`, then a `Integer[]` mapping is supported: +```java +Publisher arrayMapExample(Result result) { + return result.map(readable -> readable.get("arrayValue", Integer[].class)); +} +``` ## Secure Programming Guidelines The following security related guidelines should be adhered to when programming with the Oracle R2DBC Driver. diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java index a5b9996..5fa201b 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java @@ -263,6 +263,11 @@ public Type getType() { */ static OutParameterMetadata createParameterMetadata( String name, Type type) { + + // Translate the Oracle specific ArrayType to the standard COLLECTION type + if (type instanceof OracleR2dbcTypes.ArrayType) + type = R2dbcType.COLLECTION; + return new OracleOutParameterMetadataImpl( type, name, Nullability.NULLABLE, null, null); } diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 8cf21fc..29b7d74 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -1574,7 +1574,19 @@ private Publisher registerOutParameters() { for (int i : outBindIndexes) { Type type = ((Parameter) binds[i]).getType(); SQLType jdbcType = toJdbcType(type); - callableStatement.registerOutParameter(i + 1, jdbcType); + + if (type instanceof OracleR2dbcTypes.ArrayType) { + // Call registerOutParameter with the user defined type name + // returned by ArrayType.getName(). Oracle JDBC throws an exception + // if a name is provided for a built-in type, like VARCHAR, etc. So + // this branch should only be taken for user defined types, like + // ARRAY. + callableStatement.registerOutParameter( + i + 1, jdbcType, type.getName()); + } + else { + callableStatement.registerOutParameter(i + 1, jdbcType); + } } }); } diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index 509cbb4..a4c28ae 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -30,11 +30,11 @@ import io.r2dbc.spi.R2dbcNonTransientException; import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Result; -import io.r2dbc.spi.Result.Message; import io.r2dbc.spi.Result.UpdateCount; import io.r2dbc.spi.Statement; import io.r2dbc.spi.Type; import oracle.r2dbc.OracleR2dbcOptions; +import oracle.r2dbc.OracleR2dbcTypes; import oracle.r2dbc.OracleR2dbcWarning; import oracle.r2dbc.test.DatabaseConfig; import org.junit.jupiter.api.Test; @@ -45,9 +45,12 @@ import java.sql.RowId; import java.sql.SQLWarning; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -1279,7 +1282,7 @@ public void testOneInOutCall() { // inserted by the call. consumeOne(connection.createStatement( "BEGIN testOneInOutCallAdd(?); END;") - .bind(0, new InOutParameter(1, R2dbcType.NUMERIC)) + .bind(0, Parameters.inOut(R2dbcType.NUMERIC, 1)) .execute(), result -> { awaitNone(result.getRowsUpdated()); @@ -1293,7 +1296,7 @@ public void testOneInOutCall() { // parameter's default value to have been inserted by the call. consumeOne(connection.createStatement( "BEGIN testOneInOutCallAdd(:value); END;") - .bind("value", new InOutParameter(2, R2dbcType.NUMERIC)) + .bind("value", Parameters.inOut(R2dbcType.NUMERIC, 2)) .execute(), result -> awaitOne(1, result.map(row -> @@ -1307,7 +1310,7 @@ public void testOneInOutCall() { // parameter's value to have been inserted by the call. consumeOne(connection.createStatement( "BEGIN testOneInOutCallAdd(:value); END;") - .bind("value", new InOutParameter(3)) + .bind("value", Parameters.inOut(3)) .execute(), result -> awaitNone(result.getRowsUpdated())); @@ -1320,7 +1323,7 @@ public void testOneInOutCall() { // parameter's value to have been inserted by the call. consumeOne(connection.createStatement( "BEGIN testOneInOutCallAdd(?); END;") - .bind(0, new InOutParameter(4)) + .bind(0, Parameters.inOut(4)) .execute(), result -> awaitOne(3, result.map(row -> @@ -1374,8 +1377,8 @@ public void testMultiInOutCall() { // inserted by the call. consumeOne(connection.createStatement( "BEGIN testMultiInOutCallAdd(:value1, :value2); END;") - .bind("value1", new InOutParameter(1, R2dbcType.NUMERIC)) - .bind("value2", new InOutParameter(101, R2dbcType.NUMERIC)) + .bind("value1", Parameters.inOut(R2dbcType.NUMERIC, 1)) + .bind("value2", Parameters.inOut(R2dbcType.NUMERIC, 101)) .execute(), result -> awaitNone(result.getRowsUpdated())); @@ -1389,8 +1392,8 @@ public void testMultiInOutCall() { // parameter's default value to have been inserted by the call. consumeOne(connection.createStatement( "BEGIN testMultiInOutCallAdd(?, :value2); END;") - .bind(0, new InOutParameter(2, R2dbcType.NUMERIC)) - .bind("value2", new InOutParameter(102, R2dbcType.NUMERIC)) + .bind(0, Parameters.inOut(R2dbcType.NUMERIC, 2)) + .bind("value2", Parameters.inOut(R2dbcType.NUMERIC, 102)) .execute(), result -> awaitOne(asList(1, 101), result.map(row -> @@ -1406,8 +1409,8 @@ public void testMultiInOutCall() { // parameter's value to have been inserted by the call. consumeOne(connection.createStatement( "BEGIN testMultiInOutCallAdd(?, ?); END;") - .bind(0, new InOutParameter(3)) - .bind(1, new InOutParameter(103)) + .bind(0, Parameters.inOut(3)) + .bind(1, Parameters.inOut(103)) .execute(), result -> awaitNone(result.getRowsUpdated())); awaitQuery(asList(asList(3, 103)), @@ -1422,8 +1425,8 @@ public void testMultiInOutCall() { consumeOne(connection.createStatement( "BEGIN testMultiInOutCallAdd(" + "inout_value2 => :value2, inout_value1 => :value1); END;") - .bind("value1", new InOutParameter(4)) - .bind("value2", new InOutParameter(104)) + .bind("value1", Parameters.inOut(4)) + .bind("value2", Parameters.inOut(104)) .execute(), result -> awaitOne(asList(3, 103), result.map(row -> @@ -2238,39 +2241,307 @@ public void testGetSql() { } } - // TODO: Repalce with Parameters.inOut when that's available - private static final class InOutParameter - implements Parameter, Parameter.In, Parameter.Out { - final Type type; - final Object value; + /** + * Verifies behavior for a PL/SQL call having {@code ARRAY} type IN bind + */ + @Test + public void testInArrayCall() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TYPE TEST_IN_ARRAY AS ARRAY(8) OF NUMBER")); + awaitExecution(connection.createStatement( + "CREATE TABLE testInArrayCall(id NUMBER, value TEST_IN_ARRAY)")); + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testInArrayProcedure (" + + " id IN NUMBER," + + " inArray IN TEST_IN_ARRAY)" + + " IS" + + " BEGIN" + + " INSERT INTO testInArrayCall VALUES(id, inArray);" + + " END;")); + + class TestRow { + Long id; + int[] value; + TestRow(Long id, int[] value) { + this.id = id; + this.value = value; + } - InOutParameter(Object value) { - this(value, new Type.InferredType() { @Override - public Class getJavaType() { - return value.getClass(); + public boolean equals(Object other) { + return other instanceof TestRow + && Objects.equals(((TestRow) other).id, id) + && Objects.deepEquals(((TestRow)other).value, value); } @Override - public String getName() { - return "Inferred"; + public String toString() { + return id + ", " + Arrays.toString(value); } - }); - } + } + + TestRow row0 = new TestRow(0L, new int[]{1, 2, 3}); + OracleR2dbcTypes.ArrayType arrayType = + OracleR2dbcTypes.arrayType("TEST_IN_ARRAY"); + Statement callStatement = connection.createStatement( + "BEGIN testInArrayProcedure(:id, :value); END;"); + awaitExecution( + callStatement + .bind("id", row0.id) + .bind("value", Parameters.in(arrayType, row0.value))); + + awaitQuery( + List.of(row0), + row -> + new TestRow( + row.get("id", Long.class), + row.get("value", int[].class)), + connection.createStatement( + "SELECT id, value FROM testInArrayCall ORDER BY id")); + + TestRow row1 = new TestRow(1L, new int[]{4, 5, 6}); + awaitExecution( + callStatement + .bind("id", row1.id) + .bind("value", Parameters.in(arrayType, row1.value))); - InOutParameter(Object value, Type type) { - this.value = value; - this.type = type; + awaitQuery( + List.of(row0, row1), + row -> + new TestRow( + row.get("id", Long.class), + row.get("value", int[].class)), + connection.createStatement( + "SELECT id, value FROM testInArrayCall ORDER BY id")); } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testInArrayCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testInArrayProcedure")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE TEST_IN_ARRAY")); + tryAwaitNone(connection.close()); + } + } + + /** + * Verifies behavior for a PL/SQL call having {@code ARRAY} type OUT bind + */ + @Test + public void testOutArrayCall() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TYPE TEST_OUT_ARRAY AS ARRAY(8) OF NUMBER")); + awaitExecution(connection.createStatement( + "CREATE TABLE testOutArrayCall(id NUMBER, value TEST_OUT_ARRAY)")); + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testOutArrayProcedure (" + + " inId IN NUMBER," + + " outArray OUT TEST_OUT_ARRAY)" + + " IS" + + " BEGIN" + + " SELECT value INTO outArray" + + " FROM testOutArrayCall" + + " WHERE id = inId;" + + " EXCEPTION" + + " WHEN NO_DATA_FOUND THEN" + + " outArray := NULL;" + + " END;")); - @Override - public Type getType() { - return type; + class TestRow { + Long id; + Integer[] value; + TestRow(Long id, Integer[] value) { + this.id = id; + this.value = value; + } + + @Override + public boolean equals(Object other) { + return other instanceof TestRow + && Objects.equals(((TestRow) other).id, id) + && Objects.deepEquals(((TestRow)other).value, value); + } + + @Override + public String toString() { + return id + ", " + Arrays.toString(value); + } + } + + OracleR2dbcTypes.ArrayType arrayType = + OracleR2dbcTypes.arrayType("TEST_OUT_ARRAY"); + Statement callStatement = connection.createStatement( + "BEGIN testOutArrayProcedure(:id, :value); END;"); + + // Expect a NULL out parameter before any rows have been inserted + awaitQuery( + List.of(Optional.empty()), + outParameters -> { + assertNull(outParameters.get("value")); + assertNull(outParameters.get("value", int[].class)); + assertNull(outParameters.get("value", Integer[].class)); + return Optional.empty(); + }, + callStatement + .bind("id", -1) + .bind("value", Parameters.out(arrayType))); + + // Insert a row and expect an out parameter with the value + TestRow row0 = new TestRow(0L, new Integer[]{1, 2, 3}); + awaitUpdate(1, connection.createStatement( + "INSERT INTO testOutArrayCall VALUES (:id, :value)") + .bind("id", row0.id) + .bind("value", Parameters.in(arrayType, row0.value))); + awaitQuery( + List.of(row0), + outParameters -> + new TestRow(row0.id, outParameters.get("value", Integer[].class)), + callStatement + .bind("id", row0.id) + .bind("value", Parameters.out(arrayType))); + + // Insert another row and expect an out parameter with the value + TestRow row1 = new TestRow(1L, new Integer[]{4, 5, 6}); + awaitUpdate(1, connection.createStatement( + "INSERT INTO testOutArrayCall VALUES (:id, :value)") + .bind("id", row1.id) + .bind("value", Parameters.in(arrayType, row1.value))); + awaitQuery( + List.of(row1), + outParameters -> + new TestRow(row1.id, outParameters.get("value", Integer[].class)), + callStatement + .bind("id", row1.id) + .bind("value", Parameters.out(arrayType))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testOutArrayCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testOutArrayProcedure")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE TEST_OUT_ARRAY")); + tryAwaitNone(connection.close()); } + } + + /** + * Verifies behavior for a PL/SQL call having {@code ARRAY} type OUT bind + */ + @Test + public void testInOutArrayCall() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TYPE TEST_IN_OUT_ARRAY AS ARRAY(8) OF NUMBER")); + awaitExecution(connection.createStatement( + "CREATE TABLE testInOutArrayCall(id NUMBER, value TEST_IN_OUT_ARRAY)")); + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testInOutArrayProcedure (" + + " inId IN NUMBER," + + " inOutArray IN OUT TEST_IN_OUT_ARRAY)" + + " IS" + + " newValue TEST_IN_OUT_ARRAY;" + + " BEGIN" + + "" + + " newValue := TEST_IN_OUT_ARRAY();" + + " newValue.extend(inOutArray.count);" + + " FOR i IN 1 .. inOutArray.count LOOP" + + " newValue(i) := inOutArray(i);" + + " END LOOP;" + + "" + + " BEGIN" + + " SELECT value INTO inOutArray" + + " FROM testInOutArrayCall" + + " WHERE id = inId;" + + " DELETE FROM testInOutArrayCall WHERE id = inId;" + + " EXCEPTION" + + " WHEN NO_DATA_FOUND THEN" + + " inOutArray := NULL;" + + " END;" + + "" + + " INSERT INTO testInOutArrayCall VALUES (inId, newValue);" + + "" + + " END;")); + + class TestRow { + Long id; + Integer[] value; + TestRow(Long id, Integer[] value) { + this.id = id; + this.value = value; + } + + @Override + public boolean equals(Object other) { + return other instanceof TestRow + && Objects.equals(((TestRow) other).id, id) + && Objects.deepEquals(((TestRow)other).value, value); + } + + @Override + public String toString() { + return id + ", " + Arrays.toString(value); + } + } + + OracleR2dbcTypes.ArrayType arrayType = + OracleR2dbcTypes.arrayType("TEST_IN_OUT_ARRAY"); + Statement callStatement = connection.createStatement( + "BEGIN testInOutArrayProcedure(:id, :value); END;"); - @Override - public Object getValue() { - return value; + // Expect a NULL out parameter the first time a row is inserted + TestRow row = new TestRow(0L, new Integer[]{1, 2, 3}); + awaitQuery( + List.of(Optional.empty()), + outParameters -> { + assertNull(outParameters.get("value")); + assertNull(outParameters.get("value", int[].class)); + assertNull(outParameters.get("value", Integer[].class)); + return Optional.empty(); + }, + callStatement + .bind("id", row.id) + .bind("value", Parameters.inOut(arrayType, row.value))); + + // Update the row and expect an out parameter with the previous value + TestRow row1 = new TestRow(row.id, new Integer[]{4, 5, 6}); + awaitQuery( + List.of(row), + outParameters -> + new TestRow( + row.id, + outParameters.get("value", Integer[].class)), + callStatement + .bind("id", row.id) + .bind("value", Parameters.inOut(arrayType, row1.value))); + + // Update the row again and expect an out parameter with the previous + // value + TestRow row2 = new TestRow(row.id, new Integer[]{7, 8, 9}); + awaitQuery( + List.of(row1), + outParameters -> + new TestRow( + row.id, + outParameters.get("value", Integer[].class)), + callStatement + .bind("id", row.id) + .bind("value", Parameters.inOut(arrayType, row2.value))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testInOutArrayCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testInOutArrayProcedure")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE TEST_IN_OUT_ARRAY")); + tryAwaitNone(connection.close()); } } From 8a3909f996e5ca29070795927a05ac5af4deca32 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Tue, 25 Oct 2022 19:27:17 -0700 Subject: [PATCH 05/10] Add ReadableMetadata test for ARRAY --- .../impl/OracleReadableMetadataImplTest.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java index 55147ad..e3f82f5 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java @@ -24,6 +24,7 @@ import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Connection; import io.r2dbc.spi.Nullability; +import io.r2dbc.spi.Parameters; import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Type; import oracle.jdbc.OracleType; @@ -53,6 +54,7 @@ import static oracle.r2dbc.util.Awaits.awaitExecution; import static oracle.r2dbc.util.Awaits.awaitOne; import static oracle.r2dbc.util.Awaits.awaitUpdate; +import static oracle.r2dbc.util.Awaits.tryAwaitExecution; import static oracle.r2dbc.util.Awaits.tryAwaitNone; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -284,8 +286,6 @@ public void testRowIdTypes() { connection, "UROWID", JDBCType.ROWID, OracleR2dbcTypes.ROWID, null, null, RowId.class, rowId); - // Expect JSON and OracleJsonObject to map. - } finally { tryAwaitNone(connection.close()); @@ -325,6 +325,32 @@ public void testJsonType() { } } + /** + * Verifies the implementation of {@link OracleReadableMetadataImpl} for + * ARRAY type columns. + */ + @Test + public void testArrayTypes() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitExecution(connection.createStatement( + "CREATE TYPE TEST_ARRAY_TYPE AS ARRAY(3) OF NUMBER")); + + verifyColumnMetadata( + connection, "TEST_ARRAY_TYPE", JDBCType.ARRAY, R2dbcType.COLLECTION, + null, null, Object[].class, + Parameters.in( + OracleR2dbcTypes.arrayType("TEST_ARRAY_TYPE"), + new Integer[]{0, 1, 2})); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE TEST_ARRAY_TYPE")); + tryAwaitNone(connection.close()); + } + } + /** * Calls * {@link #verifyColumnMetadata(Connection, String, SQLType, Type, Integer, Integer, Nullability, Class, Object)} From b111e0c82a6b7aac283d20ba984985a5a29ecbf9 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Wed, 26 Oct 2022 17:58:31 -0700 Subject: [PATCH 06/10] Support multi-dimensional array mapping --- .../oracle/r2dbc/impl/OracleReadableImpl.java | 310 ++++++++---- .../oracle/r2dbc/impl/OracleResultImpl.java | 4 +- .../r2dbc/impl/OracleStatementImpl.java | 9 +- .../oracle/r2dbc/impl/TypeMappingTest.java | 477 +++++++++++++----- 4 files changed, 564 insertions(+), 236 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java index 6ecdeb3..ba8c406 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java @@ -26,24 +26,22 @@ import io.r2dbc.spi.OutParameters; import io.r2dbc.spi.OutParametersMetadata; import io.r2dbc.spi.R2dbcException; - import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import io.r2dbc.spi.Type; import oracle.jdbc.OracleArray; +import oracle.jdbc.OracleConnection; import oracle.r2dbc.OracleR2dbcTypes; 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; 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; @@ -52,7 +50,6 @@ 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; @@ -80,8 +77,9 @@ */ class OracleReadableImpl implements io.r2dbc.spi.Readable { - /** Adapts JDBC Driver APIs into Reactive Streams APIs */ - private final ReactiveJdbcAdapter adapter; + + /** The JDBC connection that created this readable */ + private final java.sql.Connection jdbcConnection; /** This values of this {@code Readable}. Values are supplied by a JDBC Driver */ private final JdbcReadable jdbcReadable; @@ -89,19 +87,26 @@ class OracleReadableImpl implements io.r2dbc.spi.Readable { /** Metadata of the values of this {@code Readable}. */ private final ReadablesMetadata readablesMetadata; + /** Adapts JDBC Driver APIs into Reactive Streams APIs */ + private final ReactiveJdbcAdapter adapter; + /** *

* 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 T[] convertArray(Object[] array, Class type) { + private T[] convertArray(Object[] array, Class desiredType) { + + if (desiredType.isAssignableFrom(array.getClass().getComponentType())) + return (T[])array; - Class arrayType = array.getClass().getComponentType(); + if (array.length == 0) + return (T[]) java.lang.reflect.Array.newInstance(desiredType, 0); - if (type.isAssignableFrom(arrayType)) { + Class elementType = array[0].getClass(); + + if (desiredType.isAssignableFrom(elementType)) { return (T[])array; } - else if (arrayType.equals(BigDecimal.class)) { - if (type.equals(Boolean.class)) { + else if (elementType.equals(BigDecimal.class)) { + if (desiredType.equals(Boolean.class)) { return (T[]) mapArray( (BigDecimal[])array, Boolean[]::new, bigDecimal -> bigDecimal.shortValue() != 0); } - else if (type.equals(Byte.class)) { + else if (desiredType.equals(Byte.class)) { return (T[]) mapArray( (BigDecimal[])array, Byte[]::new, BigDecimal::byteValue); } - else if (type.equals(Short.class)) { + else if (desiredType.equals(Short.class)) { return (T[]) mapArray( (BigDecimal[])array, Short[]::new, BigDecimal::shortValue); } - else if (type.equals(Integer.class)) { + else if (desiredType.equals(Integer.class)) { return (T[]) mapArray( (BigDecimal[])array, Integer[]::new, BigDecimal::intValue); } - else if (type.equals(Long.class)) { + else if (desiredType.equals(Long.class)) { return (T[]) mapArray( (BigDecimal[])array, Long[]::new, BigDecimal::longValue); } - else if (type.equals(Float.class)) { + else if (desiredType.equals(Float.class)) { return (T[]) mapArray( (BigDecimal[])array, Float[]::new, BigDecimal::floatValue); } - else if (type.equals(Double.class)) { + else if (desiredType.equals(Double.class)) { return (T[]) mapArray( (BigDecimal[])array, Double[]::new, BigDecimal::doubleValue); } } - else if (byte[].class.equals(arrayType) - && type.isAssignableFrom(ByteBuffer.class)) { + else if (byte[].class.equals(elementType) + && desiredType.isAssignableFrom(ByteBuffer.class)) { return (T[]) mapArray( (byte[][])array, ByteBuffer[]::new, ByteBuffer::wrap); } - else if (java.sql.Timestamp.class.isAssignableFrom(arrayType)) { + else if (java.sql.Timestamp.class.isAssignableFrom(elementType)) { // Note that DATE values are represented as Timestamp by OracleArray. // For this reason, there is no branch for java.sql.Date conversions in // this method. - if (type.isAssignableFrom(LocalDateTime.class)) { + if (desiredType.isAssignableFrom(LocalDateTime.class)) { return (T[]) mapArray( (java.sql.Timestamp[]) array, LocalDateTime[]::new, Timestamp::toLocalDateTime); } - else if (type.isAssignableFrom(LocalDate.class)) { + else if (desiredType.isAssignableFrom(LocalDate.class)) { return (T[]) mapArray( (java.sql.Timestamp[]) array, LocalDate[]::new, timestamp -> timestamp.toLocalDateTime().toLocalDate()); } - else if (type.isAssignableFrom(LocalTime.class)) { + else if (desiredType.isAssignableFrom(LocalTime.class)) { return (T[]) mapArray( (java.sql.Timestamp[]) array, LocalTime[]::new, timestamp -> timestamp.toLocalDateTime().toLocalTime()); } } - else if (java.time.OffsetDateTime.class.isAssignableFrom(arrayType)) { + else if (java.time.OffsetDateTime.class.isAssignableFrom(elementType)) { // This branch handles mapping from TIMESTAMP WITH LOCAL TIME ZONE values. // OracleArray maps these to OffsetDateTime, regardless of the Map // argument to OracleArray.getArray(Map). Oracle R2DBC defines // LocalDateTime as their default mapping. - if (type.isAssignableFrom(LocalDateTime.class)) { + if (desiredType.isAssignableFrom(LocalDateTime.class)) { return (T[]) mapArray( (java.time.OffsetDateTime[]) array, LocalDateTime[]::new, OffsetDateTime::toLocalDateTime); } - else if (type.isAssignableFrom(LocalDate.class)) { + else if (desiredType.isAssignableFrom(LocalDate.class)) { return (T[]) mapArray( (java.time.OffsetDateTime[]) array, LocalDate[]::new, OffsetDateTime::toLocalDate); } - else if (type.isAssignableFrom(LocalTime.class)) { + else if (desiredType.isAssignableFrom(LocalTime.class)) { return (T[]) mapArray( (java.time.OffsetDateTime[]) array, LocalTime[]::new, OffsetDateTime::toLocalTime); } } - else if (oracle.sql.INTERVALYM.class.isAssignableFrom(arrayType) - && type.isAssignableFrom(java.time.Period.class)) { + else if (oracle.sql.INTERVALYM.class.isAssignableFrom(elementType) + && desiredType.isAssignableFrom(java.time.Period.class)) { return (T[]) mapArray( (oracle.sql.INTERVALYM[]) array, java.time.Period[]::new, intervalym -> { @@ -558,8 +643,8 @@ else if (oracle.sql.INTERVALYM.class.isAssignableFrom(arrayType) 0); // day }); } - else if (oracle.sql.INTERVALDS.class.isAssignableFrom(arrayType) - && type.isAssignableFrom(java.time.Duration.class)) { + else if (oracle.sql.INTERVALDS.class.isAssignableFrom(elementType) + && desiredType.isAssignableFrom(java.time.Duration.class)) { return (T[]) mapArray( (oracle.sql.INTERVALDS[]) array, java.time.Duration[]::new, intervalds -> { @@ -577,12 +662,27 @@ else if (oracle.sql.INTERVALDS.class.isAssignableFrom(arrayType) ChronoUnit.NANOS); }); } + else if (oracle.jdbc.OracleArray.class.isAssignableFrom(elementType) + && desiredType.isArray()) { + return (T[]) mapArray( + array, + length -> + (Object[]) java.lang.reflect.Array.newInstance(desiredType, length), + oracleArray -> + convertOracleArray( + (OracleArray) oracleArray, desiredType.getComponentType())); + } // OracleArray seems to support mapping TIMESTAMP WITH TIME ZONE to // OffsetDateTime, so that case is not handled in this method - throw new IllegalArgumentException(format( + throw unsupportedArrayConversion(elementType, desiredType); + } + + private static IllegalArgumentException unsupportedArrayConversion( + Class fromType, Class toType) { + return new IllegalArgumentException(format( "Conversion from array of %s to array of %s is not supported", - arrayType.getName(), type)); + fromType.getName(), toType.getName())); } private static U[] mapArray( @@ -679,16 +779,16 @@ private static final class RowImpl * {@code jdbcReadable}, and uses the specified {@code rowMetadata} 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. */ private RowImpl( - JdbcReadable jdbcReadable, - RowMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { - super(jdbcReadable, metadata, adapter); + java.sql.Connection jdbcConnection, JdbcReadable jdbcReadable, + RowMetadataImpl metadata, ReactiveJdbcAdapter adapter) { + super(jdbcConnection, jdbcReadable, metadata, adapter); this.metadata = metadata; } @@ -721,16 +821,16 @@ private static final class OutParametersImpl * {@code jdbcReadable} and obtains metadata of the values from * {@code outParametersMetaData}. *

- * + * @param jdbcConnection JDBC connection that created the + * {@code jdbcReadable}. Not null. * @param jdbcReadable Readable values from a JDBC Driver. Not null. * @param metadata Metadata of each value. Not null. * @param adapter Adapts JDBC calls into reactive streams. Not null. */ private OutParametersImpl( - JdbcReadable jdbcReadable, - OutParametersMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { - super(jdbcReadable, metadata, adapter); + java.sql.Connection jdbcConnection, JdbcReadable jdbcReadable, + OutParametersMetadataImpl metadata, ReactiveJdbcAdapter adapter) { + super(jdbcConnection, jdbcReadable, metadata, adapter); this.metadata = metadata; } diff --git a/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java b/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java index ed6ed47..2dbe777 100644 --- a/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java @@ -478,7 +478,9 @@ Publisher publishSegments(Function mappingFunction) { // Avoiding object allocating by reusing the same Row object ReusableJdbcReadable reusableJdbcReadable = new ReusableJdbcReadable(); - Row row = createRow(reusableJdbcReadable, metadata, adapter); + Row row = createRow( + fromJdbc(() -> resultSet.getStatement().getConnection()), + reusableJdbcReadable, metadata, adapter); return adapter.publishRows(resultSet, jdbcReadable -> { reusableJdbcReadable.current = jdbcReadable; diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 29b7d74..86b46fb 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -1374,11 +1374,14 @@ private Array convertArrayBind( OracleR2dbcTypes.ArrayType type, Object value) { final Object jdbcValue; + Class componentType = value.getClass().getComponentType(); + while (componentType.isArray()) + componentType = componentType.getComponentType(); if (value instanceof byte[]) { // Oracle JDBC does not support creating SQL arrays from a byte[], so // convert it to an short[]. - byte[] bytes = (byte[])value; + byte[] bytes = (byte[])value; short[] shorts = new short[bytes.length]; for (int i = 0; i < bytes.length; i++) shorts[i] = (short)(0xFF & bytes[i]); @@ -1596,7 +1599,9 @@ protected Publisher executeJdbc() { return Flux.concat( super.executeJdbc(), Mono.just(createCallResult( - createOutParameters(new JdbcOutParameters(), metadata, adapter), + createOutParameters( + fromJdbc(preparedStatement::getConnection), + new JdbcOutParameters(), metadata, adapter), adapter))); } diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index 9a26859..5c8dc2e 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -51,10 +51,16 @@ import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.Collections; import java.util.Objects; import java.util.function.BiConsumer; +import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.ObjIntConsumer; +import java.util.function.UnaryOperator; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.Stream; import static io.r2dbc.spi.R2dbcType.NCHAR; @@ -513,18 +519,17 @@ public void testArrayCharacterTypeMappings() { "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))); + String[] strings = {"Hello", "Bonjour", "你好", null}; + verifyArrayTypeMapping( + connection, "CHARACTER_ARRAY", String[].class, + i -> + Arrays.stream(strings) + .map(string -> string == null ? null : i + "-" + string) + .toArray(String[]::new), + true, + Assertions::assertArrayEquals); } finally { tryAwaitExecution(connection.createStatement( @@ -556,9 +561,8 @@ public void testArrayNumericTypeMappings() { "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 + // Expect ARRAY of NUMBER and BigDecimal to map BigDecimal[] bigDecimals = new BigDecimal[] { BigDecimal.ZERO, new BigDecimal("1.23"), @@ -566,160 +570,170 @@ public void testArrayNumericTypeMappings() { new BigDecimal("7.89"), null }; - verifyTypeMapping( - connection, - Parameters.in(numberArrayType, bigDecimals), - numberArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - bigDecimals, - assertInstanceOf(BigDecimal[].class, rowValue))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", BigDecimal[].class, + i -> + Arrays.stream(bigDecimals) + .map(bigDecimal -> + bigDecimal == null + ? null + :bigDecimal.add(BigDecimal.valueOf(i))) + .toArray(BigDecimal[]::new), + true, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", byte[].class, + i -> { + byte[] moreBytes = new byte[bytes.length]; + for (int j = 0; j < bytes.length; j++) + moreBytes[j] = (byte)(bytes[j] + i); + return moreBytes; + }, + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", short[].class, + i -> { + short[] moreShorts = new short[shorts.length]; + for (int j = 0; j < shorts.length; j++) + moreShorts[j] = (short)(shorts[j] + i); + return moreShorts; + }, + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", int[].class, + i -> { + int[] moreInts = new int[ints.length]; + for (int j = 0; j < ints.length; j++) + moreInts[j] = (ints[j] + i); + return moreInts; + }, + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", long[].class, + i -> { + long[] moreLongs = new long[longs.length]; + for (int j = 0; j < longs.length; j++) + moreLongs[j] = (long)(longs[j] + i); + return moreLongs; + }, + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", float[].class, + i -> { + float[] moreFloats = new float[floats.length]; + for (int j = 0; j < floats.length; j++) + moreFloats[j] = (floats[j] + (float)i); + return moreFloats; + }, + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", double[].class, + i -> { + double[] moreDoubles = new double[doubles.length]; + for (int j = 0; j < doubles.length; j++) + moreDoubles[j] = (doubles[j] + (double)i); + return moreDoubles; + }, + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", Byte[].class, + i -> + Arrays.stream(byteObjects) + .map(byteObject -> + byteObject == null ? null : (byte)(byteObject + i)) + .toArray(Byte[]::new), + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", Short[].class, + i -> + Arrays.stream(shortObjects) + .map(shortObject -> + shortObject == null ? null : (short)(shortObject + i)) + .toArray(Short[]::new), + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", Integer[].class, + i -> + Arrays.stream(intObjects) + .map(intObject -> + intObject == null ? null : intObject + i) + .toArray(Integer[]::new), + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", Long[].class, + i -> + Arrays.stream(longObjects) + .map(longObject -> + longObject == null ? null : longObject + i) + .toArray(Long[]::new), + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", Float[].class, + i -> + Arrays.stream(floatObjects) + .map(floatObject -> + floatObject == null ? null : floatObject + i) + .toArray(Float[]::new), + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, "NUMBER_ARRAY", Double[].class, + i -> + Arrays.stream(doubleObjects) + .map(doubleObject -> + doubleObject == null ? null : doubleObject + i) + .toArray(Double[]::new), + false, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement("DROP TYPE NUMBER_ARRAY")); tryAwaitNone(connection.close()); } } @@ -1143,6 +1157,213 @@ public void testArrayDatetimeTypeMappings() { } } + /** + * For an ARRAY type of a given {@code typeName}, verifies the following + * cases: + *
    + *
  • 1 dimensional array mapping to an array of a {@code javaType}
  • + *
  • Mapping when array is empty.
  • + *
  • + * All cases listed above when the ARRAY type is the type of a a two + * dimensional and three dimensional array. + *
  • + *
+ */ + private static void verifyArrayTypeMapping( + Connection connection, String typeName, boolean isDefaultMapping, + Class javaArrayType, IntFunction arrayGenerator) { + verifyArrayTypeMapping( + connection, typeName, javaArrayType, arrayGenerator, + isDefaultMapping, + (expectedValue, rowValue) -> + assertArrayEquals((Object[]) expectedValue, (Object[]) rowValue)); + } + + private static void verifyArrayTypeMapping( + Connection connection, String typeName, + Class javaArrayClass, IntFunction arrayGenerator, + boolean isDefaultMapping, + BiConsumer equalsAssertion) { + + Type arrayType = OracleR2dbcTypes.arrayType(typeName); + + // Verify mapping of 1-dimensional array with values + T javaArray1D = arrayGenerator.apply(0); + verifyTypeMapping( + connection, + Parameters.in(arrayType, javaArray1D), + arrayType.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, javaArray1D.getClass()), + (ignored, rowValue) -> + equalsAssertion.accept( + javaArray1D, + assertInstanceOf(javaArrayClass, rowValue))); + + // Verify mapping of an empty 1-dimensional array + @SuppressWarnings("unchecked") + T emptyJavaArray = (T)java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType, emptyJavaArray), + arrayType.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, emptyJavaArray.getClass()), + (ignored, rowValue) -> + assertEquals( + 0, + java.lang.reflect.Array.getLength( + assertInstanceOf(javaArrayClass, rowValue)))); + + // Create 2 dimensional and 3 dimensional ARRAY types + Type arrayType2D = OracleR2dbcTypes.arrayType(arrayType.getName() + "_2D"); + Type arrayType3D = OracleR2dbcTypes.arrayType(arrayType.getName() + "_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE " + arrayType2D.getName() + + " AS ARRAY(10) OF " + arrayType.getName()) + .execute()); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE " + arrayType3D.getName() + + " AS ARRAY(10) OF " + arrayType2D.getName()) + .execute()); + try { + + // Create a 2D Java array + @SuppressWarnings("unchecked") + T[] javaArray2D = + (T[]) java.lang.reflect.Array.newInstance(javaArrayClass, 3); + for (int i = 0; i < javaArray2D.length; i++) { + javaArray2D[i] = arrayGenerator.apply(i); + } + + // Verify mapping of 2-dimensional array with values + verifyTypeMapping( + connection, + Parameters.in(arrayType2D, javaArray2D), + arrayType2D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, arrayType2D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + javaArray2D, + assertInstanceOf(javaArray2D.getClass(), rowValue))); + + // Verify mapping of an empty 2-dimensional array + @SuppressWarnings("unchecked") + T[] emptyJavaArray2D = (T[]) java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 0, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType2D, emptyJavaArray2D), + arrayType2D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, emptyJavaArray2D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + emptyJavaArray2D, + assertInstanceOf(emptyJavaArray2D.getClass(), rowValue))); + + // Verify of a 2-dimensional array with empty 1-dimensional arrays + @SuppressWarnings("unchecked") + T[] empty1DJavaArray2D = (T[]) java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 3, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType2D, empty1DJavaArray2D), + arrayType2D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, empty1DJavaArray2D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + empty1DJavaArray2D, + assertInstanceOf(empty1DJavaArray2D.getClass(), rowValue))); + + // Create a 3D Java array + @SuppressWarnings("unchecked") + T[][] javaArray3D = + (T[][])java.lang.reflect.Array.newInstance(javaArrayClass, 3, 3); + for (int i = 0; i < javaArray3D.length; i++) { + for (int j = 0; j < javaArray3D[i].length; j++) { + javaArray3D[i][j] = + arrayGenerator.apply((i * javaArray3D[i].length) + j); + } + } + + // Verify mapping of 3-dimensional array with values + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, javaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, javaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + javaArray3D, + assertInstanceOf(javaArray3D.getClass(), rowValue))); + + // Verify mapping of an empty 2-dimensional array + @SuppressWarnings("unchecked") + T[][] emptyJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 0, 0, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, emptyJavaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, emptyJavaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + emptyJavaArray3D, + assertInstanceOf(emptyJavaArray3D.getClass(), rowValue))); + + // Verify of a 3-dimensional array with empty 2-dimensional arrays + @SuppressWarnings("unchecked") + T[][] empty2DJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 3, 0, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, empty2DJavaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, empty2DJavaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + empty2DJavaArray3D, + assertInstanceOf(empty2DJavaArray3D.getClass(), rowValue))); + + // Verify of a 3-dimensional array with empty 1-dimensional arrays + @SuppressWarnings("unchecked") + T[][] empty1DJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 3, 3, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, empty1DJavaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, empty1DJavaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + empty1DJavaArray3D, + assertInstanceOf(empty1DJavaArray3D.getClass(), rowValue))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE " + arrayType3D.getName())); + tryAwaitExecution(connection.createStatement( + "DROP TYPE " + arrayType2D.getName())); + } + } + // 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 4f948ab56d918cb856ef032103b34ae4c745f483 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Thu, 27 Oct 2022 13:14:49 -0700 Subject: [PATCH 07/10] Support multi-dimensional array binds --- .../oracle/r2dbc/impl/OracleReadableImpl.java | 25 +- .../r2dbc/impl/OracleStatementImpl.java | 79 +- .../oracle/r2dbc/impl/TypeMappingTest.java | 832 +++++++++++------- 3 files changed, 569 insertions(+), 367 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java index ba8c406..968935c 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java @@ -546,10 +546,31 @@ private T[] convertArray(Object[] array, Class desiredType) { if (array.length == 0) return (T[]) java.lang.reflect.Array.newInstance(desiredType, 0); - Class elementType = array[0].getClass(); + // The array's component type could be Object.class; Oracle JDBC returns an + // Object[] in some cases. Search for a non-null element to determine the + // true type of type objects in the array. + Class elementType = null; + for (Object element : array) { + if (element != null) { + elementType = array[0].getClass(); + break; + } + } + + // If all array elements are null, then return an array of the desired type + // with all null elements. + if (elementType == null) { + return (T[]) java.lang.reflect.Array.newInstance( + desiredType, array.length); + } if (desiredType.isAssignableFrom(elementType)) { - return (T[])array; + // The elements are of the desired type, but the array type is something + // different, like an Object[] full of BigDecimal. + return (T[]) mapArray( + array, + length -> (T[])java.lang.reflect.Array.newInstance(desiredType, length), + Function.identity()); } else if (elementType.equals(BigDecimal.class)) { if (desiredType.equals(Boolean.class)) { diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 86b46fb..10b8e50 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -31,7 +31,6 @@ import oracle.r2dbc.OracleR2dbcTypes; import oracle.r2dbc.impl.ReactiveJdbcAdapter.JdbcReadable; import oracle.r2dbc.impl.ReadablesMetadata.OutParametersMetadataImpl; -import oracle.sql.ArrayDescriptor; import oracle.sql.INTERVALYM; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -1368,30 +1367,52 @@ private Object convertParameterBind(Parameter parameter) { } /** - * Converts a given {@code value} to a JDBC array of the given {@code type}. + * Converts a given {@code value} to a JDBC {@link Array} of the given + * {@code type}. */ private Array convertArrayBind( OracleR2dbcTypes.ArrayType type, Object value) { - final Object jdbcValue; - Class componentType = value.getClass().getComponentType(); - while (componentType.isArray()) - componentType = componentType.getComponentType(); + // TODO: createOracleArray executes a blocking database call the first + // time an OracleArray is created for a given type name. Subsequent + // creations of the same type avoid the database call using a cached type + // descriptor. If possible, rewrite this use a non-blocking call. + return fromJdbc(() -> + jdbcConnection.unwrap(OracleConnection.class) + .createOracleArray(type.getName(), convertJavaArray(value))); + } + + /** + * Converts a bind value for an ARRAY to a Java type that is supported by + * Oracle JDBC. This method handles cases for standard type mappings of + * R2DBC and extended type mapping Oracle R2DBC which are not supported by + * Oracle JDBC. + */ + private Object convertJavaArray(Object array) { + + if (array == null) + return null; - if (value instanceof byte[]) { - // Oracle JDBC does not support creating SQL arrays from a byte[], so - // convert it to an short[]. - byte[] bytes = (byte[])value; + // TODO: R2DBC drivers are only required to support ByteBuffer to + // VARBINARY (ie: RAW) conversions. However, a programmer might want to + // use byte[][] as a bind for an ARRAY of RAW. If that happens, they + // might hit this code by accident. Ideally, they can bind ByteBuffer[] + // instead. If absolutely necessary, Oracle R2DBC can do a type look up + // on the ARRAY and determine if the base type is RAW or NUMBER. + if (array instanceof byte[]) { + // Convert byte to NUMBER. Oracle JDBC does not support creating SQL + // arrays from a byte[], so convert the byte[] to an short[]. + byte[] bytes = (byte[])array; short[] shorts = new short[bytes.length]; for (int i = 0; i < bytes.length; i++) shorts[i] = (short)(0xFF & bytes[i]); - jdbcValue = shorts; + return shorts; } - else if (value instanceof ByteBuffer[]) { + else if (array instanceof ByteBuffer[]) { // Convert from R2DBC's ByteBuffer representation of binary data into // JDBC's byte[] representation - ByteBuffer[] byteBuffers = (ByteBuffer[]) value; + ByteBuffer[] byteBuffers = (ByteBuffer[]) array; byte[][] byteArrays = new byte[byteBuffers.length][]; for (int i = 0; i < byteBuffers.length; i++) { ByteBuffer byteBuffer = byteBuffers[i]; @@ -1400,12 +1421,12 @@ else if (value instanceof ByteBuffer[]) { : convertByteBufferBind(byteBuffers[i]); } - jdbcValue = byteArrays; + return byteArrays; } - else if (value instanceof Period[]) { + else if (array instanceof Period[]) { // Convert from Oracle R2DBC's Period representation of INTERVAL YEAR TO // MONTH to Oracle JDBC's INTERVALYM representation. - Period[] periods = (Period[]) value; + Period[] periods = (Period[]) array; INTERVALYM[] intervalYearToMonths = new INTERVALYM[periods.length]; for (int i = 0; i < periods.length; i++) { Period period = periods[i]; @@ -1424,19 +1445,25 @@ else if (value instanceof Period[]) { } } - jdbcValue = intervalYearToMonths; + return intervalYearToMonths; } else { - jdbcValue = value; - } + // Check if the bind value is a multi-dimensional array + Class componentType = array.getClass().getComponentType(); - // TODO: createOracleArray executes a blocking database call the first - // time an OracleArray is created for a given type name. Subsequent - // creations of the same type avoid the database call using a cached type - // descriptor. If possible, rewrite this use a non-blocking call. - return fromJdbc(() -> - jdbcConnection.unwrap(OracleConnection.class) - .createOracleArray(type.getName(), jdbcValue)); + if (componentType == null || !componentType.isArray()) + return array; + + int length = java.lang.reflect.Array.getLength(array); + Object[] converted = new Object[length]; + + for (int i = 0; i < length; i++) { + converted[i] = + convertJavaArray(java.lang.reflect.Array.get(array, i)); + } + + return converted; + } } /** diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index 5c8dc2e..4cde20c 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -30,6 +30,7 @@ import io.r2dbc.spi.Statement; import io.r2dbc.spi.Type; import oracle.r2dbc.OracleR2dbcTypes; +import oracle.r2dbc.OracleR2dbcTypes.ArrayType; import oracle.sql.json.OracleJsonFactory; import oracle.sql.json.OracleJsonObject; import org.junit.jupiter.api.Assertions; @@ -51,20 +52,13 @@ import java.time.ZoneOffset; import java.time.temporal.ChronoUnit; import java.util.Arrays; -import java.util.Collections; -import java.util.Objects; import java.util.function.BiConsumer; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.IntFunction; -import java.util.function.ObjIntConsumer; -import java.util.function.UnaryOperator; import java.util.stream.Collectors; -import java.util.stream.IntStream; import java.util.stream.Stream; import static io.r2dbc.spi.R2dbcType.NCHAR; -import static io.r2dbc.spi.R2dbcType.NCLOB; import static io.r2dbc.spi.R2dbcType.NVARCHAR; import static java.util.Arrays.asList; import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; @@ -72,7 +66,6 @@ import static oracle.r2dbc.test.DatabaseConfig.sharedConnection; import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout; import static oracle.r2dbc.util.Awaits.awaitExecution; -import static oracle.r2dbc.util.Awaits.awaitNone; import static oracle.r2dbc.util.Awaits.awaitOne; import static oracle.r2dbc.util.Awaits.awaitUpdate; import static oracle.r2dbc.util.Awaits.tryAwaitExecution; @@ -515,15 +508,26 @@ public void testArrayCharacterTypeMappings() { Connection connection = Mono.from(sharedConnection()).block(connectTimeout()); try { + ArrayType arrayType1D = OracleR2dbcTypes.arrayType("CHARACTER_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE CHARACTER_ARRAY" + " AS ARRAY(10) OF VARCHAR(100)") .execute()); + ArrayType arrayType2D = OracleR2dbcTypes.arrayType("CHARACTER_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE CHARACTER_ARRAY_2D" + + " AS ARRAY(10) OF CHARACTER_ARRAY") + .execute()); + ArrayType arrayType3D = OracleR2dbcTypes.arrayType("CHARACTER_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE CHARACTER_ARRAY_3D" + + " AS ARRAY(10) OF CHARACTER_ARRAY_2D") + .execute()); // Expect ARRAY of VARCHAR and String[] to map String[] strings = {"Hello", "Bonjour", "你好", null}; verifyArrayTypeMapping( - connection, "CHARACTER_ARRAY", String[].class, + connection, arrayType1D, arrayType2D, arrayType3D, String[].class, i -> Arrays.stream(strings) .map(string -> string == null ? null : i + "-" + string) @@ -532,6 +536,10 @@ public void testArrayCharacterTypeMappings() { Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE CHARACTER_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE CHARACTER_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE CHARACTER_ARRAY")); tryAwaitNone(connection.close()); @@ -557,11 +565,28 @@ public void testArrayNumericTypeMappings() { Connection connection = Mono.from(sharedConnection()).block(connectTimeout()); try { + + ArrayType arrayType1D = + OracleR2dbcTypes.arrayType("NUMBER_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE NUMBER_ARRAY" + " AS ARRAY(10) OF NUMBER") .execute()); + ArrayType arrayType2D = + OracleR2dbcTypes.arrayType("NUMBER_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE NUMBER_ARRAY_2D" + + " AS ARRAY(10) OF NUMBER_ARRAY") + .execute()); + + ArrayType arrayType3D = + OracleR2dbcTypes.arrayType("NUMBER_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE NUMBER_ARRAY_3D" + + " AS ARRAY(10) OF NUMBER_ARRAY_2D") + .execute()); + // Expect ARRAY of NUMBER and BigDecimal to map BigDecimal[] bigDecimals = new BigDecimal[] { BigDecimal.ZERO, @@ -571,7 +596,7 @@ public void testArrayNumericTypeMappings() { null }; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", BigDecimal[].class, + connection, arrayType1D, arrayType2D, arrayType3D, BigDecimal[].class, i -> Arrays.stream(bigDecimals) .map(bigDecimal -> @@ -585,7 +610,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and byte[] to map byte[] bytes = new byte[]{1,2,3,4,5}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", byte[].class, + connection, arrayType1D, arrayType2D, arrayType3D, byte[].class, i -> { byte[] moreBytes = new byte[bytes.length]; for (int j = 0; j < bytes.length; j++) @@ -598,7 +623,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and short[] to map short[] shorts = new short[]{1,2,3,4,5}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", short[].class, + connection, arrayType1D, arrayType2D, arrayType3D, short[].class, i -> { short[] moreShorts = new short[shorts.length]; for (int j = 0; j < shorts.length; j++) @@ -611,7 +636,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and int[] to map int[] ints = {1,2,3,4,5}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", int[].class, + connection, arrayType1D, arrayType2D, arrayType3D, int[].class, i -> { int[] moreInts = new int[ints.length]; for (int j = 0; j < ints.length; j++) @@ -624,7 +649,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and long[] to map long[] longs = {1,2,3,4,5}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", long[].class, + connection, arrayType1D, arrayType2D, arrayType3D, long[].class, i -> { long[] moreLongs = new long[longs.length]; for (int j = 0; j < longs.length; j++) @@ -637,7 +662,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and float[] to map float[] floats = {1.1f,2.2f,3.3f,4.4f,5.5f}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", float[].class, + connection, arrayType1D, arrayType2D, arrayType3D, float[].class, i -> { float[] moreFloats = new float[floats.length]; for (int j = 0; j < floats.length; j++) @@ -650,7 +675,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and double[] to map double[] doubles = {1.1,2.2,3.3,4.4,5.5}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", double[].class, + connection, arrayType1D, arrayType2D, arrayType3D, double[].class, i -> { double[] moreDoubles = new double[doubles.length]; for (int j = 0; j < doubles.length; j++) @@ -663,7 +688,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and Byte[] to map Byte[] byteObjects = new Byte[]{1,2,3,4,5,null}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", Byte[].class, + connection, arrayType1D, arrayType2D, arrayType3D, Byte[].class, i -> Arrays.stream(byteObjects) .map(byteObject -> @@ -675,7 +700,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and Short[] to map Short[] shortObjects = new Short[]{1,2,3,4,5,null}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", Short[].class, + connection, arrayType1D, arrayType2D, arrayType3D, Short[].class, i -> Arrays.stream(shortObjects) .map(shortObject -> @@ -687,7 +712,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and Integer[] to map Integer[] intObjects = {1,2,3,4,5,null}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", Integer[].class, + connection, arrayType1D, arrayType2D, arrayType3D, Integer[].class, i -> Arrays.stream(intObjects) .map(intObject -> @@ -699,7 +724,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and Long[] to map Long[] longObjects = {1L,2L,3L,4L,5L,null}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", Long[].class, + connection, arrayType1D, arrayType2D, arrayType3D, Long[].class, i -> Arrays.stream(longObjects) .map(longObject -> @@ -711,7 +736,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and Float[] to map Float[] floatObjects = {1.1f,2.2f,3.3f,4.4f,5.5f,null}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", Float[].class, + connection, arrayType1D, arrayType2D, arrayType3D, Float[].class, i -> Arrays.stream(floatObjects) .map(floatObject -> @@ -723,7 +748,7 @@ public void testArrayNumericTypeMappings() { // Expect ARRAY of NUMBER and Double[] to map Double[] doubleObjects = {1.1,2.2,3.3,4.4,5.5,null}; verifyArrayTypeMapping( - connection, "NUMBER_ARRAY", Double[].class, + connection, arrayType1D, arrayType2D, arrayType3D, Double[].class, i -> Arrays.stream(doubleObjects) .map(doubleObject -> @@ -733,6 +758,8 @@ public void testArrayNumericTypeMappings() { Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement("DROP TYPE NUMBER_ARRAY_3D")); + tryAwaitExecution(connection.createStatement("DROP TYPE NUMBER_ARRAY_2D")); tryAwaitExecution(connection.createStatement("DROP TYPE NUMBER_ARRAY")); tryAwaitNone(connection.close()); } @@ -756,37 +783,55 @@ public void testArrayBooleanTypeMappings() { Connection connection = Mono.from(sharedConnection()).block(connectTimeout()); try { + ArrayType arrayType1D = OracleR2dbcTypes.arrayType("BOOLEAN_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE BOOLEAN_ARRAY" + " AS ARRAY(10) OF NUMBER") .execute()); - Type booleanArrayType = OracleR2dbcTypes.arrayType("BOOLEAN_ARRAY"); + ArrayType arrayType2D = OracleR2dbcTypes.arrayType("BOOLEAN_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE BOOLEAN_ARRAY_2D" + + " AS ARRAY(10) OF BOOLEAN_ARRAY") + .execute()); + ArrayType arrayType3D = OracleR2dbcTypes.arrayType("BOOLEAN_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE BOOLEAN_ARRAY_3D" + + " AS ARRAY(10) OF BOOLEAN_ARRAY_2D") + .execute()); // 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))); + verifyArrayTypeMapping( + connection, arrayType1D, arrayType2D, arrayType3D, + boolean[].class, + i -> { + boolean[] moreBooleans = new boolean[booleans.length]; + for (int j = 0; j < booleans.length; j++) + moreBooleans[j] = booleans[i % booleans.length]; + return moreBooleans; + }, + false, + Assertions::assertArrayEquals); // 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))); + verifyArrayTypeMapping( + connection, arrayType1D, arrayType2D, arrayType3D, + Boolean[].class, + i -> { + Boolean[] moreBooleans = new Boolean[booleanObjects.length]; + for (int j = 0; j < booleanObjects.length; j++) + moreBooleans[j] = booleanObjects[i % booleanObjects.length]; + return moreBooleans; + }, + false, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE BOOLEAN_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE BOOLEAN_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE BOOLEAN_ARRAY")); tryAwaitNone(connection.close()); @@ -811,11 +856,21 @@ public void testArrayBinaryTypeMappings() { Connection connection = Mono.from(sharedConnection()).block(connectTimeout()); try { + ArrayType arrayType1D = OracleR2dbcTypes.arrayType("BINARY_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE BINARY_ARRAY" + " AS ARRAY(10) OF RAW(100)") .execute()); - Type binaryArrayType = OracleR2dbcTypes.arrayType("BINARY_ARRAY"); + ArrayType arrayType2D = OracleR2dbcTypes.arrayType("BINARY_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE BINARY_ARRAY_2D" + + " AS ARRAY(10) OF BINARY_ARRAY") + .execute()); + ArrayType arrayType3D = OracleR2dbcTypes.arrayType("BINARY_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE BINARY_ARRAY_3D" + + " AS ARRAY(10) OF BINARY_ARRAY_2D") + .execute()); // Expect ARRAY of RAW and ByteBuffer[] to map ByteBuffer[] byteBuffers = { @@ -827,16 +882,27 @@ public void testArrayBinaryTypeMappings() { .putInt(6).putInt(7).putInt(8).clear(), null }; - verifyTypeMapping( - connection, - Parameters.in(binaryArrayType, byteBuffers), - binaryArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - byteBuffers, - assertInstanceOf(ByteBuffer[].class, rowValue))); + verifyArrayTypeMapping(connection, + arrayType1D, arrayType2D, arrayType3D, + ByteBuffer[].class, + i -> Arrays.stream(byteBuffers) + .map(byteBuffer -> + byteBuffer == null + ? null + : ByteBuffer.allocate(3 * Integer.BYTES) + .putInt(byteBuffer.get(0) + i) + .putInt(byteBuffer.get(1) + i) + .putInt(byteBuffer.get(2) + i) + .clear()) + .toArray(ByteBuffer[]::new), + true, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE BINARY_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE BINARY_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE BINARY_ARRAY")); tryAwaitNone(connection.close()); @@ -872,14 +938,24 @@ public void testArrayDatetimeTypeMappings() { OffsetDateTime dateTimeValue = OffsetDateTime.of(2038, 10, 23, 9, 42, 1, 1, ZoneOffset.ofHours(-5)); - // Expect ARRAY of DATE and LocalDateTime[] to map + ArrayType dateArrayType1D = OracleR2dbcTypes.arrayType("DATE_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE DATE_ARRAY" + " AS ARRAY(10) OF DATE") .execute()); + ArrayType dateArrayType2D = OracleR2dbcTypes.arrayType("DATE_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE DATE_ARRAY_2D" + + " AS ARRAY(10) OF DATE_ARRAY") + .execute()); + ArrayType dateArrayType3D = OracleR2dbcTypes.arrayType("DATE_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE DATE_ARRAY_3D" + + " AS ARRAY(10) OF DATE_ARRAY_2D") + .execute()); try { - Type dateArrayType = OracleR2dbcTypes.arrayType("DATE_ARRAY"); + // Expect ARRAY of DATE and LocalDateTime[] to map LocalDateTime[] localDateTimes = { dateTimeValue.minus(Period.ofDays(7)) .toLocalDateTime() @@ -891,59 +967,89 @@ public void testArrayDatetimeTypeMappings() { .truncatedTo(ChronoUnit.SECONDS), null }; - verifyTypeMapping( - connection, - Parameters.in(dateArrayType, localDateTimes), - dateArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - localDateTimes, - assertInstanceOf(LocalDateTime[].class, rowValue))); - + verifyArrayTypeMapping( + connection, dateArrayType1D, dateArrayType2D, dateArrayType3D, + LocalDateTime[].class, + i -> + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null + ? null + : localDateTime.plus(Period.ofDays(i))) + .toArray(LocalDateTime[]::new), + true, + Assertions::assertArrayEquals); + + // Expect ARRAY of DATE and LocalDate[] to map 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))); - + verifyArrayTypeMapping( + connection, dateArrayType1D, dateArrayType2D, dateArrayType3D, + LocalDate[].class, + i -> + Arrays.stream(localDates) + .map(localDate -> + localDate == null + ? null + : localDate.plus(Period.ofDays(i))) + .toArray(LocalDate[]::new), + false, + Assertions::assertArrayEquals); + + // Expect ARRAY of DATE and LocalTime[] to map 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))); + verifyArrayTypeMapping( + connection, dateArrayType1D, dateArrayType2D, dateArrayType3D, + LocalTime[].class, + i -> + Arrays.stream(localTimes) + .map(localTime -> + localTime == null + ? null + : localTime.plus(Duration.ofMinutes(i))) + .toArray(LocalTime[]::new), + false, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE DATE_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE DATE_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE DATE_ARRAY")); } // Expect ARRAY of TIMESTAMP and LocalDateTime[] to map + ArrayType timestampArrayType1D = + OracleR2dbcTypes.arrayType("TIMESTAMP_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE TIMESTAMP_ARRAY" + " AS ARRAY(10) OF TIMESTAMP") .execute()); + ArrayType timestampArrayType2D = + OracleR2dbcTypes.arrayType("TIMESTAMP_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_ARRAY_2D" + + " AS ARRAY(10) OF TIMESTAMP_ARRAY") + .execute()); + ArrayType timestampArrayType3D = + OracleR2dbcTypes.arrayType("TIMESTAMP_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_ARRAY_3D" + + " AS ARRAY(10) OF TIMESTAMP_ARRAY_2D") + .execute()); try { - Type timestampArrayType = OracleR2dbcTypes.arrayType("TIMESTAMP_ARRAY"); + // Expect ARRAY of TIMESTAMP and LocalDateTime[] to map LocalDateTime[] localDateTimes = { dateTimeValue.minus(Duration.ofMillis(100)) .toLocalDateTime(), @@ -952,60 +1058,89 @@ public void testArrayDatetimeTypeMappings() { .toLocalDateTime(), null }; - - verifyTypeMapping( + verifyArrayTypeMapping( connection, - Parameters.in(timestampArrayType, localDateTimes), - timestampArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - localDateTimes, - assertInstanceOf(LocalDateTime[].class, rowValue))); - + timestampArrayType1D, + timestampArrayType2D, + timestampArrayType3D, + LocalDateTime[].class, + i -> + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null + ? null + : localDateTime.plus(Period.ofDays(i))) + .toArray(LocalDateTime[]::new), + true, + Assertions::assertArrayEquals); + + // Expect ARRAY of TIMESTAMP and LocalDate[] to map 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))); - + verifyArrayTypeMapping( + connection, timestampArrayType1D, timestampArrayType2D, timestampArrayType3D, + LocalDate[].class, + i -> + Arrays.stream(localDates) + .map(localDate -> + localDate == null + ? null + : localDate.plus(Period.ofDays(i))) + .toArray(LocalDate[]::new), + false, + Assertions::assertArrayEquals); + + // Expect ARRAY of TIMESTAMP and LocalTime[] to map 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))); + verifyArrayTypeMapping( + connection, timestampArrayType1D, timestampArrayType2D, timestampArrayType3D, + LocalTime[].class, + i -> + Arrays.stream(localTimes) + .map(localTime -> + localTime == null + ? null + : localTime.plus(Duration.ofMinutes(i))) + .toArray(LocalTime[]::new), + false, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE TIMESTAMP_ARRAY")); } - // Expect ARRAY of TIMESTAMP WITH TIME ZONE and OffsetDateTime[] to map + ArrayType timestampWithTimeZoneArrayType1D = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_TIME_ZONE_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY" + " AS ARRAY(10) OF TIMESTAMP WITH TIME ZONE") .execute()); + ArrayType timestampWithTimeZoneArrayType2D = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_TIME_ZONE_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY_2D" + + " AS ARRAY(10) OF TIMESTAMP_WITH_TIME_ZONE_ARRAY") + .execute()); + ArrayType timestampWithTimeZoneArrayType3D = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_TIME_ZONE_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY_3D" + + " AS ARRAY(10) OF TIMESTAMP_WITH_TIME_ZONE_ARRAY_2D") + .execute()); try { - Type timestampWithTimeZoneArrayType = - OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_TIME_ZONE_ARRAY"); - + // Expect ARRAY of TIMESTAMP WITH TIME ZONE and OffsetDateTime[] to map OffsetDateTime[] offsetDateTimes = { dateTimeValue.minus(Duration.ofMillis(100)) .toLocalDateTime() @@ -1017,30 +1152,52 @@ public void testArrayDatetimeTypeMappings() { .atOffset(ZoneOffset.ofHours(8)), null }; - - verifyTypeMapping( + verifyArrayTypeMapping( connection, - Parameters.in(timestampWithTimeZoneArrayType, offsetDateTimes), - timestampWithTimeZoneArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - offsetDateTimes, - assertInstanceOf(OffsetDateTime[].class, rowValue))); + timestampWithTimeZoneArrayType1D, + timestampWithTimeZoneArrayType2D, + timestampWithTimeZoneArrayType3D, + OffsetDateTime[].class, + i -> + Arrays.stream(offsetDateTimes) + .map(offsetDateTime -> + offsetDateTime == null + ? null + : offsetDateTime.plus(Duration.ofMinutes(i))) + .toArray(OffsetDateTime[]::new), + true, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE TIMESTAMP_WITH_TIME_ZONE_ARRAY")); } - // Expect ARRAY of TIMESTAMP WITH LOCAL TIME ZONE and LocalDateTime[] to - // map + ArrayType timestampWithLocalTimeZoneArrayType1D = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY" + " AS ARRAY(10) OF TIMESTAMP WITH LOCAL TIME ZONE") .execute()); + ArrayType timestampWithLocalTimeZoneArrayType2D = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY_2D" + + " AS ARRAY(10) OF TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY") + .execute()); + ArrayType timestampWithLocalTimeZoneArrayType3D = + OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY_3D" + + " AS ARRAY(10) OF TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY_2D") + .execute()); try { - Type timestampWithLocalTimeZoneArrayType = - OracleR2dbcTypes.arrayType("TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY"); + // Expect ARRAY of TIMESTAMP WITH LOCAL TIME ZONE and LocalDateTime[] to + // map LocalDateTime[] localDateTimes = { dateTimeValue.minus(Duration.ofMillis(100)) .toLocalDateTime(), @@ -1049,60 +1206,52 @@ public void testArrayDatetimeTypeMappings() { .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( + verifyArrayTypeMapping( 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))); + timestampWithLocalTimeZoneArrayType1D, + timestampWithLocalTimeZoneArrayType2D, + timestampWithLocalTimeZoneArrayType3D, + LocalDateTime[].class, + i -> + Arrays.stream(localDateTimes) + .map(localDateTime -> + localDateTime == null + ? null + : localDateTime.plus(Period.ofDays(i))) + .toArray(LocalDateTime[]::new), + true, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE TIMESTAMP_WITH_LOCAL_TIME_ZONE_ARRAY")); } - // Expect ARRAY of INTERVAL YEAR TO MONTH and Period[] to map + ArrayType intervalYearToMonthArrayType1D = + OracleR2dbcTypes.arrayType("INTERVAL_YEAR_TO_MONTH_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE INTERVAL_YEAR_TO_MONTH_ARRAY" + " AS ARRAY(10) OF INTERVAL YEAR TO MONTH") .execute()); + ArrayType intervalYearToMonthArrayType2D = + OracleR2dbcTypes.arrayType("INTERVAL_YEAR_TO_MONTH_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE INTERVAL_YEAR_TO_MONTH_ARRAY_2D" + + " AS ARRAY(10) OF INTERVAL_YEAR_TO_MONTH_ARRAY") + .execute()); + ArrayType intervalYearToMonthArrayType3D = + OracleR2dbcTypes.arrayType("INTERVAL_YEAR_TO_MONTH_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE INTERVAL_YEAR_TO_MONTH_ARRAY_3D" + + " AS ARRAY(10) OF INTERVAL_YEAR_TO_MONTH_ARRAY_2D") + .execute()); try { - Type intervalYearToMonthArrayType = - OracleR2dbcTypes.arrayType("INTERVAL_YEAR_TO_MONTH_ARRAY"); + // Expect ARRAY of INTERVAL YEAR TO MONTH and Period[] to map Period[] periods = { Period.of(1, 2, 0), Period.of(3, 4, 0), @@ -1110,27 +1259,50 @@ public void testArrayDatetimeTypeMappings() { null }; - verifyTypeMapping( + verifyArrayTypeMapping( connection, - Parameters.in(intervalYearToMonthArrayType, periods), - intervalYearToMonthArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - periods, - assertInstanceOf(Period[].class, rowValue))); + intervalYearToMonthArrayType1D, + intervalYearToMonthArrayType2D, + intervalYearToMonthArrayType3D, + Period[].class, + i -> + Arrays.stream(periods) + .map(period -> + period == null + ? null + : period.plus(Period.ofYears(i))) + .toArray(Period[]::new), + true, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE INTERVAL_YEAR_TO_MONTH_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE INTERVAL_YEAR_TO_MONTH_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE INTERVAL_YEAR_TO_MONTH_ARRAY")); } + ArrayType intervalDayToSecondArrayType1D = + OracleR2dbcTypes.arrayType("INTERVAL_DAY_TO_SECOND_ARRAY"); awaitOne(connection.createStatement( "CREATE OR REPLACE TYPE INTERVAL_DAY_TO_SECOND_ARRAY" + " AS ARRAY(10) OF INTERVAL DAY TO SECOND") .execute()); + ArrayType intervalDayToSecondArrayType2D = + OracleR2dbcTypes.arrayType("INTERVAL_DAY_TO_SECOND_ARRAY_2D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE INTERVAL_DAY_TO_SECOND_ARRAY_2D" + + " AS ARRAY(10) OF INTERVAL_DAY_TO_SECOND_ARRAY") + .execute()); + ArrayType intervalDayToSecondArrayType3D = + OracleR2dbcTypes.arrayType("INTERVAL_DAY_TO_SECOND_ARRAY_3D"); + awaitOne(connection.createStatement( + "CREATE OR REPLACE TYPE INTERVAL_DAY_TO_SECOND_ARRAY_3D" + + " AS ARRAY(10) OF INTERVAL_DAY_TO_SECOND_ARRAY_2D") + .execute()); try { - Type intervalDayToSecondArrayType = - OracleR2dbcTypes.arrayType("INTERVAL_DAY_TO_SECOND_ARRAY"); Duration[] durations = { Duration.ofDays(1), Duration.ofHours(2), @@ -1138,16 +1310,27 @@ public void testArrayDatetimeTypeMappings() { Duration.ofSeconds(4), null }; - verifyTypeMapping( + verifyArrayTypeMapping( connection, - Parameters.in(intervalDayToSecondArrayType, durations), - intervalDayToSecondArrayType.getName(), - (ignored, rowValue) -> - assertArrayEquals( - durations, - assertInstanceOf(Duration[].class, rowValue))); + intervalDayToSecondArrayType1D, + intervalDayToSecondArrayType2D, + intervalDayToSecondArrayType3D, + Duration[].class, + i -> + Arrays.stream(durations) + .map(duration -> + duration == null + ? null + : duration.plus(Duration.ofSeconds(i))) + .toArray(Duration[]::new), + true, + Assertions::assertArrayEquals); } finally { + tryAwaitExecution(connection.createStatement( + "DROP TYPE INTERVAL_DAY_TO_SECOND_ARRAY_3D")); + tryAwaitExecution(connection.createStatement( + "DROP TYPE INTERVAL_DAY_TO_SECOND_ARRAY_2D")); tryAwaitExecution(connection.createStatement( "DROP TYPE INTERVAL_DAY_TO_SECOND_ARRAY")); } @@ -1170,29 +1353,20 @@ public void testArrayDatetimeTypeMappings() { * */ private static void verifyArrayTypeMapping( - Connection connection, String typeName, boolean isDefaultMapping, - Class javaArrayType, IntFunction arrayGenerator) { - verifyArrayTypeMapping( - connection, typeName, javaArrayType, arrayGenerator, - isDefaultMapping, - (expectedValue, rowValue) -> - assertArrayEquals((Object[]) expectedValue, (Object[]) rowValue)); - } - - private static void verifyArrayTypeMapping( - Connection connection, String typeName, + Connection connection, + ArrayType arrayType1D, + ArrayType arrayType2D, + ArrayType arrayType3D, Class javaArrayClass, IntFunction arrayGenerator, boolean isDefaultMapping, BiConsumer equalsAssertion) { - Type arrayType = OracleR2dbcTypes.arrayType(typeName); - // Verify mapping of 1-dimensional array with values T javaArray1D = arrayGenerator.apply(0); verifyTypeMapping( connection, - Parameters.in(arrayType, javaArray1D), - arrayType.getName(), + Parameters.in(arrayType1D, javaArray1D), + arrayType1D.getName(), isDefaultMapping ? row -> row.get(0) : row -> row.get(0, javaArray1D.getClass()), @@ -1207,8 +1381,8 @@ private static void verifyArrayTypeMapping( javaArrayClass.getComponentType(), 0); verifyTypeMapping( connection, - Parameters.in(arrayType, emptyJavaArray), - arrayType.getName(), + Parameters.in(arrayType1D, emptyJavaArray), + arrayType1D.getName(), isDefaultMapping ? row -> row.get(0) : row -> row.get(0, emptyJavaArray.getClass()), @@ -1218,150 +1392,130 @@ private static void verifyArrayTypeMapping( java.lang.reflect.Array.getLength( assertInstanceOf(javaArrayClass, rowValue)))); - // Create 2 dimensional and 3 dimensional ARRAY types - Type arrayType2D = OracleR2dbcTypes.arrayType(arrayType.getName() + "_2D"); - Type arrayType3D = OracleR2dbcTypes.arrayType(arrayType.getName() + "_3D"); - awaitOne(connection.createStatement( - "CREATE OR REPLACE TYPE " + arrayType2D.getName() + - " AS ARRAY(10) OF " + arrayType.getName()) - .execute()); - awaitOne(connection.createStatement( - "CREATE OR REPLACE TYPE " + arrayType3D.getName() + - " AS ARRAY(10) OF " + arrayType2D.getName()) - .execute()); - try { + // Create a 2D Java array + @SuppressWarnings("unchecked") + T[] javaArray2D = + (T[]) java.lang.reflect.Array.newInstance(javaArrayClass, 3); + for (int i = 0; i < javaArray2D.length; i++) { + javaArray2D[i] = arrayGenerator.apply(i); + } - // Create a 2D Java array - @SuppressWarnings("unchecked") - T[] javaArray2D = - (T[]) java.lang.reflect.Array.newInstance(javaArrayClass, 3); - for (int i = 0; i < javaArray2D.length; i++) { - javaArray2D[i] = arrayGenerator.apply(i); - } + // Verify mapping of 2-dimensional array with values + verifyTypeMapping( + connection, + Parameters.in(arrayType2D, javaArray2D), + arrayType2D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, javaArray2D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + javaArray2D, + assertInstanceOf(javaArray2D.getClass(), rowValue))); - // Verify mapping of 2-dimensional array with values - verifyTypeMapping( - connection, - Parameters.in(arrayType2D, javaArray2D), - arrayType2D.getName(), - isDefaultMapping - ? row -> row.get(0) - : row -> row.get(0, arrayType2D.getClass()), - (ignored, rowValue) -> - assertArrayEquals( - javaArray2D, - assertInstanceOf(javaArray2D.getClass(), rowValue))); - - // Verify mapping of an empty 2-dimensional array - @SuppressWarnings("unchecked") - T[] emptyJavaArray2D = (T[]) java.lang.reflect.Array.newInstance( - javaArrayClass.getComponentType(), 0, 0); - verifyTypeMapping( - connection, - Parameters.in(arrayType2D, emptyJavaArray2D), - arrayType2D.getName(), - isDefaultMapping - ? row -> row.get(0) - : row -> row.get(0, emptyJavaArray2D.getClass()), - (ignored, rowValue) -> - assertArrayEquals( - emptyJavaArray2D, - assertInstanceOf(emptyJavaArray2D.getClass(), rowValue))); - - // Verify of a 2-dimensional array with empty 1-dimensional arrays - @SuppressWarnings("unchecked") - T[] empty1DJavaArray2D = (T[]) java.lang.reflect.Array.newInstance( - javaArrayClass.getComponentType(), 3, 0); - verifyTypeMapping( - connection, - Parameters.in(arrayType2D, empty1DJavaArray2D), - arrayType2D.getName(), - isDefaultMapping - ? row -> row.get(0) - : row -> row.get(0, empty1DJavaArray2D.getClass()), - (ignored, rowValue) -> - assertArrayEquals( - empty1DJavaArray2D, - assertInstanceOf(empty1DJavaArray2D.getClass(), rowValue))); - - // Create a 3D Java array - @SuppressWarnings("unchecked") - T[][] javaArray3D = - (T[][])java.lang.reflect.Array.newInstance(javaArrayClass, 3, 3); - for (int i = 0; i < javaArray3D.length; i++) { - for (int j = 0; j < javaArray3D[i].length; j++) { - javaArray3D[i][j] = - arrayGenerator.apply((i * javaArray3D[i].length) + j); - } - } + // Verify mapping of an empty 2-dimensional array + @SuppressWarnings("unchecked") + T[] emptyJavaArray2D = (T[]) java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 0, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType2D, emptyJavaArray2D), + arrayType2D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, emptyJavaArray2D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + emptyJavaArray2D, + assertInstanceOf(emptyJavaArray2D.getClass(), rowValue))); - // Verify mapping of 3-dimensional array with values - verifyTypeMapping( - connection, - Parameters.in(arrayType3D, javaArray3D), - arrayType3D.getName(), - isDefaultMapping - ? row -> row.get(0) - : row -> row.get(0, javaArray3D.getClass()), - (ignored, rowValue) -> - assertArrayEquals( - javaArray3D, - assertInstanceOf(javaArray3D.getClass(), rowValue))); - - // Verify mapping of an empty 2-dimensional array - @SuppressWarnings("unchecked") - T[][] emptyJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( - javaArrayClass.getComponentType(), 0, 0, 0); - verifyTypeMapping( - connection, - Parameters.in(arrayType3D, emptyJavaArray3D), - arrayType3D.getName(), - isDefaultMapping - ? row -> row.get(0) - : row -> row.get(0, emptyJavaArray3D.getClass()), - (ignored, rowValue) -> - assertArrayEquals( - emptyJavaArray3D, - assertInstanceOf(emptyJavaArray3D.getClass(), rowValue))); - - // Verify of a 3-dimensional array with empty 2-dimensional arrays - @SuppressWarnings("unchecked") - T[][] empty2DJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( - javaArrayClass.getComponentType(), 3, 0, 0); - verifyTypeMapping( - connection, - Parameters.in(arrayType3D, empty2DJavaArray3D), - arrayType3D.getName(), - isDefaultMapping - ? row -> row.get(0) - : row -> row.get(0, empty2DJavaArray3D.getClass()), - (ignored, rowValue) -> - assertArrayEquals( - empty2DJavaArray3D, - assertInstanceOf(empty2DJavaArray3D.getClass(), rowValue))); - - // Verify of a 3-dimensional array with empty 1-dimensional arrays - @SuppressWarnings("unchecked") - T[][] empty1DJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( - javaArrayClass.getComponentType(), 3, 3, 0); - verifyTypeMapping( - connection, - Parameters.in(arrayType3D, empty1DJavaArray3D), - arrayType3D.getName(), - isDefaultMapping - ? row -> row.get(0) - : row -> row.get(0, empty1DJavaArray3D.getClass()), - (ignored, rowValue) -> - assertArrayEquals( - empty1DJavaArray3D, - assertInstanceOf(empty1DJavaArray3D.getClass(), rowValue))); - } - finally { - tryAwaitExecution(connection.createStatement( - "DROP TYPE " + arrayType3D.getName())); - tryAwaitExecution(connection.createStatement( - "DROP TYPE " + arrayType2D.getName())); + // Verify of a 2-dimensional array with empty 1-dimensional arrays + @SuppressWarnings("unchecked") + T[] empty1DJavaArray2D = (T[]) java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 3, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType2D, empty1DJavaArray2D), + arrayType2D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, empty1DJavaArray2D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + empty1DJavaArray2D, + assertInstanceOf(empty1DJavaArray2D.getClass(), rowValue))); + + // Create a 3D Java array + @SuppressWarnings("unchecked") + T[][] javaArray3D = + (T[][])java.lang.reflect.Array.newInstance(javaArrayClass, 3, 3); + for (int i = 0; i < javaArray3D.length; i++) { + for (int j = 0; j < javaArray3D[i].length; j++) { + javaArray3D[i][j] = + arrayGenerator.apply((i * javaArray3D[i].length) + j); + } } + + // Verify mapping of 3-dimensional array with values + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, javaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, javaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + javaArray3D, + assertInstanceOf(javaArray3D.getClass(), rowValue))); + + // Verify mapping of an empty 2-dimensional array + @SuppressWarnings("unchecked") + T[][] emptyJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 0, 0, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, emptyJavaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, emptyJavaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + emptyJavaArray3D, + assertInstanceOf(emptyJavaArray3D.getClass(), rowValue))); + + // Verify of a 3-dimensional array with empty 2-dimensional arrays + @SuppressWarnings("unchecked") + T[][] empty2DJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 3, 0, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, empty2DJavaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, empty2DJavaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + empty2DJavaArray3D, + assertInstanceOf(empty2DJavaArray3D.getClass(), rowValue))); + + // Verify of a 3-dimensional array with empty 1-dimensional arrays + @SuppressWarnings("unchecked") + T[][] empty1DJavaArray3D = (T[][])java.lang.reflect.Array.newInstance( + javaArrayClass.getComponentType(), 3, 3, 0); + verifyTypeMapping( + connection, + Parameters.in(arrayType3D, empty1DJavaArray3D), + arrayType3D.getName(), + isDefaultMapping + ? row -> row.get(0) + : row -> row.get(0, empty1DJavaArray3D.getClass()), + (ignored, rowValue) -> + assertArrayEquals( + empty1DJavaArray3D, + assertInstanceOf(empty1DJavaArray3D.getClass(), rowValue))); } // TODO: More tests for JDBC 4.3 mappings like BigInteger to BIGINT, From c16319b8626a044487e813d672409902f7b480f4 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Thu, 27 Oct 2022 13:18:41 -0700 Subject: [PATCH 08/10] Remove redundant cast --- src/test/java/oracle/r2dbc/impl/TypeMappingTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index 4cde20c..88fe2ef 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -653,7 +653,7 @@ public void testArrayNumericTypeMappings() { i -> { long[] moreLongs = new long[longs.length]; for (int j = 0; j < longs.length; j++) - moreLongs[j] = (long)(longs[j] + i); + moreLongs[j] = longs[j] + i; return moreLongs; }, false, From d9306d7d80f1bc22048e8d58a97508cb44867ff0 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Thu, 27 Oct 2022 14:08:09 -0700 Subject: [PATCH 09/10] Add and improve JavaDocs --- .../java/oracle/r2dbc/OracleR2dbcTypes.java | 4 +- .../oracle/r2dbc/impl/OracleReadableImpl.java | 137 +++++++++--------- .../r2dbc/impl/OracleStatementImpl.java | 19 ++- 3 files changed, 82 insertions(+), 78 deletions(-) diff --git a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java index 1124a3a..e4c73a1 100644 --- a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java +++ b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java @@ -106,8 +106,8 @@ private OracleR2dbcTypes() {} * {@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}. + * 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
    * Publisher arrayBindExample(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 static U[] 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 Publisher bind() { + 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