- * 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}.
*
@@ -1001,41 +1022,27 @@ 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(() -> {
+
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
@@ -1044,6 +1051,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
@@ -1216,14 +1289,23 @@ private Publisher closeStatement() {
* @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);
+ }
});
}
@@ -1253,7 +1335,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);
@@ -1269,6 +1351,123 @@ 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 {@link Array} of the given
+ * {@code type}.
+ */
+ private Array convertArrayBind(
+ OracleR2dbcTypes.ArrayType type, Object 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(), 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;
+
+ // 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]);
+
+ return shorts;
+ }
+ else if (array instanceof ByteBuffer[]) {
+ // Convert from R2DBC's ByteBuffer representation of binary data into
+ // JDBC's byte[] representation
+ ByteBuffer[] byteBuffers = (ByteBuffer[]) array;
+ 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]);
+ }
+
+ return byteArrays;
+ }
+ 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[]) array;
+ 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);
+ }
+ }
+
+ return intervalYearToMonths;
+ }
+ else {
+ // Check if the bind value is a multi-dimensional array
+ Class> componentType = array.getClass().getComponentType();
+
+ 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;
+ }
+ }
+
/**
* Converts an R2DBC Blob to a JDBC Blob. The returned {@code Publisher}
* asynchronously writes the {@code r2dbcBlob's} content to a JDBC Blob and
@@ -1407,7 +1606,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);
+ }
}
});
}
@@ -1419,6 +1630,7 @@ protected Publisher executeJdbc() {
Mono.just(createCallResult(
dependentCounter,
createOutParameters(
+ fromJdbc(preparedStatement::getConnection),
dependentCounter,
new JdbcOutParameters(), metadata, adapter),
adapter)));
diff --git a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java
index b2ad059..8bffe5c 100644
--- a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java
+++ b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java
@@ -133,6 +133,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(...)
@@ -160,8 +161,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)
);
/**
@@ -216,9 +228,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/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)}
diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java
index ed63857..8812509 100644
--- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java
+++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java
@@ -23,17 +23,13 @@
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactories;
-import io.r2dbc.spi.ConnectionFactoryOptions;
-import io.r2dbc.spi.Parameter;
import io.r2dbc.spi.Parameters;
import io.r2dbc.spi.R2dbcException;
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;
@@ -47,11 +43,12 @@
import java.sql.RowId;
import java.sql.SQLWarning;
import java.util.ArrayList;
-import java.util.Collection;
+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;
@@ -64,19 +61,13 @@
import java.util.stream.IntStream;
import java.util.stream.Stream;
-import static java.lang.String.format;
import static java.util.Arrays.asList;
import static oracle.r2dbc.test.DatabaseConfig.connectTimeout;
import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions;
-import static oracle.r2dbc.test.DatabaseConfig.host;
import static oracle.r2dbc.test.DatabaseConfig.newConnection;
-import static oracle.r2dbc.test.DatabaseConfig.password;
-import static oracle.r2dbc.test.DatabaseConfig.port;
-import static oracle.r2dbc.test.DatabaseConfig.serviceName;
import static oracle.r2dbc.test.DatabaseConfig.sharedConnection;
import static oracle.r2dbc.test.DatabaseConfig.showErrors;
import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout;
-import static oracle.r2dbc.test.DatabaseConfig.user;
import static oracle.r2dbc.util.Awaits.awaitError;
import static oracle.r2dbc.util.Awaits.awaitExecution;
import static oracle.r2dbc.util.Awaits.awaitMany;
@@ -1286,7 +1277,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());
@@ -1300,7 +1291,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 ->
@@ -1314,7 +1305,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()));
@@ -1327,7 +1318,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 ->
@@ -1381,8 +1372,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()));
@@ -1396,8 +1387,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 ->
@@ -1413,8 +1404,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)),
@@ -1429,8 +1420,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 ->
@@ -2431,39 +2422,306 @@ public void testMultipleRefCursorOut() {
}
}
- // 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;"));
- InOutParameter(Object value) {
- this(value, new Type.InferredType() {
+ class TestRow {
+ Long id;
+ int[] value;
+ TestRow(Long id, int[] value) {
+ this.id = id;
+ this.value = value;
+ }
@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);
}
- });
- }
+ }
- InOutParameter(Object value, Type type) {
- this.value = value;
- this.type = type;
+ 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)));
+
+ 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());
}
+ }
- @Override
- public Type getType() {
- return type;
+ /**
+ * 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;"));
+
+ 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());
}
+ }
- @Override
- public Object getValue() {
- return value;
+ /**
+ * 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;");
+
+ // 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());
}
}
diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java
index 1f8907b..88fe2ef 100644
--- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java
+++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java
@@ -24,9 +24,13 @@
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.r2dbc.OracleR2dbcTypes.ArrayType;
import oracle.sql.json.OracleJsonFactory;
import oracle.sql.json.OracleJsonObject;
import org.junit.jupiter.api.Assertions;
@@ -40,19 +44,21 @@
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.function.BiConsumer;
import java.util.function.Function;
+import java.util.function.IntFunction;
import java.util.stream.Collectors;
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;
@@ -62,10 +68,12 @@
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.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 +490,1034 @@ 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 {
+ 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, arrayType1D, arrayType2D, arrayType3D, String[].class,
+ i ->
+ Arrays.stream(strings)
+ .map(string -> string == null ? null : i + "-" + string)
+ .toArray(String[]::new),
+ true,
+ 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());
+ }
+ }
+
+ /**
+ *
+ * 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 {
+
+ 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,
+ new BigDecimal("1.23"),
+ new BigDecimal("4.56"),
+ new BigDecimal("7.89"),
+ null
+ };
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, long[].class,
+ i -> {
+ long[] moreLongs = new long[longs.length];
+ for (int j = 0; j < longs.length; j++)
+ moreLongs[j] = 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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};
+ verifyArrayTypeMapping(
+ connection, arrayType1D, arrayType2D, arrayType3D, 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_3D"));
+ tryAwaitExecution(connection.createStatement("DROP TYPE NUMBER_ARRAY_2D"));
+ tryAwaitExecution(connection.createStatement("DROP TYPE NUMBER_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 boolean.
+ *
+ */
+ @Test
+ 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());
+ 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};
+ 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};
+ 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());
+ }
+ }
+
+ /**
+ *
+ * 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 {
+ ArrayType arrayType1D = OracleR2dbcTypes.arrayType("BINARY_ARRAY");
+ awaitOne(connection.createStatement(
+ "CREATE OR REPLACE TYPE BINARY_ARRAY" +
+ " AS ARRAY(10) OF RAW(100)")
+ .execute());
+ 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 = {
+ 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
+ };
+ 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());
+ }
+ }
+
+ /**
+ *
+ * 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));
+
+ 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 {
+
+ // Expect ARRAY of DATE and LocalDateTime[] to map
+ 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
+ };
+ 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);
+ 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);
+ 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 {
+
+ // Expect ARRAY of TIMESTAMP and LocalDateTime[] to map
+ LocalDateTime[] localDateTimes = {
+ dateTimeValue.minus(Duration.ofMillis(100))
+ .toLocalDateTime(),
+ dateTimeValue.toLocalDateTime(),
+ dateTimeValue.plus(Duration.ofMillis(100))
+ .toLocalDateTime(),
+ null
+ };
+ verifyArrayTypeMapping(
+ connection,
+ 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);
+ 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);
+ 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"));
+ }
+
+ 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 {
+ // Expect ARRAY of TIMESTAMP WITH TIME ZONE and OffsetDateTime[] to map
+ 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
+ };
+ verifyArrayTypeMapping(
+ connection,
+ 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"));
+ }
+
+ 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 {
+ // Expect ARRAY of TIMESTAMP WITH LOCAL TIME ZONE and LocalDateTime[] to
+ // map
+ LocalDateTime[] localDateTimes = {
+ dateTimeValue.minus(Duration.ofMillis(100))
+ .toLocalDateTime(),
+ dateTimeValue.toLocalDateTime(),
+ dateTimeValue.plus(Duration.ofMillis(100))
+ .toLocalDateTime(),
+ null
+ };
+ verifyArrayTypeMapping(
+ connection,
+ 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"));
+ }
+
+ 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 {
+
+ // Expect ARRAY of INTERVAL YEAR TO MONTH and Period[] to map
+ Period[] periods = {
+ Period.of(1, 2, 0),
+ Period.of(3, 4, 0),
+ Period.of(5, 6, 0),
+ null
+ };
+
+ verifyArrayTypeMapping(
+ connection,
+ 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 {
+ Duration[] durations = {
+ Duration.ofDays(1),
+ Duration.ofHours(2),
+ Duration.ofMinutes(3),
+ Duration.ofSeconds(4),
+ null
+ };
+ verifyArrayTypeMapping(
+ connection,
+ 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"));
+ }
+ }
+ finally {
+ tryAwaitNone(connection.close());
+ }
+ }
+
+ /**
+ * 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,
+ ArrayType arrayType1D,
+ ArrayType arrayType2D,
+ ArrayType arrayType3D,
+ Class javaArrayClass, IntFunction arrayGenerator,
+ boolean isDefaultMapping,
+ BiConsumer equalsAssertion) {
+
+ // Verify mapping of 1-dimensional array with values
+ T javaArray1D = arrayGenerator.apply(0);
+ verifyTypeMapping(
+ connection,
+ Parameters.in(arrayType1D, javaArray1D),
+ arrayType1D.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(arrayType1D, emptyJavaArray),
+ arrayType1D.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 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 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)));
+ }
+
// 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 +1601,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(