Skip to content

Commit

Permalink
Merge pull request #146 from oracle/144-vector
Browse files Browse the repository at this point in the history
VECTOR Support
  • Loading branch information
jeandelavarene authored May 24, 2024
2 parents fc037e4 + 8aa5121 commit 17adf49
Show file tree
Hide file tree
Showing 7 changed files with 417 additions and 18 deletions.
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

0 comments on commit 17adf49

Please sign in to comment.