From 06021b52571a5cfc1569247f774e5b8663fd26a3 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Fri, 18 Oct 2024 14:44:22 -0700 Subject: [PATCH 1/3] Update to ojdbc 23.5 --- pom.xml | 2 +- .../oracle/r2dbc/impl/OracleBatchImpl.java | 14 ++-- .../r2dbc/impl/OracleConnectionImpl.java | 74 ++++++++++++++++++- .../r2dbc/impl/OracleStatementImpl.java | 73 +++++++++++++++--- .../r2dbc/impl/OracleConnectionImplTest.java | 8 ++ .../r2dbc/impl/OracleLargeObjectsTest.java | 4 + .../impl/OracleReactiveJdbcAdapterTest.java | 9 +++ .../r2dbc/impl/OracleStatementImplTest.java | 17 ++--- 8 files changed, 172 insertions(+), 29 deletions(-) diff --git a/pom.xml b/pom.xml index a727e54..180d75b 100755 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 11 - 23.4.0.24.05 + 23.5.0.24.07 1.0.0.RELEASE 3.5.11 1.0.3 diff --git a/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java b/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java index 94c1ad8..e45cb56 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java @@ -56,6 +56,9 @@ */ final class OracleBatchImpl implements Batch { + /** The OracleConnectionImpl that created this Batch */ + private final OracleConnectionImpl r2dbcConnection; + /** Adapts Oracle JDBC Driver APIs into Reactive Streams APIs */ private final ReactiveJdbcAdapter adapter; @@ -83,12 +86,11 @@ final class OracleBatchImpl implements Batch { * @param jdbcConnection JDBC connection to an Oracle Database. Not null. * @param adapter Adapts JDBC calls into reactive streams. Not null. */ - OracleBatchImpl( - Duration timeout, Connection jdbcConnection, ReactiveJdbcAdapter adapter) { + OracleBatchImpl(Duration timeout, OracleConnectionImpl r2dbcConnection) { this.timeout = timeout; - this.jdbcConnection = - requireNonNull(jdbcConnection, "jdbcConnection is null"); - this.adapter = requireNonNull(adapter, "adapter is null"); + this.r2dbcConnection = r2dbcConnection; + this.jdbcConnection = r2dbcConnection.jdbcConnection(); + this.adapter = r2dbcConnection.adapter(); } /** @@ -103,7 +105,7 @@ public Batch add(String sql) { requireOpenConnection(jdbcConnection); requireNonNull(sql, "sql is null"); statements.add( - new OracleStatementImpl(sql, timeout, jdbcConnection, adapter)); + new OracleStatementImpl(sql, timeout, r2dbcConnection)); return this; } diff --git a/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java b/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java index 8d82538..c453c5d 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java @@ -38,6 +38,8 @@ import java.sql.SQLException; import java.sql.Savepoint; import java.time.Duration; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import static io.r2dbc.spi.IsolationLevel.READ_COMMITTED; import static io.r2dbc.spi.IsolationLevel.SERIALIZABLE; @@ -126,6 +128,12 @@ final class OracleConnectionImpl implements Connection, Lifecycle { */ private TransactionDefinition currentTransaction = null; + /** + * A queue of tasks that must complete before the {@link #jdbcConnection} is + * closed. + */ + private final Queue> closeTasks = new ConcurrentLinkedQueue<>(); + /** * Constructs a new connection that uses the specified {@code adapter} to * perform database operations with the specified {@code jdbcConnection}. @@ -369,7 +377,46 @@ else if (isReadOnly == null && name == null) { */ @Override public Publisher close() { - return adapter.publishClose(jdbcConnection); + + Publisher closeTasksPublisher = Mono.defer(() -> { + Publisher[] closeTasksArray = closeTasks.toArray(Publisher[]::new); + closeTasks.clear(); + + return Flux.concatDelayError(closeTasksArray).then(); + }); + + return Flux.concatDelayError( + closeTasksPublisher, + adapter.publishClose(jdbcConnection)); + } + + /** + *

+ * Adds a publisher that must be subscribed to and must terminate before + * closing the JDBC connection. This can be used to ensure that a publisher + * has completed a task before the {@link #jdbcConnection()} has been closed + * and becomes unusable. + *

+ * A call to this method should always be accompanied with a call to + * {@link #removeCloseTask(Publisher)}: If the publisher is subscribed to + * and it terminates before {@link #close()} is called, then any reference + * to the Publisher must be removed so that it can be garbage collected. + *

+ * @param publisher Publisher that must terminate before closing the JDBC + * connection. Not null. + */ + void addCloseTask(Publisher publisher) { + closeTasks.add(publisher); + } + + /** + * Removes a publisher that was previously added with + * {@link #addCloseTask(Publisher)}. + * + * @param publisher Publisher to remove. Not null. + */ + void removeCloseTask(Publisher publisher) { + closeTasks.remove(publisher); } /** @@ -417,7 +464,7 @@ public Publisher commitTransaction() { @Override public Batch createBatch() { requireOpenConnection(jdbcConnection); - return new OracleBatchImpl(statementTimeout, jdbcConnection, adapter); + return new OracleBatchImpl(statementTimeout, this); } /** @@ -441,8 +488,7 @@ public Batch createBatch() { public Statement createStatement(String sql) { requireNonNull(sql, "sql is null"); requireOpenConnection(jdbcConnection); - return new OracleStatementImpl( - sql, statementTimeout, jdbcConnection, adapter); + return new OracleStatementImpl(sql, statementTimeout, this); } /** @@ -826,4 +872,24 @@ public Publisher preRelease() { }); } + /** + * Returns the JDBC connection that this R2DBC connection executes database + * calls with. + * + * @return The JDBC connection that backs this R2DBC connection. Not null. + */ + java.sql.Connection jdbcConnection() { + return jdbcConnection; + } + + /** + * Returns the adapter that adapts the asynchronous API of the + * {@link #jdbcConnection()} that backs this R2DBC connection. + * + * @return The JDBC connection that backs this R2DBC connection. Not null. + */ + ReactiveJdbcAdapter adapter() { + return adapter; + } + } \ No newline at end of file diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 0651f80..0b3eedb 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -197,6 +197,11 @@ final class OracleStatementImpl implements Statement { /** Adapts Oracle JDBC Driver APIs into Reactive Streams APIs */ private final ReactiveJdbcAdapter adapter; + /** + * The instance of OracleConnectionImpl that created this statement. + */ + private final OracleConnectionImpl r2dbcConnection; + /** * SQL Language command that this statement executes. The command is * provided by user code and may include parameter markers. @@ -255,15 +260,15 @@ final class OracleStatementImpl implements Statement { * @param sql SQL Language statement that may include parameter markers. * @param timeout Timeout applied to the execution of the constructed * {@code Statement}. Not null. Not negative. - * @param jdbcConnection JDBC connection to an Oracle Database. - * @param adapter Adapts JDBC calls into reactive streams. + * @param r2dbcConnection The OracleConnectionImpl that is creating this + * statement. Not null. */ OracleStatementImpl( - String sql, Duration timeout, Connection jdbcConnection, - ReactiveJdbcAdapter adapter) { + String sql, Duration timeout, OracleConnectionImpl r2dbcConnection) { this.sql = sql; - this.jdbcConnection = jdbcConnection; - this.adapter = adapter; + this.r2dbcConnection = r2dbcConnection; + this.jdbcConnection = r2dbcConnection.jdbcConnection(); + this.adapter = r2dbcConnection.adapter(); // The SQL string is parsed to identify parameter markers and allocate the // bindValues array accordingly @@ -987,13 +992,29 @@ private JdbcStatement(PreparedStatement preparedStatement, Object[] binds) { this.preparedStatement = preparedStatement; this.binds = binds; + // Work around for Oracle JDBC bug #37160069: The JDBC statement must be + // closed before closeAsyncOracle is called. This bug should be fixed + // by the 23.7 release of Oracle JDBC. The fix can be verified by the + // OracleConnectionImplTest.testSetStatementTimeout method (test won't + // fail, but look for an error in stderr). Typically, statement closing + // is a no-op if the connection is closed. However, if the statement + // executes SELECT ... FOR UPDATE, then JDBC will implicitly execute a + // commit() when the Statement (or really the ResultSet) is closed. This + // commit operation fails if the JDBC connection is already closed. + Publisher closePublisher = closeStatement(); + r2dbcConnection.addCloseTask(closePublisher); + + dependentCounter = new DependentCounter(Publishers.concatTerminal( + closePublisher, + Mono.fromRunnable(() -> + r2dbcConnection.removeCloseTask(closePublisher)))); + // Add this statement as a "party" (think j.u.c.Phaser) to the dependent // results by calling increment(). After the Result publisher returned by // execute() terminates, this statement "arrives" by calling decrement(). // Calling decrement() after the Result publisher terminates ensures that // the JDBC statement can not be closed until all results have had a // chance to be emitted to user code. - dependentCounter = new DependentCounter(closeStatement()); dependentCounter.increment(); } @@ -1864,7 +1885,24 @@ private Publisher convertBlobBind( Mono.from(adapter.publishBlobWrite(r2dbcBlob.stream(), jdbcBlob)) .thenReturn(jdbcBlob), jdbcBlob -> { - addDeallocation(adapter.publishBlobFree(jdbcBlob)); + Publisher freePublisher = adapter.publishBlobFree(jdbcBlob); + + // Work around for Oracle JDBC bug #37160069: All LOBs need to be + // freed before closeAsyncOracle is called. This bug should be fixed + // by the 23.7 release of Oracle JDBC. The fix can be verified by the + // clobInsert and blobInsert methods in the TestKit class of the R2DBC + // SPI test: These tests will subscribe to Connection.close() before + // this freePublisher is subscribed to. + r2dbcConnection.addCloseTask(freePublisher); + + addDeallocation( + Publishers.concatTerminal( + freePublisher, + Mono.fromRunnable(() -> + r2dbcConnection.removeCloseTask(freePublisher)))); + + // TODO: Why is discard() called here? It should be called by the + // user who allocated the Blob, not by Oracle R2DBC. return r2dbcBlob.discard(); }); } @@ -1891,7 +1929,24 @@ private Publisher convertClobBind( Mono.from(adapter.publishClobWrite(r2dbcClob.stream(), jdbcClob)) .thenReturn(jdbcClob), jdbcClob -> { - addDeallocation(adapter.publishClobFree(jdbcClob)); + Publisher freePublisher = adapter.publishClobFree(jdbcClob); + + // Work around for Oracle JDBC bug #37160069: All LOBs need to be + // freed before closeAsyncOracle is called. This bug should be fixed + // by the 23.7 release of Oracle JDBC. The fix can be verified by the + // clobInsert and blobInsert methods in the TestKit class of the R2DBC + // SPI test: These tests will subscribe to Connection.close() before + // this freePublisher is subscribed to. + r2dbcConnection.addCloseTask(freePublisher); + + addDeallocation( + Publishers.concatTerminal( + freePublisher, + Mono.fromRunnable(() -> + r2dbcConnection.removeCloseTask(freePublisher)))); + + // TODO: Why is discard() called here? It should be called by the + // user who allocated the Clob, not by Oracle R2DBC. return r2dbcClob.discard(); }); } diff --git a/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java index 8452007..6996e73 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java @@ -1356,6 +1356,14 @@ public void testValidate() { */ @Test public void testSetStatementTimeout() { + // Assume that oracle.jdbc.disablePipeline is only set to false when + // experimenting with pipelining on Mac OS. In this scenario, statement + // cancellation is known to not work. + String disabledProperty = System.getProperty("oracle.jdbc.disablePipeline"); + assumeTrue( + disabledProperty == null || disabledProperty.equalsIgnoreCase("true"), + "oracle.jdbc.disablePipeline is set, and the value is not \"true\""); + Connection connection = Mono.from(sharedConnection()).block(connectTimeout()); try { diff --git a/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java b/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java index a9ed093..144d29d 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java @@ -29,7 +29,10 @@ import io.r2dbc.spi.Statement; import oracle.r2dbc.OracleR2dbcObject; import oracle.r2dbc.OracleR2dbcTypes; +import oracle.r2dbc.test.DatabaseConfig; +import oracle.r2dbc.test.TestUtils; import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,6 +41,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static java.util.Arrays.asList; diff --git a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java index 8396da5..66ad6e2 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java @@ -98,6 +98,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; /** * Verifies that @@ -382,6 +383,14 @@ public void testConnectTimeout() */ @Test public void testStatementTimeout() { + // Assume that oracle.jdbc.disablePipeline is only set to false when + // experimenting with pipelining on Mac OS. In this scenario, statement + // cancellation is known to not work. + String disabledProperty = System.getProperty("oracle.jdbc.disablePipeline"); + assumeTrue( + disabledProperty == null || disabledProperty.equalsIgnoreCase("true"), + "oracle.jdbc.disablePipeline is set, and the value is not \"true\""); + Connection connection0 = Mono.from(ConnectionFactories.get(connectionFactoryOptions() .mutate() diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index bde77ba..d0a2f1b 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -31,6 +31,7 @@ import io.r2dbc.spi.Result; import io.r2dbc.spi.Result.Message; import io.r2dbc.spi.Result.UpdateCount; +import io.r2dbc.spi.Row; import io.r2dbc.spi.Statement; import oracle.r2dbc.OracleR2dbcObject; import oracle.r2dbc.OracleR2dbcOptions; @@ -76,12 +77,11 @@ 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.DatabaseConfig.sqlTimeout; import static oracle.r2dbc.test.TestUtils.constructObject; import static oracle.r2dbc.test.TestUtils.showErrors; -import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout; import static oracle.r2dbc.util.Awaits.awaitError; import static oracle.r2dbc.util.Awaits.awaitExecution; import static oracle.r2dbc.util.Awaits.awaitMany; @@ -3231,10 +3231,10 @@ public boolean equals(Object other) { // 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. + // #36607804, it will be fixed in the 23.6 release. Assumptions.assumeTrue( - jdbcMinorVersion() >= 5, - "Oracle JDBC 23.4 does not support generated keys for VECTOR"); + jdbcMinorVersion() >= 6, + "Oracle JDBC 23.5 does not support generated keys for VECTOR"); IdVector expected2 = new IdVector( 0, @@ -3257,12 +3257,11 @@ public boolean equals(Object other) { return Mono.just(((UpdateCount) segment).value()); } else if (segment instanceof Result.RowSegment) { - OutParameters outParameters = - ((Result.OutSegment)segment).outParameters(); + Row generatedRow = ((Result.RowSegment)segment).row(); return Mono.just(new IdVector( - outParameters.get("outId", Integer.class), - outParameters.get("outVector", VECTOR.class))); + generatedRow.get("id", Integer.class), + generatedRow.get("value", VECTOR.class))); } else if (segment instanceof Message) { throw ((Message)segment).exception(); From ba7a5f20d7429a6d7844386fd9c8ad7896c7ecc0 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Fri, 8 Nov 2024 11:32:28 -0800 Subject: [PATCH 2/3] Updating ojdbc11 and README --- README.md | 176 ++++++++---------- pom.xml | 6 +- .../r2dbc/impl/OracleConnectionImpl.java | 58 +++--- .../oracle/r2dbc/impl/OracleReadableImpl.java | 94 +++++----- .../oracle/r2dbc/impl/OracleResultImpl.java | 33 ++-- .../r2dbc/impl/OracleStatementImpl.java | 56 ++---- .../r2dbc/impl/OracleLargeObjectsTest.java | 140 +++++++++++++- 7 files changed, 336 insertions(+), 227 deletions(-) diff --git a/README.md b/README.md index 4ace718..a058c86 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,14 @@ Project Reactor, RxJava, and Akka Streams. # About This Version The 1.2.0 release Oracle R2DBC implements version 1.0.0.RELEASE of the R2DBC SPI. -Fixes in this release: - - [Fixed "Operator has been terminated" message](https://github.com/oracle/oracle-r2dbc/pull/134) - - [Checking for Zero Threads in the common ForkJoinPool](https://github.com/oracle/oracle-r2dbc/pull/131) - New features in this release: -- [Supporting Option Values from Supplier and Publisher](https://github.com/oracle/oracle-r2dbc/pull/137) -- [Added Options for Kerberos](https://github.com/oracle/oracle-r2dbc/pull/127) +- [Pipelined Operations](https://github.com/oracle/oracle-r2dbc/pull/145) +- [Vector Data Type](https://github.com/oracle/oracle-r2dbc/pull/146) +- [Options for Fetch Size and Proxy Authentication](https://github.com/oracle/oracle-r2dbc/pull/155) Updated dependencies: -- Updated Oracle JDBC from 21.7.0.0 to 21.11.0.0 -- Updated Project Reactor from 3.5.0 to 3.5.11 +- Updated Oracle JDBC from 21.11.0.0 to 23.6.0.24.10 +- Updated Project Reactor from 3.5.11 to 3.6.11 ## Installation Oracle R2DBC can be obtained from Maven Central. @@ -45,7 +42,7 @@ Oracle R2DBC can be obtained from Maven Central. com.oracle.database.r2dbc oracle-r2dbc - 1.2.0 + 1.3.0 ``` @@ -60,8 +57,8 @@ Oracle R2DBC can also be built from source using Maven: Oracle R2DBC is compatible with JDK 11 (or newer), and has the following runtime dependencies: - R2DBC SPI 1.0.0.RELEASE - Reactive Streams 1.0.3 -- Project Reactor 3.5.11 -- Oracle JDBC 21.11.0.0 for JDK 11 (ojdbc11.jar) +- Project Reactor 3.6.11 +- Oracle JDBC 23.6.0.24.10 for JDK 11 (ojdbc11.jar) - Oracle R2DBC relies on the Oracle JDBC Driver's [Reactive Extensions ](https://docs.oracle.com/en/database/oracle/oracle-database/23/jjdbc/jdbc-reactive-extensions.html#GUID-1C40C43B-3823-4848-8B5A-D2F97A82F79B) APIs. @@ -73,7 +70,7 @@ Oracle R2DBC can only interoperate with libraries that support the 1.0.0.RELEASE version of the R2DBC SPI. When using libraries like Spring and r2dbc-pool, be sure to use a version which supports the 1.0.0.RELEASE of the SPI. -Oracle R2DBC depends on the JDK 11 build of Oracle JDBC 21.11.0.0. Other +Oracle R2DBC depends on the JDK 11 build of Oracle JDBC 23.6.0.24.10. Other libraries may depend on a different version of Oracle JDBC, and this version may be incompatible. To resolve incompatibilities, it may be necessary to explicitly declare the dependency in your project, ie: @@ -81,11 +78,11 @@ declare the dependency in your project, ie: com.oracle.database.jdbc ojdbc11 - 21.11.0.0 + 23.6.0.24.10 ``` -## Code Examples +## Basic Code Examples The following method returns an Oracle R2DBC `ConnectionFactory` ```java @@ -150,7 +147,8 @@ a greeting message. "France" is set as the bind value for `locale_name`, so the query should return a greeting like "Bonjour" when `row.get("greeting")` is called. -Additional code examples can be found [here](sample). +Additional code examples appear throughout this document, and can also be found +[here](sample). ## Help For help programming with Oracle R2DBC, ask questions on Stack Overflow tagged with [[oracle] and [r2dbc]](https://stackoverflow.com/tags/oracle+r2dbc). The development team monitors Stack Overflow regularly. @@ -169,7 +167,7 @@ vulnerability disclosure process. ## License -Copyright (c) 2021, 2023 Oracle and/or its affiliates. +Copyright (c) 2021, 2024 Oracle and/or its affiliates. This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License @@ -366,9 +364,12 @@ ConnectionFactoryOptions.builder() If this option is not configured, then the common `java.util.concurrent.ForkJoinPool` is used as a default. + #### Configuring Oracle JDBC Connection Properties -A subset of Oracle JDBC's connection properties are also supported by Oracle -R2DBC. These connection properties may be configured as options having the same +A subset of Oracle JDBC's connection properties are defined as `Option` +constants in the +[OracleR2dbcOptions](src/main/java/oracle/r2dbc/OracleR2dbcOptions.java) class. +These connection properties may be configured as options having the same name as the Oracle JDBC connection property, and may have `CharSequence` value types. @@ -406,23 +407,6 @@ Oracle R2DBC. - [ssl.trustManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_TRUSTMANAGERFACTORY_ALGORITHM) - [oracle.net.ssl_context_protocol](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_SSL_CONTEXT_PROTOCOL) -##### Miscellaneous Connection Properties - - [oracle.jdbc.fanEnabled](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_FAN_ENABLED) - - [oracle.jdbc.implicitStatementCacheSize](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE) - - [oracle.jdbc.defaultLobPrefetchSize](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE) - - [oracle.net.disableOob](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK) - - Out of band (OOB) breaks effect statement timeouts. Set this to "true" if - statement timeouts are not working correctly. OOB breaks are a - - [requirement for pipelining](#requirements-for-pipelining) - - [oracle.jdbc.enableQueryResultCache](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_ENABLE_QUERY_RESULT_CACHE) - - Cached query results can cause phantom reads even if the serializable - transaction isolation level is set. Set this to "false" if using the - serializable isolation level. - - [oracle.jdbc.timezoneAsRegion](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_TIMEZONE_AS_REGION) - - Setting this option to "false" may resolve "ORA-01882: timezone region not - found". The ORA-01882 error happens when Oracle Database doesn't recognize - the name returned by `java.util.TimeZone.getDefault().getId()`. - ##### Database Tracing Connection Properties - [v$session.terminal](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_TERMINAL) - [v$session.machine](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_MACHINE) @@ -462,6 +446,25 @@ Oracle R2DBC. - [oracle.net.ldap.ssl.trustManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTMANAGER_FACTORY_ALGORITHM) - [oracle.net.ldap.ssl.ssl_context_protocol](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_CONTEXT_PROTOCOL) +##### Miscellaneous Connection Properties +- [oracle.jdbc.fanEnabled](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_FAN_ENABLED) +- [oracle.jdbc.implicitStatementCacheSize](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE) +- [oracle.jdbc.defaultLobPrefetchSize](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE) +- [oracle.net.disableOob](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK) + - Out of band (OOB) breaks effect statement timeouts. Set this to "true" if + statement timeouts are not working correctly. OOB breaks are a + [requirement for pipelining](#requirements-for-pipelining) +- [oracle.jdbc.enableQueryResultCache](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_ENABLE_QUERY_RESULT_CACHE) + - Cached query results can cause phantom reads even if the serializable + transaction isolation level is set. Set this to "false" if using the + serializable isolation level. +- [oracle.jdbc.timezoneAsRegion](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_TIMEZONE_AS_REGION) + - Setting this option to "false" may resolve "ORA-01882: timezone region not + found". The ORA-01882 error happens when Oracle Database doesn't recognize + the name returned by `java.util.TimeZone.getDefault().getId()`. +- [oracle.jdbc.defaultRowPrefetch](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_DEFAULT_ROW_PREFETCH) +- [oracle.jdbc.proxyClientName](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_PROXY_CLIENT_NAME) + ### Thread Safety Oracle R2DBC's `ConnectionFactory` and `ConnectionFactoryProvider` are the only classes that have a thread safe implementation. All other classes implemented @@ -772,14 +775,14 @@ for the out parameters is emitted last, after the `Result` for each cursor. Oracle R2DBC supports type mappings between Java and SQL for non-standard data types of Oracle Database. -| Oracle SQL Type | Java Type | -|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------| -| [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[]`, `byte[]`, or `oracle.sql.VECTOR` | +| Oracle SQL Type | Java Type | +|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| +| [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[]`, `byte[]`, `boolean[]` 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 @@ -1011,7 +1014,7 @@ Publisher mapRefCursorRows(Result refCursorResult) { ``` ### VECTOR -The default mapping for `VECTOR` is the +The default mapping for the VECTOR data type 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)`: @@ -1027,24 +1030,16 @@ void bindVector(Statement statement, float[] floatArray) throws 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 `oracle.sql.VECTOR` class defines factory methods that convert a +`double[]`, `float[]`, `long[]`, `int[]`, `short[]`, `byte[]`, or `boolean[]` +into a VECTOR of a specific dimension type: +- `ofFloat64Values` +- `ofFloat32Values` +- `ofInt8Values` +- `ofBinaryValues` (only supports `boolean[]` and `byte[]`) + +These factory methods may perform a lossy conversion, such +as when converting a `double[]` into a VECTOR of FLOAT32 dimensions. [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 @@ -1052,60 +1047,35 @@ IN/OUT parameter: ```java void registerOutVector(Statement statement) { Parameter outVector = Parameters.out(OracleR2dbcTypes.VECTOR); - statement.bind("vector", outVector); + statement.bind("out_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[]`: +The `OracleR2dbcTypes.VECTOR` type descriptor can also be used as an alternative +to `oracle.sql.VECTOR` for binding a `double[]`, `float[]`, `byte[]`, or +`boolean[]`. Arrays of these types are respectively converted into a VECTOR of +FLOAT64, FLOAT32, INT8, or BINARY dimensions. ```java void bindVector(Statement statement, float[] floatArray) { Parameter inVector = Parameters.in(OracleR2dbcTypes.VECTOR, floatArray); - statement.bind("vector", inVector); + statement.bind("in_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: +Note that passing arrays directly into `Statement.bind(int/String, Object)` will +NOT create a VECTOR bind. The R2DBC Specification already defines ARRAY as +the default mapping for Java arrays, not VECTOR. + +A VECTOR column or OUT parameter is converted to `oracle.sql.VECTOR` by +default. But a VECTOR column or OUT parameter having FLOAT64, FLOAT32, or INT8 +dimensions may also be converted to +`double[]`, `float[]`, `long[]`, `int[]`, `short[]`, `byte[]`, or `boolean[]`. +A VECTOR of BINARY dimensions may only be converted to `boolean[]` or `byte[]`. +These array classes may be passed to the `get` methods of a `Readable`: ```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 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 180d75b..b33e710 100755 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ com.oracle.database.r2dbc oracle-r2dbc - 1.2.0 + 1.3.0 oracle-r2dbc Oracle R2DBC Driver implementing version 1.0.0 of the R2DBC SPI for Oracle Database. @@ -65,9 +65,9 @@ 11 - 23.5.0.24.07 + 23.6.0.24.10 1.0.0.RELEASE - 3.5.11 + 3.6.11 1.0.3 5.9.1 5.3.19 diff --git a/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java b/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java index c453c5d..11b8bec 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleConnectionImpl.java @@ -38,8 +38,8 @@ import java.sql.SQLException; import java.sql.Savepoint; import java.time.Duration; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import static io.r2dbc.spi.IsolationLevel.READ_COMMITTED; import static io.r2dbc.spi.IsolationLevel.SERIALIZABLE; @@ -47,8 +47,8 @@ import static io.r2dbc.spi.TransactionDefinition.LOCK_WAIT_TIMEOUT; import static io.r2dbc.spi.TransactionDefinition.NAME; import static io.r2dbc.spi.TransactionDefinition.READ_ONLY; -import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; import static oracle.r2dbc.impl.OracleR2dbcExceptions.fromJdbc; +import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireOpenConnection; import static oracle.r2dbc.impl.OracleR2dbcExceptions.runJdbc; import static oracle.r2dbc.impl.OracleR2dbcExceptions.toR2dbcException; @@ -129,10 +129,10 @@ final class OracleConnectionImpl implements Connection, Lifecycle { private TransactionDefinition currentTransaction = null; /** - * A queue of tasks that must complete before the {@link #jdbcConnection} is - * closed. + * A set of tasks that must complete before the {@link #jdbcConnection} is + * closed. The tasks are executed by subscribing to a Publisher. */ - private final Queue> closeTasks = new ConcurrentLinkedQueue<>(); + private final Set> closeTasks = ConcurrentHashMap.newKeySet(); /** * Constructs a new connection that uses the specified {@code adapter} to @@ -393,30 +393,36 @@ public Publisher close() { /** *

* Adds a publisher that must be subscribed to and must terminate before - * closing the JDBC connection. This can be used to ensure that a publisher - * has completed a task before the {@link #jdbcConnection()} has been closed - * and becomes unusable. + * closing the JDBC connection. This method can be used to ensure that certain + * tasks are completed before the {@link #jdbcConnection()} is closed and + * becomes unusable. *

- * A call to this method should always be accompanied with a call to - * {@link #removeCloseTask(Publisher)}: If the publisher is subscribed to - * and it terminates before {@link #close()} is called, then any reference - * to the Publisher must be removed so that it can be garbage collected. + * The publisher returned by this method emits the same result as the + * publisher passed into this method. However, when the returned publisher + * terminates, it will also remove any reference to the publisher that was + * passed into this method. If the returned publisher is never subscribed + * to, then the reference will not be cleared until the connection is + * closed! So, this method should only be used in cases where the user is + * responsible for subscribing to the returned publisher, in the same way they + * would be responsible for calling close() on an AutoCloseable. If the user + * is not responsible for subscribing, then there is no reasonable way for + * them to reduce the number object references that this method will create; + * They would either need to close this connection, or subscribe to a + * publisher when there is no obligation to do so. *

- * @param publisher Publisher that must terminate before closing the JDBC - * connection. Not null. + * + * @param publisher Publisher that must be subscribed to before closing the + * JDBC connection. Not null. + * + * @return A publisher that emits the same result as the publisher passed into + * this method, and clears any reference to it when terminated. Not null. */ - void addCloseTask(Publisher publisher) { + Publisher addCloseTask(Publisher publisher) { closeTasks.add(publisher); - } - /** - * Removes a publisher that was previously added with - * {@link #addCloseTask(Publisher)}. - * - * @param publisher Publisher to remove. Not null. - */ - void removeCloseTask(Publisher publisher) { - closeTasks.remove(publisher); + return Publishers.concatTerminal( + publisher, + Mono.fromRunnable(() -> closeTasks.remove(publisher))); } /** @@ -876,7 +882,7 @@ public Publisher preRelease() { * Returns the JDBC connection that this R2DBC connection executes database * calls with. * - * @return The JDBC connection that backs this R2DBC connection. Not null. + * @return The JDBC connection which backs this R2DBC connection. Not null. */ java.sql.Connection jdbcConnection() { return jdbcConnection; diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java index c18aebd..850f886 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java @@ -46,6 +46,7 @@ import oracle.sql.INTERVALYM; import oracle.sql.TIMESTAMPLTZ; import oracle.sql.TIMESTAMPTZ; +import org.reactivestreams.Publisher; import java.math.BigDecimal; import java.nio.ByteBuffer; @@ -93,6 +94,9 @@ class OracleReadableImpl implements io.r2dbc.spi.Readable { + /** The R2DBC connection that created this readable */ + private final OracleConnectionImpl r2dbcConnection; + /** The JDBC connection that created this readable */ private final java.sql.Connection jdbcConnection; @@ -117,21 +121,21 @@ 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 + * @param r2dbcConnection R2DBC 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( - java.sql.Connection jdbcConnection, DependentCounter dependentCounter, - JdbcReadable jdbcReadable, ReadablesMetadata readablesMetadata, - ReactiveJdbcAdapter adapter) { - this.jdbcConnection = jdbcConnection; + OracleConnectionImpl r2dbcConnection, DependentCounter dependentCounter, + JdbcReadable jdbcReadable, ReadablesMetadata readablesMetadata) { + this.r2dbcConnection = r2dbcConnection; + this.jdbcConnection = r2dbcConnection.jdbcConnection(); this.dependentCounter = dependentCounter; this.jdbcReadable = jdbcReadable; this.readablesMetadata = readablesMetadata; - this.adapter = adapter; + this.adapter = r2dbcConnection.adapter(); } /** @@ -151,11 +155,10 @@ private OracleReadableImpl( * {@code metadata}. Not null. */ static Row createRow( - java.sql.Connection jdbcConnection, DependentCounter dependentCounter, - JdbcReadable jdbcReadable, RowMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { + OracleConnectionImpl r2dbcConnection, DependentCounter dependentCounter, + JdbcReadable jdbcReadable, RowMetadataImpl metadata) { return new RowImpl( - jdbcConnection, dependentCounter, jdbcReadable, metadata, adapter); + r2dbcConnection, dependentCounter, jdbcReadable, metadata); } /** @@ -164,7 +167,7 @@ static Row createRow( * 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 + * @param r2dbcConnection R2DBC connection that created the * {@code jdbcReadable}. Not null. * @param dependentCounter Counter that is increased for each dependent * {@code Result} created by the returned {@code OutParameters} @@ -175,11 +178,10 @@ static Row createRow( * {@code metadata}. Not null. */ static OutParameters createOutParameters( - java.sql.Connection jdbcConnection, DependentCounter dependentCounter, - JdbcReadable jdbcReadable, OutParametersMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { + OracleConnectionImpl r2dbcConnection, DependentCounter dependentCounter, + JdbcReadable jdbcReadable, OutParametersMetadataImpl metadata) { return new OutParametersImpl( - jdbcConnection, dependentCounter, jdbcReadable, metadata, adapter); + r2dbcConnection, dependentCounter, jdbcReadable, metadata); } /** @@ -335,11 +337,16 @@ private ByteBuffer getByteBuffer(int index) { */ private Blob getBlob(int index) { java.sql.Blob jdbcBlob = jdbcReadable.getObject(index, java.sql.Blob.class); - return jdbcBlob == null - ? null - : OracleLargeObjects.createBlob( - adapter.publishBlobRead(jdbcBlob), - adapter.publishBlobFree(jdbcBlob)); + + if (jdbcBlob == null) + return null; + + Publisher freePublisher = + r2dbcConnection.addCloseTask(adapter.publishBlobFree(jdbcBlob)); + + return OracleLargeObjects.createBlob( + adapter.publishBlobRead(jdbcBlob), + freePublisher); } /** @@ -367,11 +374,15 @@ private Clob getClob(int index) { jdbcClob = jdbcReadable.getObject(index, java.sql.Clob.class); } - return jdbcClob == null - ? null - : OracleLargeObjects.createClob( - adapter.publishClobRead(jdbcClob), - adapter.publishClobFree(jdbcClob)); + if (jdbcClob == null) + return null; + + Publisher freePublisher = + r2dbcConnection.addCloseTask(adapter.publishClobFree(jdbcClob)); + + return OracleLargeObjects.createClob( + adapter.publishClobRead(jdbcClob), + freePublisher); } /** @@ -685,11 +696,10 @@ private OracleR2dbcObjectImpl getOracleR2dbcObject(int index) { return null; return new OracleR2dbcObjectImpl( - jdbcConnection, + r2dbcConnection, dependentCounter, new StructJdbcReadable(oracleStruct), - ReadablesMetadata.createAttributeMetadata(oracleStruct), - adapter); + ReadablesMetadata.createAttributeMetadata(oracleStruct)); } /** @@ -956,7 +966,7 @@ private Result getResult(int index) { dependentCounter.increment(); return OracleResultImpl.createQueryResult( - dependentCounter, resultSet, adapter); + r2dbcConnection, dependentCounter, resultSet); } /** @@ -994,17 +1004,16 @@ private static final class RowImpl * {@code jdbcReadable}, and uses the specified {@code rowMetadata} to * determine the default type mapping of column values. *

- * @param jdbcConnection JDBC connection that created the + * @param r2dbcConnection R2DBC 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. */ private RowImpl( - java.sql.Connection jdbcConnection, DependentCounter dependentCounter, - JdbcReadable jdbcReadable, RowMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { - super(jdbcConnection, dependentCounter, jdbcReadable, metadata, adapter); + OracleConnectionImpl r2dbcConnection, DependentCounter dependentCounter, + JdbcReadable jdbcReadable, RowMetadataImpl metadata) { + super(r2dbcConnection, dependentCounter, jdbcReadable, metadata); this.metadata = metadata; } @@ -1044,10 +1053,9 @@ private static final class OutParametersImpl * @param adapter Adapts JDBC calls into reactive streams. Not null. */ private OutParametersImpl( - java.sql.Connection jdbcConnection, DependentCounter dependentCounter, - JdbcReadable jdbcReadable, OutParametersMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { - super(jdbcConnection, dependentCounter, jdbcReadable, metadata, adapter); + OracleConnectionImpl r2dbcConnection, DependentCounter dependentCounter, + JdbcReadable jdbcReadable, OutParametersMetadataImpl metadata) { + super(r2dbcConnection, dependentCounter, jdbcReadable, metadata); this.metadata = metadata; } @@ -1068,20 +1076,18 @@ private final class OracleR2dbcObjectImpl * {@code jdbcReadable} and obtains metadata of the values from * {@code outParametersMetaData}. *

- * @param jdbcConnection JDBC connection that created the + * @param r2dbcConnection R2DBC connection that created the * {@code jdbcReadable}. Not null. * @param structJdbcReadable Readable values from a JDBC Driver. Not null. * @param metadata Metadata of each value. Not null. * @param adapter Adapts JDBC calls into reactive streams. Not null. */ private OracleR2dbcObjectImpl( - java.sql.Connection jdbcConnection, + OracleConnectionImpl r2dbcConnection, DependentCounter dependentCounter, StructJdbcReadable structJdbcReadable, - OracleR2dbcObjectMetadataImpl metadata, - ReactiveJdbcAdapter adapter) { - super( - jdbcConnection, dependentCounter, structJdbcReadable, metadata, adapter); + OracleR2dbcObjectMetadataImpl metadata) { + super(r2dbcConnection, dependentCounter, structJdbcReadable, metadata); this.metadata = metadata; } diff --git a/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java b/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java index 5757a6b..6cd7327 100644 --- a/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java @@ -262,6 +262,8 @@ protected void setPublished() { /** * Creates a {@code Result} that publishes a JDBC {@code resultSet} as * {@link RowSegment}s + * @param r2dbcConnection The R2DBC connection that created this result. Not + * null. * @param dependentCounter Collection of results that depend on the JDBC * statement which created the {@code ResultSet} to remain open until all * results are consumed. @@ -270,9 +272,9 @@ protected void setPublished() { * @return A {@code Result} for a ResultSet */ public static OracleResultImpl createQueryResult( - DependentCounter dependentCounter, ResultSet resultSet, - ReactiveJdbcAdapter adapter) { - return new ResultSetResult(dependentCounter, resultSet, adapter); + OracleConnectionImpl r2dbcConnection, + DependentCounter dependentCounter, ResultSet resultSet) { + return new ResultSetResult(r2dbcConnection, dependentCounter, resultSet); } /** @@ -296,6 +298,8 @@ static OracleResultImpl createCallResult( * {@link UpdateCount} segment, followed by a {@code generatedKeys} * {@code ResultSet} as {@link RowSegment}s * @return A {@code Result} for values generated by DML + * @param r2dbcConnection The R2DBC connection that created this result. Not + * null. * @param updateCount Update count to publish * @param dependentCounter Collection of results that depend on the JDBC * statement which created the {@code generatedKeys} {@code ResultSet} to @@ -304,10 +308,10 @@ static OracleResultImpl createCallResult( * @param adapter Adapts JDBC calls into reactive streams. Not null. */ static OracleResultImpl createGeneratedValuesResult( - long updateCount, DependentCounter dependentCounter, - ResultSet generatedKeys, ReactiveJdbcAdapter adapter) { + OracleConnectionImpl r2dbcConnection, long updateCount, + DependentCounter dependentCounter, ResultSet generatedKeys) { return new GeneratedKeysResult( - updateCount, dependentCounter, generatedKeys, adapter); + r2dbcConnection, updateCount, dependentCounter, generatedKeys); } /** @@ -434,17 +438,20 @@ protected Publisher mapSegments( */ private static final class ResultSetResult extends DependentResult { + private final OracleConnectionImpl r2dbcConnection; private final ResultSet resultSet; private final RowMetadataImpl metadata; private final ReactiveJdbcAdapter adapter; private ResultSetResult( - DependentCounter dependentCounter, ResultSet resultSet, - ReactiveJdbcAdapter adapter) { + OracleConnectionImpl r2dbcConnection, + DependentCounter dependentCounter, + ResultSet resultSet) { super(dependentCounter); + this.r2dbcConnection = r2dbcConnection; this.resultSet = resultSet; this.metadata = createRowMetadata(fromJdbc(resultSet::getMetaData)); - this.adapter = adapter; + this.adapter = r2dbcConnection.adapter(); } @Override @@ -457,8 +464,7 @@ protected Publisher mapDependentSegments( // Avoiding object allocation by reusing the same Row object ReusableJdbcReadable reusableJdbcReadable = new ReusableJdbcReadable(); Row row = createRow( - fromJdbc(() -> resultSet.getStatement().getConnection()), - dependentCounter, reusableJdbcReadable, metadata, adapter); + r2dbcConnection, dependentCounter, reusableJdbcReadable, metadata); return adapter.publishRows(resultSet, jdbcReadable -> { reusableJdbcReadable.current = jdbcReadable; @@ -500,11 +506,12 @@ private static final class GeneratedKeysResult extends OracleResultImpl { private final OracleResultImpl generatedKeysResult; private GeneratedKeysResult( + OracleConnectionImpl r2dbcConnection, long updateCount, DependentCounter dependentCounter, - ResultSet generatedKeys, ReactiveJdbcAdapter adapter) { + ResultSet generatedKeys) { updateCountResult = createUpdateCountResult(updateCount); generatedKeysResult = - createQueryResult(dependentCounter, generatedKeys, adapter); + createQueryResult(r2dbcConnection, dependentCounter, generatedKeys); } @Override diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 0b3eedb..8cd2771 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -1001,13 +1001,9 @@ private JdbcStatement(PreparedStatement preparedStatement, Object[] binds) { // executes SELECT ... FOR UPDATE, then JDBC will implicitly execute a // commit() when the Statement (or really the ResultSet) is closed. This // commit operation fails if the JDBC connection is already closed. - Publisher closePublisher = closeStatement(); - r2dbcConnection.addCloseTask(closePublisher); - - dependentCounter = new DependentCounter(Publishers.concatTerminal( - closePublisher, - Mono.fromRunnable(() -> - r2dbcConnection.removeCloseTask(closePublisher)))); + Publisher closePublisher = + r2dbcConnection.addCloseTask(closeStatement()); + dependentCounter = new DependentCounter(closePublisher); // Add this statement as a "party" (think j.u.c.Phaser) to the dependent // results by calling increment(). After the Result publisher returned by @@ -1266,7 +1262,8 @@ private OracleResultImpl getCurrentResult(boolean isResultSet) { return fromJdbc(() -> { if (isResultSet) { return createQueryResult( - dependentCounter, preparedStatement.getResultSet(), adapter); + r2dbcConnection, dependentCounter, + preparedStatement.getResultSet()); } else { long updateCount = preparedStatement.getLargeUpdateCount(); @@ -1885,25 +1882,17 @@ private Publisher convertBlobBind( Mono.from(adapter.publishBlobWrite(r2dbcBlob.stream(), jdbcBlob)) .thenReturn(jdbcBlob), jdbcBlob -> { - Publisher freePublisher = adapter.publishBlobFree(jdbcBlob); - // Work around for Oracle JDBC bug #37160069: All LOBs need to be // freed before closeAsyncOracle is called. This bug should be fixed // by the 23.7 release of Oracle JDBC. The fix can be verified by the // clobInsert and blobInsert methods in the TestKit class of the R2DBC // SPI test: These tests will subscribe to Connection.close() before // this freePublisher is subscribed to. - r2dbcConnection.addCloseTask(freePublisher); - - addDeallocation( - Publishers.concatTerminal( - freePublisher, - Mono.fromRunnable(() -> - r2dbcConnection.removeCloseTask(freePublisher)))); + Publisher freePublisher = + r2dbcConnection.addCloseTask(adapter.publishBlobFree(jdbcBlob)); + addDeallocation(freePublisher); - // TODO: Why is discard() called here? It should be called by the - // user who allocated the Blob, not by Oracle R2DBC. - return r2dbcBlob.discard(); + return Mono.empty(); }); } @@ -1929,25 +1918,17 @@ private Publisher convertClobBind( Mono.from(adapter.publishClobWrite(r2dbcClob.stream(), jdbcClob)) .thenReturn(jdbcClob), jdbcClob -> { - Publisher freePublisher = adapter.publishClobFree(jdbcClob); - // Work around for Oracle JDBC bug #37160069: All LOBs need to be // freed before closeAsyncOracle is called. This bug should be fixed // by the 23.7 release of Oracle JDBC. The fix can be verified by the // clobInsert and blobInsert methods in the TestKit class of the R2DBC // SPI test: These tests will subscribe to Connection.close() before // this freePublisher is subscribed to. - r2dbcConnection.addCloseTask(freePublisher); - - addDeallocation( - Publishers.concatTerminal( - freePublisher, - Mono.fromRunnable(() -> - r2dbcConnection.removeCloseTask(freePublisher)))); + Publisher freePublisher = + r2dbcConnection.addCloseTask(adapter.publishClobFree(jdbcClob)); + addDeallocation(freePublisher); - // TODO: Why is discard() called here? It should be called by the - // user who allocated the Clob, not by Oracle R2DBC. - return r2dbcClob.discard(); + return Mono.empty(); }); } @@ -2065,9 +2046,10 @@ protected Publisher executeJdbc() { Mono.just(createCallResult( dependentCounter, createOutParameters( - fromJdbc(preparedStatement::getConnection), + r2dbcConnection, dependentCounter, - new JdbcOutParameters(), metadata, adapter), + new JdbcOutParameters(), + metadata), adapter))); } @@ -2251,8 +2233,10 @@ protected Publisher executeJdbc() { if (generatedKeys.isBeforeFirst()) { return Mono.just(createGeneratedValuesResult( - preparedStatement.getLargeUpdateCount(), - dependentCounter, generatedKeys, adapter)) + r2dbcConnection, + preparedStatement.getLargeUpdateCount(), + dependentCounter, + generatedKeys)) .concatWith(super.getResults( preparedStatement.getMoreResults(KEEP_CURRENT_RESULT))); } diff --git a/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java b/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java index 144d29d..5645586 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleLargeObjectsTest.java @@ -30,18 +30,17 @@ import oracle.r2dbc.OracleR2dbcObject; import oracle.r2dbc.OracleR2dbcTypes; import oracle.r2dbc.test.DatabaseConfig; -import oracle.r2dbc.test.TestUtils; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import static java.util.Arrays.asList; @@ -51,10 +50,13 @@ import static oracle.r2dbc.util.Awaits.awaitMany; import static oracle.r2dbc.util.Awaits.awaitNone; import static oracle.r2dbc.util.Awaits.awaitOne; +import static oracle.r2dbc.util.Awaits.awaitQuery; 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; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Verifies the Oracle R2DBC Driver implements behavior related to {@link Blob} @@ -504,4 +506,138 @@ public void testClobObject() { } } + /** + * Verifies behavior around discarding LOBs. + * + * A Blob/Clob bind should not be discarded until a user calls discard() and + * subscribes. Oracle R2DBC used to call discard() itself on Blob/Clob binds, + * which is not correct. + * + * When Connection.close() is called and subscribed to, it should not fail + * due to Oracle JDBC bug #37160069 which requires all LOBs to be freed before + * closeAsyncOracle is called. + */ + @Test + public void testLobDiscard() { + byte[] data = getBytes(64 * 1024); + + class TestBlob implements Blob { + final ByteBuffer blobData = ByteBuffer.wrap(data); + + boolean isDiscarded = false; + + @Override + public Publisher stream() { + return Mono.just(blobData); + } + + @Override + public Publisher discard() { + return Mono.empty() + .doOnSuccess(nil -> isDiscarded = true); + } + } + + class TestClob implements Clob { + final CharBuffer clobData = + CharBuffer.wrap(new String(data, StandardCharsets.US_ASCII)); + + boolean isDiscarded = false; + + @Override + public Publisher stream() { + return Mono.just(clobData); + } + + @Override + public Publisher discard() { + return Mono.empty() + .doOnSuccess(nil -> isDiscarded = true); + } + } + + Connection connection = + Mono.from(DatabaseConfig.newConnection()).block(connectTimeout()); + try { + awaitExecution( + connection.createStatement( + "CREATE TABLE testLobDiscard (blobValue BLOB, clobValue CLOB)")); + + // Verify that LOBs are not discarded until discard() is subscribed to + TestBlob testBlob = new TestBlob(); + TestClob testClob = new TestClob(); + awaitUpdate(1, connection.createStatement( + "INSERT INTO testLobDiscard VALUES (?, ?)") + .bind(0, testBlob) + .bind(1, testClob)); + assertFalse(testBlob.isDiscarded); + assertFalse(testClob.isDiscarded); + awaitNone(testBlob.discard()); + awaitNone(testClob.discard()); + assertTrue(testBlob.isDiscarded); + assertTrue(testClob.isDiscarded); + + // Query temporary LOBs, and don't discard them + Object[] blobAndClob = + awaitOne(Flux.from(connection.createStatement( + "SELECT TO_BLOB(HEXTORAW('ABCDEF')), TO_CLOB('ABCDEF') FROM sys.dual") + .execute()) + .flatMap(result -> + result.map(row -> + new Object[]{ + row.get(0, Blob.class), + row.get(1, Clob.class)}))); + + awaitExecution(connection.createStatement( + "DROP TABLE testLobDiscard")); + + // Close the connection. It should not fail due to Oracle JDBC bug + // #37160069. + awaitNone(connection.close()); + } + catch (Exception exception) { + try { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testLobDiscard")); + tryAwaitNone(connection.close()); + } + catch (Exception closeException) { + exception.addSuppressed(closeException); + } + throw exception; + } + } + + /** + * Verifies inserts and selects on NULL valued BLOBs and CLOBs + */ + @Test + public void testNullLob() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testNullLob(blobValue BLOB, clobValue CLOB)")); + + awaitUpdate(1, connection.createStatement( + "INSERT INTO testNullLob VALUES (?, ?)") + .bindNull(0, Blob.class) + .bindNull(1, Clob.class)); + + awaitQuery( + List.of( + Arrays.asList(null, null)), + row -> Arrays.asList( + row.get(0, Blob.class), + row.get(1, Clob.class)), + connection.createStatement( + "SELECT blobValue, clobValue FROM testNullLob")); + + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testNullLob")); + tryAwaitNone(connection.close()); + } + } + } From ad2d0233830d6341b26b14e965fd5c63a010c581 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Fri, 8 Nov 2024 13:37:27 -0800 Subject: [PATCH 3/3] DB 19 test compatiblity and README updates --- README.md | 31 +++++++++++-------- ...acleConnectionFactoryProviderImplTest.java | 7 ++--- .../r2dbc/impl/OracleResultImplTest.java | 28 ++++++++--------- .../r2dbc/impl/OracleStatementImplTest.java | 15 +++++++++ .../oracle/r2dbc/test/DatabaseConfig.java | 18 +++++++++-- .../java/oracle/r2dbc/test/OracleTestKit.java | 7 ++--- .../r2dbc/util/SharedConnectionFactory.java | 2 +- 7 files changed, 68 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index fb26618..b1407a4 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ Project Reactor, RxJava, and Akka Streams. [Reactive Streams Specification v1.0.3](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.3/README.md) # About This Version -The 1.2.0 release Oracle R2DBC implements version 1.0.0.RELEASE of the R2DBC SPI. +The 1.3.0 release Oracle R2DBC implements version 1.0.0.RELEASE of the R2DBC SPI. New features in this release: -- [Pipelined Operations](https://github.com/oracle/oracle-r2dbc/pull/145) -- [Vector Data Type](https://github.com/oracle/oracle-r2dbc/pull/146) -- [Options for Fetch Size and Proxy Authentication](https://github.com/oracle/oracle-r2dbc/pull/155) +- [Pipelining](#pipelining) +- [Vector Data Type](#vector) +- [Fetch Size and Proxy Authentication Options](https://github.com/oracle/oracle-r2dbc/pull/155) Updated dependencies: - Updated Oracle JDBC from 21.11.0.0 to 23.6.0.24.10 @@ -369,23 +369,28 @@ If this option is not configured, then the common A subset of Oracle JDBC's connection properties are defined as `Option` constants in the [OracleR2dbcOptions](src/main/java/oracle/r2dbc/OracleR2dbcOptions.java) class. -These connection properties may be configured as options having the same -name as the Oracle JDBC connection property, and may have `CharSequence` value -types. - -For example, the following URL configures the `oracle.net.wallet_location` -connection property: +If an Oracle JDBC property is not defined as an `Option`, in most cases it can +instead be configured by a +[connection properties file](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_CONFIG_FILE) +or a JVM system property instead. +[Pull requests to add missing options](https://github.com/oracle/oracle-r2dbc/pull/124) +are also a welcome addition. + +When a connection property is defined in `OracleR2dbcOptions`, it may be +configured as an R2DBC URL parameter. For example, the following URL configures +the `oracle.net.wallet_location` connection property: ``` r2dbcs:oracle://db.host.example.com:1522/db.service.name?oracle.net.wallet_location=/path/to/wallet/ ``` -The same property can also be configured programmatically: +And, the `OracleR2dbcOptions` constants can be used in programmatic +configuration: ```java ConnectionFactoryOptions.builder() .option(OracleR2dbcOptions.TLS_WALLET_LOCATION, "/path/to/wallet") ``` -The next sections list Oracle JDBC connection properties which are supported by -Oracle R2DBC. +All Oracle JDBC connection properties defined in `OracleR2dbcOptions` are listed +in the next sections. ##### TLS/SSL Connection Properties - [oracle.net.tns_admin](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_TNS_ADMIN) diff --git a/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryProviderImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryProviderImplTest.java index 01273fc..406d8f9 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryProviderImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryProviderImplTest.java @@ -33,7 +33,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.concurrent.TimeoutException; import java.util.function.Supplier; import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; @@ -56,7 +55,7 @@ /** * Verifies that * {@link OracleConnectionFactoryProviderImpl} implements behavior that - * is specified in it's class and method level javadocs. + * is specified in its class and method level javadocs. */ public class OracleConnectionFactoryProviderImplTest { @@ -216,7 +215,7 @@ public void testSupplierOptionNull() { Supplier portSupplier = DatabaseConfig::port; Supplier databaseSupplier = DatabaseConfig::serviceName; Supplier userSupplier = DatabaseConfig::user; - TestSupplier passwordSupplier = new TestSupplier(password()); + TestSupplier passwordSupplier = new TestSupplier<>(password()); ConnectionFactoryOptions connectionFactoryOptions = connectionFactoryOptions() @@ -326,7 +325,7 @@ public void testPublisherOptionNull() { Publisher portPublisher = Mono.fromSupplier(DatabaseConfig::port); Publisher databasePublisher = Mono.fromSupplier(DatabaseConfig::serviceName); Publisher userPublisher = Mono.fromSupplier(DatabaseConfig::user); - TestSupplier passwordPublisher = new TestSupplier(password()); + TestSupplier passwordPublisher = new TestSupplier<>(password()); ConnectionFactoryOptions connectionFactoryOptions = connectionFactoryOptions() diff --git a/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java index 66bd37f..735b3b8 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java @@ -640,10 +640,10 @@ public void testOracleR2dbcWarning() { Connection connection = awaitOne(sharedConnection()); try { - // Expect a warning for forcing a view that references a non-existent + // Expect a warning for invalid PL/SQL // table - String sql = "CREATE OR REPLACE FORCE VIEW testOracleR2dbcWarning AS" + - " SELECT x FROM thisdoesnotexist"; + String sql = "CREATE OR REPLACE PROCEDURE testOracleR2dbcWarning AS" + + " BEGIN this is not valid pl/sql; END;"; Statement warningStatement = connection.createStatement(sql); // Collect the segments @@ -684,7 +684,7 @@ public void testOracleR2dbcWarning() { } finally { tryAwaitExecution( - connection.createStatement("DROP VIEW testOracleR2dbcWarning")); + connection.createStatement("DROP PROCEDURE testOracleR2dbcWarning")); tryAwaitNone(connection.close()); } } @@ -697,11 +697,10 @@ public void testOracleR2dbcWarningIgnored() { Connection connection = awaitOne(sharedConnection()); try { - // Expect a warning for forcing a view that references a non-existent - // table + // Expect a warning for invalid PL/SQL String sql = - "CREATE OR REPLACE FORCE VIEW testOracleR2dbcWarningIgnored AS" + - " SELECT x FROM thisdoesnotexist"; + "CREATE OR REPLACE PROCEDURE testOracleR2dbcWarningIgnored AS" + + " BEGIN this is not valid pl/sql; END;"; Statement warningStatement = connection.createStatement(sql); // Verify that an update count of 0 is returned. @@ -719,7 +718,8 @@ public void testOracleR2dbcWarningIgnored() { } finally { tryAwaitExecution( - connection.createStatement("DROP VIEW testOracleR2dbcWarningIgnored")); + connection.createStatement( + "DROP PROCEDURE testOracleR2dbcWarningIgnored")); tryAwaitNone(connection.close()); } } @@ -732,11 +732,10 @@ public void testOracleR2dbcWarningIgnored() { public void testOracleR2dbcWarningNotIgnored() { Connection connection = awaitOne(sharedConnection()); try { - // Expect a warning for forcing a view that references a non-existent - // table + // Expect a warning for invalid PL/SQL String sql = - "CREATE OR REPLACE FORCE VIEW testOracleR2dbcWarningIgnored AS" + - " SELECT x FROM thisdoesnotexist"; + "CREATE OR REPLACE PROCEDURE testOracleR2dbcWarningIgnored AS" + + " BEGIN this is not valid pl/sql; END;"; Statement warningStatement = connection.createStatement(sql); AtomicInteger segmentIndex = new AtomicInteger(0); awaitError( @@ -754,7 +753,8 @@ public void testOracleR2dbcWarningNotIgnored() { } finally { tryAwaitExecution( - connection.createStatement("DROP VIEW testOracleR2dbcWarningIgnored")); + connection.createStatement( + "DROP PROCEDURE testOracleR2dbcWarningIgnored")); tryAwaitNone(connection.close()); } } diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index d0a2f1b..e88cf04 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -2476,6 +2476,11 @@ public boolean equals(Object other) { public String toString() { return id + ", " + Arrays.toString(value); } + + @Override + public int hashCode() { + return Objects.hash(id, Arrays.hashCode(value)); + } } TestRow row0 = new TestRow(0L, new int[]{1, 2, 3}); @@ -2684,6 +2689,11 @@ public boolean equals(Object other) { public String toString() { return id + ", " + Arrays.toString(value); } + + @Override + public int hashCode() { + return Objects.hash(id, Arrays.hashCode(value)); + } } OracleR2dbcTypes.ArrayType arrayType = @@ -3199,6 +3209,11 @@ public boolean equals(Object other) { && ((IdVector)other).id == id && Objects.equals(((IdVector)other).vector, vector); } + + @Override + public int hashCode() { + return Objects.hash(id, vector); + } } // Round 1: Use PL/SQL to return column values diff --git a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java index aa67679..56ae2cb 100644 --- a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java +++ b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java @@ -35,6 +35,7 @@ import java.sql.DriverManager; import java.sql.SQLException; import java.time.Duration; +import java.util.Optional; import java.util.Properties; /** @@ -178,9 +179,9 @@ public static Publisher sharedConnection() { * @return The major version number of the test database. */ public static int databaseVersion() { - try (var jdbcConnection = DriverManager.getConnection(String.format( - "jdbc:oracle:thin:@%s:%s/%s", host(), port(), serviceName()), - user(), password())) { + try ( + var jdbcConnection = + DriverManager.getConnection(jdbcUrl(), user(), password())) { return jdbcConnection.getMetaData().getDatabaseMajorVersion(); } catch (SQLException sqlException) { @@ -188,6 +189,17 @@ public static int databaseVersion() { } } + /** + * Returns an Oracle JDBC URL for opening connections to the test database. + * @return URL for the Oracle JDBC Driver. Not null. + */ + public static String jdbcUrl() { + return String.format( + "jdbc:oracle:thin:@%s%s:%d/%s", + protocol() == null ? "" : protocol() + ":", + host(), port(), serviceName()); + } + /** * Returns the major version number of the Oracle JDBC Driver installed as * a service provider for java.sql.Driver. diff --git a/src/test/java/oracle/r2dbc/test/OracleTestKit.java b/src/test/java/oracle/r2dbc/test/OracleTestKit.java index 90cabca..fe58473 100755 --- a/src/test/java/oracle/r2dbc/test/OracleTestKit.java +++ b/src/test/java/oracle/r2dbc/test/OracleTestKit.java @@ -57,6 +57,7 @@ import static io.r2dbc.spi.ConnectionFactoryOptions.USER; import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions; import static oracle.r2dbc.test.DatabaseConfig.host; +import static oracle.r2dbc.test.DatabaseConfig.jdbcUrl; import static oracle.r2dbc.test.DatabaseConfig.password; import static oracle.r2dbc.test.DatabaseConfig.port; import static oracle.r2dbc.test.DatabaseConfig.protocol; @@ -95,11 +96,7 @@ public class OracleTestKit implements TestKit { try { OracleDataSource dataSource = new oracle.jdbc.datasource.impl.OracleDataSource(); - dataSource.setURL(String.format("jdbc:oracle:thin:@%s%s:%d/%s", - Optional.ofNullable(protocol()) - .map(protocol -> protocol + ":") - .orElse(""), - host(), port(), serviceName())); + dataSource.setURL(jdbcUrl()); dataSource.setUser(user()); dataSource.setPassword(password()); this.jdbcOperations = new JdbcTemplate(dataSource); diff --git a/src/test/java/oracle/r2dbc/util/SharedConnectionFactory.java b/src/test/java/oracle/r2dbc/util/SharedConnectionFactory.java index 41fdd96..1fd8ddc 100644 --- a/src/test/java/oracle/r2dbc/util/SharedConnectionFactory.java +++ b/src/test/java/oracle/r2dbc/util/SharedConnectionFactory.java @@ -245,7 +245,7 @@ private static RuntimeException openCursorNotAccessible() { return new RuntimeException( "V$OPEN_CUROSR is not accessible to the test user. " + "Grant access as SYSDBA with: " + - "\"GRANT SELECT ON v_$open_cursor TO "+user()+"\", " + + "\"GRANT SELECT ON v$open_cursor TO "+user()+"\", " + "or disable open cursor checks with: " + " -Doracle.r2bdc.disableCursorCloseVerification=true"); }