Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VECTOR Support #146

Merged
merged 5 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 99 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ types of Oracle Database.
| [INTERVAL DAY TO SECOND](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-B03DD036-66F8-4BD3-AF26-6D4433EBEC1C) | `java.time.Duration` |
| [INTERVAL YEAR TO MONTH](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-ED59E1B3-BA8D-4711-B5C8-B0199C676A95) | `java.time.Period` |
| [SYS_REFCURSOR](https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/static-sql.html#GUID-470A7A99-888A-46C2-BDAF-D4710E650F27) | `io.r2dbc.spi.Result` |
| [VECTOR](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-801FFE49-217D-4012-9C55-66DAE1BA806F) | `double[]`, `float[]`, `byte[]`, or `oracle.sql.VECTOR` |
> Unlike the standard SQL type named "DATE", the Oracle Database type named
> "DATE" stores values for year, month, day, hour, minute, and second. The
> standard SQL type only stores year, month, and day. LocalDateTime objects are able
Expand Down Expand Up @@ -981,8 +982,8 @@ void printObjectMetadata(OracleR2dbcObject oracleObject) {
```

### REF Cursor
Use the `oracle.r2dbc.OracleR2dbcTypes.REF_CURSOR` type to bind `SYS_REFCURSOR` out
parameters:
Use the `oracle.r2dbc.OracleR2dbcTypes.REF_CURSOR` type to bind `SYS_REFCURSOR`
out parameters:
```java
Publisher<Result> executeProcedure(Connection connection) {
connection.createStatement(
Expand All @@ -1009,6 +1010,102 @@ Publisher<ExampleObject> mapRefCursorRows(Result refCursorResult) {
}
```

### VECTOR
The default mapping for `VECTOR` is the
[oracle.sql.VECTOR](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/sql/VECTOR.html)
class. Instances of this class may be passed to
`Statement.bind(int/String, Object)`:
```java
void bindVector(Statement statement, float[] floatArray) throws SQLException {
final VECTOR vector;
try {
vector = VECTOR.ofFloat32Values(floatArray);
}
catch (SQLException sqlException) {
throw new IllegalArgumentException(sqlException);
}
statement.bind("vector", vector);
}
```
The `oracle.sql.VECTOR` class defines three factory methods: `ofFloat64Values`,
`ofFloat32Values`, and `ofInt8Values`. These methods support Java to VECTOR
conversions of `boolean[]`, `byte[]`, `short[]`, `int[]`, `long[]`,
`float[]`, and `double[]`:
```java
void bindVector(Statement statement, int[] intArray) {
final VECTOR vector;
try {
vector = VECTOR.ofFloat64Values(intArray);
}
catch (SQLException sqlException) {
throw new IllegalArgumentException(sqlException);
}
statement.bind("vector", vector);
}
```
The factory methods of `oracle.sql.VECTOR` may perform lossy conversions, such
as when converting a `double[]` into a VECTOR of 32-bit floating point numbers.
[The JavaDocs of these methods specify which conversions are lossy](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/sql/VECTOR.html).

The `OracleR2dbcTypes.VECTOR` type descriptor can be used to register an OUT or
IN/OUT parameter:
```java
void registerOutVector(Statement statement) {
Parameter outVector = Parameters.out(OracleR2dbcTypes.VECTOR);
statement.bind("vector", outVector);
}
```
The `OracleR2dbcTypes.VECTOR` type descriptor can also be used as an alternative to
`oracle.sql.VECTOR` when binding an IN parameter to a `double[]`, `float[]`, or
`byte[]`:
```java
void bindVector(Statement statement, float[] floatArray) {
Parameter inVector = Parameters.in(OracleR2dbcTypes.VECTOR, floatArray);
statement.bind("vector", inVector);
}
```
Note that `double[]`, `float[]`, and `byte[]` can NOT be passed directly to
`Statement.bind(int/String, Object)` when binding `VECTOR` data. The R2DBC
Specification defines `ARRAY` as the default mapping for Java arrays.

A `VECTOR` column or OUT parameter is converted to `oracle.sql.VECTOR` by
default. The column or OUT parameter can also be converted to `double[]`,
`float[]`, or `byte[]` by passing the corresponding array class to the `get`
methods:
```java
float[] getVector(io.r2dbc.Readable readable) {
return readable.get("vector", float[].class);
}
```

#### Returning VECTOR from DML
Returning a VECTOR column with `Statement.returningGeneratedValues(String...)`
is not supported due to a defect in the 23.4 release of Oracle JDBC. Attempting
to return a `VECTOR` column will result in a `Subscriber` that never receives
`onComplete` or `onError`. The defect will be fixed in the next release of
Oracle JDBC.

A `RETURNING ... INTO` clause can be used as a temporary workaround. This clause
must appear within a PL/SQL block, denoted by the `BEGIN` and `END;` keywords.
In the following example, a `VECTOR` column named "embedding" is returned:
```java
Publisher<double[]> returningVectorExample(Connection connection, String vectorString) {

Statement statement = connection.createStatement(
"BEGIN INSERT INTO example(embedding)"
+ " VALUES (TO_VECTOR(:vectorString, 999, FLOAT64))"
+ " RETURNING embedding INTO :embedding;"
+ " END;")
.bind("vectorString", vectorString)
.bind("embedding", Parameters.out(OracleR2dbcTypes.VECTOR));

return Flux.from(statement.execute())
.flatMap(result ->
result.map(outParameters ->
outParameters.get("embedding", double[].class)));
}
```

## Secure Programming Guidelines
The following security related guidelines should be adhered to when programming
with the Oracle R2DBC Driver.
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/oracle/r2dbc/OracleR2dbcTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ private OracleR2dbcTypes() {}
public static final Type REF_CURSOR =
new TypeImpl(Result.class, "SYS_REFCURSOR");

/**
* A vector of 64-bit floating point numbers, 32-bit floating point numbers,
* or 8-bit signed integers. Maps to <code>double[]</code> by default, as a
* <code>double</code> can store all the possible number formats without
* losing information.
*/
public static final Type VECTOR =
new TypeImpl(oracle.sql.VECTOR.class, "VECTOR");

/**
* <p>
* Creates an {@link ArrayType} representing a user defined {@code ARRAY}
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/oracle/r2dbc/impl/SqlTypeMap.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import oracle.jdbc.OracleType;
import oracle.r2dbc.OracleR2dbcObject;
import oracle.r2dbc.OracleR2dbcTypes;
import oracle.sql.VECTOR;
import oracle.sql.json.OracleJsonObject;

import java.math.BigDecimal;
Expand Down Expand Up @@ -85,6 +86,7 @@ final class SqlTypeMap {
entry(JDBCType.NUMERIC, R2dbcType.NUMERIC),
entry(JDBCType.NVARCHAR, R2dbcType.NVARCHAR),
entry(JDBCType.REAL, R2dbcType.REAL),
entry(JDBCType.REF_CURSOR, OracleR2dbcTypes.REF_CURSOR),
entry(JDBCType.ROWID, OracleR2dbcTypes.ROWID),
entry(JDBCType.SMALLINT, R2dbcType.SMALLINT),
entry(JDBCType.TIME, R2dbcType.TIME),
Expand All @@ -101,7 +103,7 @@ final class SqlTypeMap {
entry(JDBCType.TINYINT, R2dbcType.TINYINT),
entry(JDBCType.VARBINARY, R2dbcType.VARBINARY),
entry(JDBCType.VARCHAR, R2dbcType.VARCHAR),
entry(JDBCType.REF_CURSOR, OracleR2dbcTypes.REF_CURSOR)
entry(OracleType.VECTOR, OracleR2dbcTypes.VECTOR)
);

/**
Expand Down Expand Up @@ -177,10 +179,13 @@ final class SqlTypeMap {
entry(float[].class, JDBCType.ARRAY),
entry(double[].class, JDBCType.ARRAY),

// Support binding OracleR2dbcReadable, Object[], and Map<String, Object>
// to OBJECT (ie: STRUCT)
// Support binding Map<String, Object> and OracleR2dbcObject to OBJECT
// (ie: STRUCT)
entry(Map.class, JDBCType.STRUCT),
entry(OracleR2dbcObject.class, JDBCType.STRUCT)
entry(OracleR2dbcObject.class, JDBCType.STRUCT),

// Support binding oracle.sql.VECTOR to VECTOR
entry(VECTOR.class, OracleType.VECTOR)
);

/**
Expand Down Expand Up @@ -269,6 +274,7 @@ else if (r2dbcType instanceof OracleR2dbcTypes.ObjectType)
* <li>{@link Period} : INTERVAL YEAR TO MONTH</li>
* <li>{@link RowId} : ROWID</li>
* <li>{@link OracleJsonObject} : JSON</li>
* <li>{@link oracle.sql.VECTOR} : VECTOR</li>
* </ul>
* @param javaType Java type to map
* @return SQL type mapping for the {@code javaType}
Expand Down
48 changes: 37 additions & 11 deletions src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,15 @@
import io.r2dbc.spi.Parameters;
import io.r2dbc.spi.R2dbcType;
import io.r2dbc.spi.ReadableMetadata;
import io.r2dbc.spi.Statement;
import io.r2dbc.spi.Type;
import oracle.jdbc.OracleType;
import oracle.r2dbc.OracleR2dbcObject;
import oracle.r2dbc.OracleR2dbcObjectMetadata;
import oracle.r2dbc.OracleR2dbcTypes;
import oracle.sql.VECTOR;
import oracle.sql.json.OracleJsonFactory;
import oracle.sql.json.OracleJsonObject;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
Expand All @@ -43,6 +44,7 @@
import java.nio.ByteBuffer;
import java.sql.JDBCType;
import java.sql.RowId;
import java.sql.SQLException;
import java.sql.SQLType;
import java.time.Duration;
import java.time.LocalDate;
Expand All @@ -53,6 +55,7 @@
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;

import static java.lang.String.format;
Expand Down Expand Up @@ -258,10 +261,6 @@ public void testDateTimeTypes() {
OracleR2dbcTypes.INTERVAL_DAY_TO_SECOND, 9, 9, Duration.class,
Duration.parse("+P123456789DT23H59M59.123456789S"));

// Expect ROWID and String to map.
// Expect UROWID and String to map.
// Expect JSON and OracleJsonObject to map.

}
finally {
tryAwaitNone(connection.close());
Expand Down Expand Up @@ -305,17 +304,14 @@ public void testRowIdTypes() {

/**
* Verifies the implementation of {@link OracleReadableMetadataImpl} for
* JSON type columns. When the test database older than version 21c, this test
* is expected to fail with an ORA-00902 error indicating that JSON is not
* a valid data type. The JSON type was added in 21c.
* JSON type columns. When the test database is older than version 21c, this
* test is ignored; The JSON type was added in 21c.
*/
@Test
public void testJsonType() {

// The JSON data type was introduced in Oracle Database version 21c, so this
// test is a no-op if the version is older than 21c.
if (databaseVersion() < 21)
return;
Assumptions.assumeTrue(databaseVersion() >= 21);

Connection connection =
Mono.from(sharedConnection()).block(connectTimeout());
Expand Down Expand Up @@ -529,6 +525,36 @@ public void testObjectTypes() {
}
}

/**
* Verifies the implementation of {@link OracleReadableMetadataImpl} for
* VECTOR type columns. When the test database is older than version 23ai,
* this test is ignored; The VECTOR type was added in 23ai.
*/
@Test
public void testVectorType() throws SQLException {
Assumptions.assumeTrue(databaseVersion() >= 23);

Connection connection =
Mono.from(sharedConnection()).block(connectTimeout());
try {
double[] doubleArray =
DoubleStream.iterate(0d, previous -> previous + 0.1d)
.limit(30)
.toArray();
VECTOR vector = VECTOR.ofFloat64Values(doubleArray);

// Expect VECTOR and double[] to map.
verifyColumnMetadata(
connection, "VECTOR", OracleType.VECTOR, OracleR2dbcTypes.VECTOR,
null, null,
VECTOR.class, vector);
}
finally {
tryAwaitNone(connection.close());
}

}

/**
* Calls
* {@link #verifyColumnMetadata(Connection, String, SQLType, Type, Integer, Integer, Nullability, Class, Object)}
Expand Down
Loading
Loading