diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh
index 4ba9e9b..fed4029 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=240" >> src/test/resources/config.properties
+echo "SQL_TIMEOUT=240" >> src/test/resources/config.properties
mvn clean compile test
diff --git a/README.md b/README.md
index 3352062..4cc64e9 100644
--- a/README.md
+++ b/README.md
@@ -570,7 +570,169 @@ 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.
-### REF Cursors
+### 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 a 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();
+}
+```
+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));
+}
+```
+
+### OBJECT
+Oracle Database supports `OBJECT` as a user defined type. A `CREATE TYPE`
+command is used to define an `OBJECT` type:
+```sql
+CREATE TYPE PET AS OBJECT(
+ name VARCHAR(128),
+ species VARCHAR(128),
+ weight NUMBER,
+ birthday DATE)
+```
+Oracle R2DBC defines `oracle.r2dbc.OracleR2dbcType.ObjectType` as a `Type` for
+representing user defined `OBJECT` types. A `Parameter` with a type of
+`ObjectType` may be used to bind `OBJECT` values to a `Statement`.
+
+Use an `Object[]` to bind the attribute values of an `OBJECT` by index:
+```java
+Publisher objectArrayBindExample(Connection connection) {
+ Statement statement =
+ connection.createStatement("INSERT INTO petTable VALUES (:petObject)");
+
+ // Bind the attributes of the PET OBJECT defined above
+ ObjectType objectType = OracleR2dbcTypes.objectType("PET");
+ Object[] attributeValues = {
+ "Derby",
+ "Dog",
+ 22.8,
+ LocalDate.of(2015, 11, 07)
+ };
+ statement.bind("petObject", Parameters.in(objectType, attributeValues));
+
+ return statement.execute();
+}
+```
+
+Use a `Map` to bind the attribute values of an `OBJECT` by name:
+```java
+Publisher objectMapBindExample(Connection connection) {
+ Statement statement =
+ connection.createStatement("INSERT INTO petTable VALUES (:petObject)");
+
+ // Bind the attributes of the PET OBJECT defined above
+ ObjectType objectType = OracleR2dbcTypes.objectType("PET");
+ Map attributeValues = Map.of(
+ "name", "Derby",
+ "species", "Dog",
+ "weight", 22.8,
+ "birthday", LocalDate.of(2015, 11, 07));
+ statement.bind("petObject", Parameters.in(objectType, attributeValues));
+
+ return statement.execute();
+}
+```
+A `Parameter` with a type of `ObjectType` must be used when binding OUT
+parameters of `OBJECT` types for a PL/SQL call:
+```java
+Publisher objectOutBindExample(Connection connection) {
+ Statement statement =
+ connection.createStatement("BEGIN; getPet(:petObject); END;");
+
+ ObjectType objectType = OracleR2dbcTypes.objectType("PET");
+ statement.bind("petObject", Parameters.out(objectType));
+
+ return statement.execute();
+}
+```
+`OBJECT` values may be consumed from a `Row` or `OutParameter` as an
+`oracle.r2dbc.OracleR2dbcObject`. The `OracleR2dbcObject` interface is a subtype
+of `io.r2dbc.spi.Readable`. Attribute values may be accessed using the standard
+`get` methods of `Readable`. The `get` methods of `OracleR2dbcObject` support
+alll SQL to Java type mappings defined by the
+[R2DBC Specification](https://r2dbc.io/spec/1.0.0.RELEASE/spec/html/#datatypes.mapping):
+```java
+Publisher objectMapExample(Result result) {
+ return result.map(row -> {
+ OracleR2dbcObject oracleObject = row.get(0, OracleR2dbcObject.class);
+ return new Pet(
+ oracleObject.get("name", String.class),
+ oracleObject.get("species", String.class),
+ oracleObject.get("weight", Float.class),
+ oracleObject.get("birthday", LocalDate.class));
+ });
+}
+```
+
+Instances of `OracleR2dbcObject` may be passed directly to `Statement` bind
+methods:
+```java
+Publisher objectBindExample(
+ OracleR2dbcObject oracleObject, Connection connection) {
+ Statement statement =
+ connection.createStatement("INSERT INTO petTable VALUES (:petObject)");
+
+ statement.bind("petObject", oracleObject);
+
+ return statement.execute();
+}
+```
+Attribute metadata is exposed by the `getMetadata` method of
+`OracleR2dbcObject`:
+```java
+void printObjectMetadata(OracleR2dbcObject oracleObject) {
+ OracleR2dbcObjectMetadata metadata = oracleObject.getMetadata();
+ OracleR2dbcTypes.ObjectType objectType = metadata.getObjectType();
+
+ System.out.println("Object Type: " + objectType);
+ metadata.getAttributeMetadatas()
+ .stream()
+ .forEach(attributeMetadata -> {
+ System.out.println("\tAttribute Name: " + attributeMetadata.getName()));
+ System.out.println("\tAttribute Type: " + attributeMetadata.getType()));
+ });
+}
+```
+
+### REF Cursor
Use the `oracle.r2dbc.OracleR2dbcTypes.REF_CURSOR` type to bind `SYS_REFCURSOR` out
parameters:
```java
diff --git a/src/main/java/oracle/r2dbc/OracleR2dbcObject.java b/src/main/java/oracle/r2dbc/OracleR2dbcObject.java
new file mode 100644
index 0000000..388c56e
--- /dev/null
+++ b/src/main/java/oracle/r2dbc/OracleR2dbcObject.java
@@ -0,0 +1,7 @@
+package oracle.r2dbc;
+
+public interface OracleR2dbcObject extends io.r2dbc.spi.Readable {
+
+ OracleR2dbcObjectMetadata getMetadata();
+
+}
diff --git a/src/main/java/oracle/r2dbc/OracleR2dbcObjectMetadata.java b/src/main/java/oracle/r2dbc/OracleR2dbcObjectMetadata.java
new file mode 100644
index 0000000..ee8535c
--- /dev/null
+++ b/src/main/java/oracle/r2dbc/OracleR2dbcObjectMetadata.java
@@ -0,0 +1,49 @@
+package oracle.r2dbc;
+
+import io.r2dbc.spi.ReadableMetadata;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * Represents the metadata for attributes of an OBJECT. Metadata for attributes
+ * can either be retrieved by index or by name. Attribute indexes are
+ * {@code 0}-based. Retrieval by attribute name is case-insensitive.
+ */
+public interface OracleR2dbcObjectMetadata {
+
+ /**
+ * Returns the type of the OBJECT which metadata is provided for.
+ * @return The type of the OBJECT. Not null.
+ */
+ OracleR2dbcTypes.ObjectType getObjectType();
+
+ /**
+ * Returns the {@link ReadableMetadata} for one attribute.
+ *
+ * @param index the attribute index starting at 0
+ * @return the {@link ReadableMetadata} for one attribute. Not null.
+ * @throws IndexOutOfBoundsException if {@code index} is out of range
+ * (negative or equals/exceeds {@code getParameterMetadatas().size()})
+ */
+ ReadableMetadata getAttributeMetadata(int index);
+
+ /**
+ * Returns the {@link ReadableMetadata} for one attribute.
+ *
+ * @param name the name of the attribute. Not null. Parameter names are
+ * case-insensitive.
+ * @return the {@link ReadableMetadata} for one attribute. Not null.
+ * @throws IllegalArgumentException if {@code name} is {@code null}
+ * @throws NoSuchElementException if there is no attribute with the
+ * {@code name}
+ */
+ ReadableMetadata getAttributeMetadata(String name);
+
+ /**
+ * Returns the {@link ReadableMetadata} for all attributes.
+ *
+ * @return the {@link ReadableMetadata} for all attributes. Not null.
+ */
+ List extends ReadableMetadata> getAttributeMetadatas();
+}
diff --git a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java
index 08c6c56..2639f42 100644
--- a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java
+++ b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java
@@ -20,7 +20,10 @@
*/
package oracle.r2dbc;
+import io.r2dbc.spi.Parameter;
+import io.r2dbc.spi.R2dbcType;
import io.r2dbc.spi.Result;
+import io.r2dbc.spi.Statement;
import io.r2dbc.spi.Type;
import oracle.sql.json.OracleJsonObject;
@@ -29,6 +32,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
@@ -99,10 +103,144 @@ private OracleR2dbcTypes() {}
public static final Type REF_CURSOR =
new TypeImpl(Result.class, "SYS_REFCURSOR");
+ /**
+ *
+ * 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 create
+ * 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"));
+ }
+
+ public static ObjectType objectType(String name) {
+ return new ObjectTypeImpl(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();
+ }
+
+ public interface ObjectType 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}. {@code Object[]} is
+ * the default mapping of the constructed type. 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);
+ }
+ }
+
+ /** Concrete implementation of the {@code ObjectType} interface */
+ private static final class ObjectTypeImpl
+ extends TypeImpl implements ObjectType {
+
+ /**
+ * Constructs an OBJECT type with the given {@code name}.
+ * {@code OracleR2dbcObject} is the default mapping of the constructed type.
+ * @param name User defined name of the type. Not null.
+ */
+ ObjectTypeImpl(String name) {
+ super(OracleR2dbcObject.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.
@@ -163,6 +301,19 @@ public String getName() {
public String toString() {
return getName();
}
+
+ @Override
+ public boolean equals(Object other) {
+ if (! (other instanceof Type))
+ return false;
+
+ return sqlName.equals(((Type)other).getName());
+ }
+
+ @Override
+ public int hashCode() {
+ return sqlName.hashCode();
+ }
}
}
diff --git a/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java b/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java
index f638ac9..dd4b7bb 100755
--- a/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java
+++ b/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java
@@ -81,6 +81,7 @@ static T requireNonNull(T obj, String message) {
return obj;
}
+
/**
* Checks if a {@code jdbcConnection} is open, and throws an exception if the
* check fails.
diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java
index 4d4b56c..02490cd 100755
--- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java
+++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java
@@ -26,24 +26,58 @@
import io.r2dbc.spi.OutParameters;
import io.r2dbc.spi.OutParametersMetadata;
import io.r2dbc.spi.R2dbcException;
-
import io.r2dbc.spi.R2dbcType;
+import io.r2dbc.spi.ReadableMetadata;
import io.r2dbc.spi.Result;
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.jdbc.OracleStruct;
+import oracle.r2dbc.OracleR2dbcObject;
+import oracle.r2dbc.OracleR2dbcObjectMetadata;
import oracle.r2dbc.OracleR2dbcTypes;
import oracle.r2dbc.impl.ReactiveJdbcAdapter.JdbcReadable;
+import oracle.r2dbc.impl.ReadablesMetadata.OracleR2dbcObjectMetadataImpl;
import oracle.r2dbc.impl.ReadablesMetadata.OutParametersMetadataImpl;
import oracle.r2dbc.impl.ReadablesMetadata.RowMetadataImpl;
+import oracle.sql.INTERVALDS;
+import oracle.sql.INTERVALYM;
+import oracle.sql.TIMESTAMPLTZ;
+import oracle.sql.TIMESTAMPTZ;
+import java.math.BigDecimal;
import java.nio.ByteBuffer;
+import java.sql.Array;
import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Struct;
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.OffsetTime;
+import java.time.Period;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.TreeMap;
+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;
/**
*
@@ -58,8 +92,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;
@@ -67,6 +102,9 @@ 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;
+
/**
* A collection of results that depend on the JDBC statement which created
* this readable to remain open until all results are consumed.
@@ -79,14 +117,17 @@ class OracleReadableImpl implements io.r2dbc.spi.Readable {
* {@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(
- DependentCounter dependentCounter, JdbcReadable jdbcReadable,
- ReadablesMetadata> readablesMetadata, ReactiveJdbcAdapter adapter) {
+ java.sql.Connection jdbcConnection, DependentCounter dependentCounter,
+ JdbcReadable jdbcReadable, ReadablesMetadata> readablesMetadata,
+ ReactiveJdbcAdapter adapter) {
+ this.jdbcConnection = jdbcConnection;
this.dependentCounter = dependentCounter;
this.jdbcReadable = jdbcReadable;
this.readablesMetadata = readablesMetadata;
@@ -99,6 +140,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.
@@ -106,16 +149,21 @@ private OracleReadableImpl(
* {@code metadata}. Not null.
*/
static Row createRow(
- DependentCounter dependentCounter, JdbcReadable jdbcReadable,
- RowMetadataImpl metadata, ReactiveJdbcAdapter adapter) {
- return new RowImpl(dependentCounter, jdbcReadable, metadata, adapter);
+ java.sql.Connection jdbcConnection, DependentCounter dependentCounter,
+ JdbcReadable jdbcReadable, RowMetadataImpl metadata,
+ ReactiveJdbcAdapter adapter) {
+ return new RowImpl(
+ jdbcConnection, dependentCounter, jdbcReadable, metadata, adapter);
}
+
/**
*
* Creates a new {@code OutParameters} that supplies values and metadata from
* 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.
@@ -123,10 +171,11 @@ static Row createRow(
* {@code metadata}. Not null.
*/
static OutParameters createOutParameters(
- DependentCounter dependentCounter, JdbcReadable jdbcReadable,
- OutParametersMetadataImpl metadata, ReactiveJdbcAdapter adapter) {
+ java.sql.Connection jdbcConnection, DependentCounter dependentCounter,
+ JdbcReadable jdbcReadable, OutParametersMetadataImpl metadata,
+ ReactiveJdbcAdapter adapter) {
return new OutParametersImpl(
- dependentCounter, jdbcReadable, metadata, adapter);
+ jdbcConnection, dependentCounter, jdbcReadable, metadata, adapter);
}
/**
@@ -232,7 +281,18 @@ else if (Object.class.equals(type)) {
: convert(index, defaultType);
}
else {
- value = jdbcReadable.getObject(index, type);
+ Type sqlType = readablesMetadata.get(index).getType();
+
+ if (sqlType instanceof OracleR2dbcTypes.ArrayType) {
+ value = getOracleArray(index, type);
+ }
+ else if (sqlType instanceof OracleR2dbcTypes.ObjectType) {
+ value = getOracleObject(index, type);
+ }
+ else {
+ // Fallback to a built-in conversion supported by Oracle JDBC
+ value = jdbcReadable.getObject(index, type);
+ }
}
return type.cast(value);
@@ -330,6 +390,8 @@ private Clob getClob(int index) {
* value is NULL.
*/
private LocalDateTime getLocalDateTime(int index) {
+ return jdbcReadable.getObject(index, LocalDateTime.class);
+ /*
if (OracleR2dbcTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE
.equals(readablesMetadata.get(index).getType())) {
// TODO: Remove this when Oracle JDBC implements a correct conversion
@@ -339,6 +401,528 @@ private LocalDateTime getLocalDateTime(int index) {
else {
return jdbcReadable.getObject(index, LocalDateTime.class);
}
+ */
+ }
+
+ /**
+ * Returns an ARRAY at the given {@code index} as a specified {@code type}.
+ * This method supports conversions to Java arrays of the default Java type
+ * mapping for the ARRAY's element type. This conversion is the standard type
+ * mapping for a COLLECTION, as defined by the R2DBC Specification.
+ */
+ private T getOracleArray(int index, Class type) {
+ if (type.isArray()) {
+ return type.cast(getJavaArray(index, type.getComponentType()));
+ }
+ else {
+ // Fallback to a built-in conversion supported by Oracle JDBC
+ return jdbcReadable.getObject(index, type);
+ }
+ }
+
+ /**
+ *
+ * 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) {
+ OracleArray oracleArray = jdbcReadable.getObject(index, OracleArray.class);
+ return oracleArray == null
+ ? null
+ : 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 {
+ // 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 = getArrayTypeMapping(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);
+ }
+ }
+
+ /**
+ * 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> getArrayTypeMapping(
+ OracleArray oracleArray, Class> javaType) {
+
+ if (!Object.class.equals(javaType))
+ return javaType;
+
+ int jdbcType = fromJdbc(oracleArray::getBaseType);
+
+ // Check if the array is multi-dimensional
+ if (jdbcType == Types.ARRAY) {
+
+ Object[] oracleArrays = (Object[]) fromJdbc(oracleArray::getArray);
+
+ // 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 {
+ // 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(
+ 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);
+
+ // 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;
+ }
+
+ /**
+ * 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 {
+ if (boolean.class.equals(primitiveType)) {
+ // OracleArray does not support conversion to boolean[], so map shorts
+ // to booleans.
+ 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(primitiveType)) {
+ // OracleArray does not support conversion to byte[], so map shorts to
+ // bytes
+ short[] shorts = oracleArray.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(primitiveType)) {
+ return oracleArray.getShortArray();
+ }
+ else if (int.class.equals(primitiveType)) {
+ return oracleArray.getIntArray();
+ }
+ else if (long.class.equals(primitiveType)) {
+ return oracleArray.getLongArray();
+ }
+ else if (float.class.equals(primitiveType)) {
+ return oracleArray.getFloatArray();
+ }
+ else if (double.class.equals(primitiveType)) {
+ return oracleArray.getDoubleArray();
+ }
+ else {
+ // 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 unsupportedConversion(javaArrayType, primitiveType);
+ }
+ }
+ catch (SQLException sqlException) {
+ throw toR2dbcException(sqlException);
+ }
+ }
+
+ /**
+ * 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) {
+
+ if (desiredType.isAssignableFrom(array.getClass().getComponentType()))
+ return (T[])array;
+
+ if (array.length == 0)
+ return (T[]) java.lang.reflect.Array.newInstance(desiredType, 0);
+
+ // 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);
+ }
+
+ return mapArray(
+ array,
+ length -> (T[]) java.lang.reflect.Array.newInstance(desiredType, length),
+ (Function