From 5e49eb53436050e091ba2dd84351f327f2bcd353 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Thu, 9 May 2024 17:13:22 -0700 Subject: [PATCH 1/4] Added VECTOR --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 349da94..ce062b7 100644 --- a/README.md +++ b/README.md @@ -668,11 +668,12 @@ types of Oracle Database. | Oracle SQL Type | Java Type | |---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| -| [JSON](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Data-Types.html#GUID-E441F541-BA31-4E8C-B7B4-D2FB8C42D0DF) | `javax.json.JsonObject` or `oracle.sql.json.OracleJsonObject` | -| [DATE](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Data-Types.html#GUID-5405B652-C30E-4F4F-9D33-9A4CB2110F1B) | `java.time.LocalDateTime` | -| [INTERVAL DAY TO SECOND](https://docs.oracle.com/en/database/oracle/oracle-database/21/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/21/sqlrf/Data-Types.html#GUID-ED59E1B3-BA8D-4711-B5C8-B0199C676A95) | `java.time.Period` | -| [SYS_REFCURSOR](https://docs.oracle.com/en/database/oracle/oracle-database/21/lnpls/static-sql.html#GUID-470A7A99-888A-46C2-BDAF-D4710E650F27) | `io.r2dbc.spi.Result` | +| [JSON](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-E441F541-BA31-4E8C-B7B4-D2FB8C42D0DF) | `javax.json.JsonObject` or `oracle.sql.json.OracleJsonObject` | +| [DATE](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-5405B652-C30E-4F4F-9D33-9A4CB2110F1B) | `java.time.LocalDateTime` | +| [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[]` or `byte[]` | > 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 From e55e3a918a6db46c3b39db34ad8bd4e7d939e8c9 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Tue, 14 May 2024 16:24:22 -0700 Subject: [PATCH 2/4] Added VECTOR support --- README.md | 92 +++++++++- pom.xml | 2 +- .../java/oracle/r2dbc/OracleR2dbcTypes.java | 9 + .../java/oracle/r2dbc/impl/SqlTypeMap.java | 14 +- .../impl/OracleReadableMetadataImplTest.java | 48 +++-- .../r2dbc/impl/OracleStatementImplTest.java | 173 +++++++++++++++++- .../oracle/r2dbc/impl/TypeMappingTest.java | 74 ++++++++ .../oracle/r2dbc/test/DatabaseConfig.java | 28 +++ 8 files changed, 419 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ce062b7..fa716b3 100644 --- a/README.md +++ b/README.md @@ -673,7 +673,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[]` or `byte[]` | +| [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 @@ -876,8 +876,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 executeProcedure(Connection connection) { connection.createStatement( @@ -904,6 +904,92 @@ Publisher 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. + +#### 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 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. diff --git a/pom.xml b/pom.xml index 80f630e..a727e54 100755 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 11 - 21.11.0.0 + 23.4.0.24.05 1.0.0.RELEASE 3.5.11 1.0.3 diff --git a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java index 85ce884..6f8c52f 100644 --- a/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java +++ b/src/main/java/oracle/r2dbc/OracleR2dbcTypes.java @@ -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 double[] by default, as a + * double can store all the possible number formats without + * losing information. + */ + public static final Type VECTOR = + new TypeImpl(oracle.sql.VECTOR.class, "VECTOR"); + /** *

* Creates an {@link ArrayType} representing a user defined {@code ARRAY} diff --git a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java index 75afdd1..ef424c0 100644 --- a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java +++ b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java @@ -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; @@ -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), @@ -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) ); /** @@ -177,10 +179,13 @@ final class SqlTypeMap { entry(float[].class, JDBCType.ARRAY), entry(double[].class, JDBCType.ARRAY), - // Support binding OracleR2dbcReadable, Object[], and Map - // to OBJECT (ie: STRUCT) + // Support binding Map 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) ); /** @@ -269,6 +274,7 @@ else if (r2dbcType instanceof OracleR2dbcTypes.ObjectType) *

  • {@link Period} : INTERVAL YEAR TO MONTH
  • *
  • {@link RowId} : ROWID
  • *
  • {@link OracleJsonObject} : JSON
  • + *
  • {@link oracle.sql.VECTOR} : VECTOR
  • * * @param javaType Java type to map * @return SQL type mapping for the {@code javaType} diff --git a/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java index 043e55e..c242994 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java @@ -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; @@ -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; @@ -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; @@ -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()); @@ -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()); @@ -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)} diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index 59045cf..9f01478 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -29,6 +29,7 @@ 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 oracle.r2dbc.OracleR2dbcObject; @@ -36,9 +37,10 @@ import oracle.r2dbc.OracleR2dbcTypes; import oracle.r2dbc.OracleR2dbcWarning; import oracle.r2dbc.test.DatabaseConfig; -import oracle.r2dbc.test.TestUtils; +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 org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -46,6 +48,7 @@ import reactor.core.publisher.Signal; import java.sql.RowId; +import java.sql.SQLException; import java.sql.SQLWarning; import java.util.ArrayList; import java.util.Arrays; @@ -64,6 +67,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.DoubleStream; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -71,6 +75,8 @@ import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions; import static oracle.r2dbc.test.DatabaseConfig.databaseVersion; +import static oracle.r2dbc.test.DatabaseConfig.jdbcMinorVersion; +import static oracle.r2dbc.test.DatabaseConfig.jdbcVersion; import static oracle.r2dbc.test.DatabaseConfig.newConnection; import static oracle.r2dbc.test.DatabaseConfig.sharedConnection; import static oracle.r2dbc.test.TestUtils.constructObject; @@ -86,6 +92,7 @@ import static oracle.r2dbc.util.Awaits.consumeOne; 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.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -3162,12 +3169,174 @@ public void testJsonDualityView() { "DROP TABLE testJsonDualityViewTable")); tryAwaitNone(connection.close()); } + } + + /** + * Verifies the case where the VECTOR type descriptor is used to register an + * OUT parameter. + */ + @Test + public void testVectorOutParameter() throws SQLException { + Assumptions.assumeTrue(databaseVersion() >= 23); // VECTOR is added in 23 + + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testVectorOutParameter (id NUMBER, value VECTOR)")); + + class IdVector { + final int id; + final VECTOR vector; + + IdVector(int id, VECTOR vector) { + this.id = id; + this.vector = vector; + } + + @Override + public boolean equals(Object other) { + return other instanceof IdVector + && ((IdVector)other).id == id + && Objects.equals(((IdVector)other).vector, vector); + } + } + + // Round 1: Use PL/SQL to return column values + IdVector expected1 = new IdVector( + 0, + VECTOR.ofFloat64Values( + DoubleStream.iterate(-3.0, previous -> previous + 0.1) + .limit(60) + .toArray())); + + awaitOne( + expected1, + Flux.from(connection.createStatement( + "BEGIN" + + " INSERT INTO testVectorOutParameter VALUES(:inId, :inVector)" + + " RETURNING id, value INTO :outId, :outVector;" + + " END;") + .bind("inId", expected1.id) + .bind("inVector", expected1.vector) + .bind("outId", Parameters.out(R2dbcType.NUMERIC)) + .bind("outVector", Parameters.out(OracleR2dbcTypes.VECTOR)) + .execute()) + .flatMap(result -> + result.map(outParameters -> + new IdVector( + outParameters.get("outId", Integer.class), + outParameters.get("outVector", VECTOR.class))))); + + // Round 2: Use returnGeneratedValues(String...) to return column values + + // Oracle JDBC 23.4 has a defect which prevents the Subscriber from + // receiving a terminal signal. The defect has been reported as bug + // #36607804, and is expected to be fixed in the 23.5 release. + Assumptions.assumeTrue( + jdbcMinorVersion() >= 5, + "Oracle JDBC 23.4 does not support generated keys for VECTOR"); + + IdVector expected2 = new IdVector( + 0, + VECTOR.ofFloat64Values( + DoubleStream.iterate(-3.0, previous -> previous + 0.1) + .limit(60) + .toArray())); + + awaitMany( + List.of(/*update count ->*/1L, expected2), + Flux.from(connection.createStatement( + "INSERT INTO testVectorOutParameter VALUES(:id, :vector)") + .bind("id", expected2.id) + .bind("vector", expected2.vector) + .returnGeneratedValues("id", "value") + .execute()) + .flatMap(result -> + result.flatMap(segment -> { + if (segment instanceof UpdateCount) { + return Mono.just(((UpdateCount) segment).value()); + } + else if (segment instanceof Result.RowSegment) { + OutParameters outParameters = + ((Result.OutSegment)segment).outParameters(); + return Mono.just(new IdVector( + outParameters.get("outId", Integer.class), + outParameters.get("outVector", VECTOR.class))); + } + else if (segment instanceof Message) { + throw ((Message)segment).exception(); + } + else { + throw new AssertionError("Unexpected segment: " + segment); + } + }))); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testVectorOutParameter")); + tryAwaitNone(connection.close()); + } + } + + @Test + public void testVectorReturningExample() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE example (text CLOB, embedding VECTOR)")); + + double[] expected = + DoubleStream.iterate(0, previous -> previous + 1) + .limit(999) + .toArray(); + + double[] actual = + awaitOne(returningVectorExample(connection, Arrays.toString(expected))); + assertArrayEquals(expected, actual); + } + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE example")); + tryAwaitNone(connection.close()); + } + } + + Publisher 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))); + } + + Publisher returningVectorExample0(Connection connection, String text) { + return Flux.from(connection.createStatement( + "BEGIN" + + " INSERT INTO example(text, embedding) VALUES(" + +" :text," + +" TO_VECTOR(VECTOR_EMBEDDING(doc_model USING :text as data)))" + +" RETURNING embedding INTO :embedding;" + + " END;") + .bind("text", text) + .bind("embedding", Parameters.out(OracleR2dbcTypes.VECTOR)) + .execute()) + .flatMap(result-> + result.map(outParameters-> + outParameters.get("outVector", double[].class))); } /** * Connect to the database configured by {@link DatabaseConfig}, with a - * the connection configured to use a given {@code executor} for async + * connection configured to use a given {@code executor} for async * callbacks. * @param executor Executor for async callbacks * @return Connection that uses the {@code executor} diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index 5c28f30..28e0755 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -34,6 +34,7 @@ import oracle.r2dbc.OracleR2dbcTypes.ArrayType; import oracle.r2dbc.OracleR2dbcTypes.ObjectType; import oracle.r2dbc.test.TestUtils; +import oracle.sql.VECTOR; import oracle.sql.json.OracleJsonFactory; import oracle.sql.json.OracleJsonObject; import org.junit.jupiter.api.Assertions; @@ -45,6 +46,7 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.sql.RowId; +import java.sql.SQLException; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -63,6 +65,7 @@ import java.util.function.Function; import java.util.function.IntFunction; import java.util.stream.Collectors; +import java.util.stream.DoubleStream; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -1520,6 +1523,77 @@ public void testObjectTypeMappings() { } + /** + *

    + * Verifies the implementation of Java to SQL and SQL to Java type mappings + * for the VECTOR data type. The R2DBC 1.0.0 Specification does not contain + * mapping guidelines for the VECTOR data type. The Oracle R2DBC Driver is + * expected to map VECTOR to a {@link oracle.sql.VECTOR} value. + *

    + */ + @Test + public void testVectorMapping() throws SQLException { + + // The VECTOR data type was introduced in Oracle Database version 23ai, so + // this test is skipped if the version is older than 23. + assumeTrue(databaseVersion() >= 23, + "JSON columns are not supported by database versions older than 21"); + + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + double[] doubleArray = + DoubleStream.iterate(-2.0d, previous -> previous + 0.1d) + .limit(40) + .toArray(); + + // Expect VECTOR and oracle.sql.VECTOR to map. + VECTOR vector = VECTOR.ofFloat64Values(doubleArray); + verifyTypeMapping(connection, vector, "VECTOR"); + + // Expect VECTOR and double[] to map + verifyTypeMapping( + connection, + Parameters.in(OracleR2dbcTypes.VECTOR, doubleArray), + "VECTOR", + row -> + row.get(0, double[].class), + (ignored, actualValue) -> + assertArrayEquals(doubleArray, actualValue)); + + float[] floatArray = new float[doubleArray.length]; + for (int i = 0; i < floatArray.length; i++) + floatArray[i] = (float) doubleArray[i]; + + // Expect VECTOR and float[] to map + verifyTypeMapping( + connection, + Parameters.in(OracleR2dbcTypes.VECTOR, floatArray), + "VECTOR", + row -> + row.get(0, float[].class), + (ignored, actualValue) -> + assertArrayEquals(floatArray, actualValue)); + + byte[] byteArray = new byte[doubleArray.length]; + for (int i = 0; i < byteArray.length; i++) + byteArray[i] = (byte) doubleArray[i]; + + // Expect VECTOR and byte[] to map + verifyTypeMapping( + connection, + Parameters.in(OracleR2dbcTypes.VECTOR, byteArray), + "VECTOR", + row -> + row.get(0, byte[].class), + (ignored, actualValue) -> + assertArrayEquals(byteArray, actualValue)); + + } + finally { + tryAwaitNone(connection.close()); + } + } /** * For an ARRAY type of a given {@code typeName}, verifies the following diff --git a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java index c8d3d75..aa67679 100644 --- a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java +++ b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java @@ -188,6 +188,34 @@ public static int databaseVersion() { } } + /** + * Returns the major version number of the Oracle JDBC Driver installed as + * a service provider for java.sql.Driver. + * @return The major version number, such as 21 or 23. + */ + public static int jdbcVersion() { + try { + return DriverManager.getDriver("jdbc:oracle:thin:").getMajorVersion(); + } + catch (SQLException sqlException) { + throw new AssertionError(sqlException); + } + } + + /** + * Returns the minor version number of the Oracle JDBC Driver installed as + * a service provider for java.sql.Driver. + * @return The major version number, such as 11 for 21.11, or 4 for 23.4. + */ + public static int jdbcMinorVersion() { + try { + return DriverManager.getDriver("jdbc:oracle:thin:").getMinorVersion(); + } + catch (SQLException sqlException) { + throw new AssertionError(sqlException); + } + } + /** * Returns the options parsed from the "config.properties" resource. */ From 8e8a593247ee3debd0ddfdc3596f84ebf7290e86 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Wed, 15 May 2024 10:27:33 -0700 Subject: [PATCH 3/4] Added VECTOR to array example --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index fa716b3..bf46ab7 100644 --- a/README.md +++ b/README.md @@ -962,6 +962,16 @@ 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 From be1d1181c66323f1d9cc511c758b11b16708b84d Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Wed, 15 May 2024 14:34:25 -0700 Subject: [PATCH 4/4] Added version 23 assumption for VECTOR test --- src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index 9f01478..e8880df 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -3281,6 +3281,8 @@ else if (segment instanceof Message) { @Test public void testVectorReturningExample() { + assumeTrue(databaseVersion() >= 23); // VECTOR is not added until 23.4 + Connection connection = awaitOne(sharedConnection()); try { awaitExecution(connection.createStatement(