From cedc054a90378a7ca7fb8741eb9ac464c9fa2223 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 5 Nov 2021 17:16:18 -0700 Subject: [PATCH 1/9] Update pom. Throw IndexOutOfBoundsException --- pom.xml | 2 +- src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index d81d899..e52eb29 100755 --- a/pom.xml +++ b/pom.xml @@ -66,7 +66,7 @@ 11 21.3.0.0 - 0.9.0.M2 + 0.9.0.RC1 3.3.0.RELEASE 1.0.3 5.7.0 diff --git a/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java b/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java index 7fe09f6..cb9808f 100755 --- a/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java +++ b/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java @@ -131,10 +131,10 @@ static OutParametersMetadataImpl createOutParametersMetadata( */ protected final T get(int index) { if (index < 0) { - throw new ArrayIndexOutOfBoundsException("Negative index: " + index); + throw new IndexOutOfBoundsException("Negative index: " + index); } else if (index >= metadataList.size()) { - throw new ArrayIndexOutOfBoundsException( + throw new IndexOutOfBoundsException( "Index " + index + " exceeds the maximum index: " + metadataList.size()); } From 5327d4681d68581c42996f8d708a2c352a9ce639 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Wed, 10 Nov 2021 15:52:13 -0800 Subject: [PATCH 2/9] Support R2dbcException.getSql() --- .../r2dbc/impl/OracleR2dbcExceptions.java | 71 +++++++++++++------ .../r2dbc/impl/OracleReactiveJdbcAdapter.java | 11 ++- .../oracle/r2dbc/impl/OracleResultImpl.java | 9 +++ .../r2dbc/impl/ReactiveJdbcAdapter.java | 4 +- .../oracle/r2dbc/impl/ReadablesMetadata.java | 2 +- .../r2dbc/impl/OracleR2dbcExceptionsTest.java | 3 +- 6 files changed, 67 insertions(+), 33 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java b/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java index 0819801..7afc72a 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java +++ b/src/main/java/oracle/r2dbc/impl/OracleR2dbcExceptions.java @@ -30,6 +30,7 @@ import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.R2dbcTransientException; import io.r2dbc.spi.R2dbcTransientResourceException; +import oracle.jdbc.OracleDatabaseException; import java.sql.SQLException; import java.sql.SQLIntegrityConstraintViolationException; @@ -166,41 +167,42 @@ static R2dbcException toR2dbcException(SQLException sqlException) { final String message = sqlException.getMessage(); final String sqlState = sqlException.getSQLState(); final int errorCode = sqlException.getErrorCode(); + final String sql = getSql(sqlException); if (sqlException instanceof SQLNonTransientException) { if (sqlException instanceof SQLSyntaxErrorException) { return new R2dbcBadGrammarException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } else if (sqlException instanceof SQLIntegrityConstraintViolationException) { return new R2dbcDataIntegrityViolationException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } else if (sqlException instanceof SQLNonTransientConnectionException) { return new R2dbcNonTransientResourceException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } else { return new OracleR2dbcNonTransientException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } } else if (sqlException instanceof SQLTransientException) { if (sqlException instanceof SQLTimeoutException) { return new R2dbcTimeoutException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } else if (sqlException instanceof SQLTransactionRollbackException) { return new R2dbcRollbackException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } else if (sqlException instanceof SQLTransientConnectionException) { return new R2dbcTransientResourceException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } else { return new OracleR2dbcTransientException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } } else if (sqlException instanceof SQLRecoverableException) { @@ -209,11 +211,11 @@ else if (sqlException instanceof SQLRecoverableException) { // the connection is no longer valid. The R2dbcTransientResourceException // expresses the same conditions. return new R2dbcTransientResourceException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } else { return new OracleR2dbcException( - message, sqlState, errorCode, sqlException); + message, sqlState, errorCode, sql, sqlException); } } @@ -293,8 +295,30 @@ static T fromJdbc(ThrowingSupplier supplier) * @return A new non-transient exception. */ static R2dbcNonTransientException newNonTransientException( - String message, Throwable cause) { - return new OracleR2dbcNonTransientException(message, null, 0, cause); + String message, String sql, Throwable cause) { + return new OracleR2dbcNonTransientException(message, null, 0, sql, cause); + } + + /** + * Returns the SQL command that caused a {@code sqlException}, if it is + * available. This method is only implemented to support the case where the + * exception is caused by a {@link oracle.jdbc.OracleDatabaseException}. + * @param sqlException Exception to extract SQL from. Not null. + * @return The SQL that caused the {@code sqlException}, of {@code null} if + * the SQL is not available. + */ + private static String getSql(SQLException sqlException) { + Throwable cause = sqlException.getCause(); + + while (cause != null) { + + if (cause instanceof OracleDatabaseException) + return ((OracleDatabaseException)cause).getSql(); + + cause = cause.getCause(); + } + + return null; } /** @@ -368,17 +392,17 @@ default T get() throws R2dbcException { * concrete subclass must be defined. This subclass does not implement any * behavior that is specific to the Oracle driver. *

- * This subclass is defined so that {@link #toR2dbcException(SQLException)} - * can throw an instance of {@code R2dbcException} when mapping a - * {@link SQLException}. + * This subclass is defined so that + * {@link #toR2dbcException(SQLException)} can throw an instance of + * {@code R2dbcException} when mapping a {@link SQLException}. *

*/ private static final class OracleR2dbcException extends R2dbcException { private OracleR2dbcException( - String message, String sqlState, int errorCode, + String message, String sqlState, int errorCode, String sql, SQLException sqlException) { - super(message, sqlState, errorCode, sqlException); + super(message, sqlState, errorCode, sql, sqlException); } } @@ -390,17 +414,18 @@ private OracleR2dbcException( * This subclass does implement any behavior that is specific to the * Oracle driver. *

- * This subclass is defined so that {@link #toR2dbcException(SQLException)} - * can throw an instance of {@code R2dbcTransientException} when mapping a + * This subclass is defined so that + * {@link #toR2dbcException(SQLException)} can throw an instance of + * {@code R2dbcTransientException} when mapping a * {@link SQLTransientException}. *

*/ private static final class OracleR2dbcTransientException extends R2dbcTransientException { private OracleR2dbcTransientException( - String message, String sqlState, int errorCode, + String message, String sqlState, int errorCode, String sql, SQLException sqlException) { - super(message, sqlState, errorCode, sqlException); + super(message, sqlState, errorCode, sql, sqlException); } } @@ -420,9 +445,9 @@ private OracleR2dbcTransientException( private static final class OracleR2dbcNonTransientException extends R2dbcNonTransientException { private OracleR2dbcNonTransientException( - String message, String sqlState, int errorCode, + String message, String sqlState, int errorCode, String sql, Throwable cause) { - super(message, sqlState, errorCode, cause); + super(message, sqlState, errorCode, sql, cause); } } diff --git a/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java b/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java index 36ac0f7..d0f299b 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java @@ -194,7 +194,7 @@ final class OracleReactiveJdbcAdapter implements ReactiveJdbcAdapter { // may need to be disabled when connecting to an 18.x database. Starting // in 19.x, the database can detect when it's running on a system where // OOB is not supported and automatically disable OOB. This automated - // detection is not impleneted in 18.x. + // detection is not implemented in 18.x. OracleR2dbcOptions.DISABLE_OUT_OF_BAND_BREAK, // Allow the client-side ResultSet cache to be disabled. It is @@ -247,7 +247,7 @@ static OracleReactiveJdbcAdapter getInstance() { * When the "descriptor" option is provided, it is invalid to specify any * other options that might conflict with values also specified in the * descriptor. For instance, the descriptor element of - * {@code (ADDRESSS=(HOST=...)(PORT=...)(PROTOCOL=...))} specifies values + * {@code (ADDRESS=(HOST=...)(PORT=...)(PROTOCOL=...))} specifies values * that overlap with the standard {@code Option}s of {@code HOST}, {@code * PORT}, and {@code SSL}. An {@code IllegalArgumentException} is thrown * when the descriptor is provided with any overlapping {@code Option}s. @@ -1136,7 +1136,7 @@ private T castAsType(Object object, Class type) { } else { throw OracleR2dbcExceptions.newNonTransientException( - object.getClass() + " is not an instance of " + type, null); + object.getClass() + " is not an instance of " + type, null, null); } } @@ -1468,9 +1468,8 @@ private static final class AsyncLock { new ConcurrentLinkedDeque<>(); /** - * Returns a {@code Publisher} that emits {@code onComplete} when - * this lock is acquired. - * @return + * Executes a {@code callback} with exclusive access to the guarded + * resource. */ void lock(Runnable callback) { assert waitCount.get() >= 0 : "Wait count is less than 0: " + waitCount; diff --git a/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java b/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java index 5f58c3a..5c8af89 100644 --- a/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleResultImpl.java @@ -586,9 +586,18 @@ Publisher publishSegments(Function mappingFunction) { */ private static final class WarningResult extends OracleResultImpl { + /** The warning of this result */ private final SQLWarning warning; + + /** The result that follows this result */ private final OracleResultImpl result; + /** + * Constructs a result that publishes a {@code warning} as a + * {@link Message}, and then publishes the segments of a {@code result}. + * @param warning Warning to publish + * @param result Result of segments to publish after the warning + */ private WarningResult(SQLWarning warning, OracleResultImpl result) { this.warning = warning; this.result = result; diff --git a/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java b/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java index 3bef388..1bb9e78 100755 --- a/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java +++ b/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java @@ -95,7 +95,7 @@ static ReactiveJdbcAdapter getOracleAdapter() throws R2dbcException { } catch (SQLException getDriverException) { throw OracleR2dbcExceptions.newNonTransientException( - "Failed to locate the Oracle JDBC Driver", getDriverException); + "Failed to locate the Oracle JDBC Driver", null, getDriverException); } return OracleReactiveJdbcAdapter.getInstance(); @@ -107,7 +107,7 @@ static ReactiveJdbcAdapter getOracleAdapter() throws R2dbcException { * the {@code options} parameter. Adapters implementing this method return a * {@code DataSource} that is supported as an argument to their * implementation of - * {@link ReactiveJdbcAdapter#publishConnection(DataSource)}. + * {@link ReactiveJdbcAdapter#publishConnection(DataSource, Executor)}. *

* Adapters implementing this method must specify each supported value * of {@link ConnectionFactoryOptions}, how the value of each option is diff --git a/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java b/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java index cb9808f..eeb3192 100755 --- a/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java +++ b/src/main/java/oracle/r2dbc/impl/ReadablesMetadata.java @@ -123,7 +123,7 @@ static OutParametersMetadataImpl createOutParametersMetadata( * @param index the value index starting at 0 * @return the {@link ReadableMetadata} for one value in this result. Not * null. - * @throws ArrayIndexOutOfBoundsException if the {@code index} is less than + * @throws IndexOutOfBoundsException if the {@code index} is less than * zero or greater than the number of available values. * @implSpec This method implements common behavior specified for both * {@link RowMetadata#getColumnMetadata(int)} and diff --git a/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java b/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java index ce22af1..b0ae67f 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java @@ -145,7 +145,8 @@ public void testNewNonTransientException() { "SQL-MESSAGE", "SQL-STATE", 9, ioException); String message = "MESSAGE"; R2dbcNonTransientException r2dbcException = - OracleR2dbcExceptions.newNonTransientException(message, sqlException); + OracleR2dbcExceptions.newNonTransientException( + message, null, sqlException); // Expect the R2dbcException to have the same message assertSame(message, r2dbcException.getMessage()); From ea93daf00515265397d8cf5ffde5fda5fc127187 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Wed, 10 Nov 2021 16:05:08 -0800 Subject: [PATCH 3/9] Add test for R2dbcException.getSql() --- .../r2dbc/impl/OracleR2dbcExceptionsTest.java | 2 +- .../r2dbc/impl/OracleStatementImplTest.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java b/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java index b0ae67f..178dbc9 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java @@ -136,7 +136,7 @@ public void testRunOrHandleSqlException() { /** * Verifies the implementation of - * {@link OracleR2dbcExceptions#newNonTransientException(String, Throwable)} + * {@link OracleR2dbcExceptions#newNonTransientException(String, String, Throwable)} */ @Test public void testNewNonTransientException() { diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index 06d341a..b6794ef 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -2211,6 +2211,25 @@ public void testUsingWhenCancel() { } } + /** + * Verifies that {@link R2dbcException#getSql()} returns the SQL command + * that caused an exception. + */ + @Test + public void testGetSql() { + Connection connection = awaitOne(sharedConnection()); + try { + String badSql = "SELECT 0 FROM dooool"; + Result result = awaitOne(connection.createStatement(badSql).execute()); + R2dbcException r2dbcException = assertThrows(R2dbcException.class, () -> + awaitOne(result.getRowsUpdated())); + assertEquals(badSql, r2dbcException.getSql()); + } + finally { + tryAwaitNone(connection.close()); + } + } + // TODO: Repalce with Parameters.inOut when that's available private static final class InOutParameter implements Parameter, Parameter.In, Parameter.Out { From bfca03898d37c6d378e2edf9ebf7cb414815238c Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Wed, 10 Nov 2021 18:03:30 -0800 Subject: [PATCH 4/9] Fix metadata for JSON columns --- src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java | 8 +++++++- src/main/java/oracle/r2dbc/impl/SqlTypeMap.java | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java index 88c8d2e..ae4b048 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java @@ -206,7 +206,13 @@ else if (LocalDateTime.class.equals(type)) { value = getLocalDateTime(index); } else if (Object.class.equals(type)) { - value = convert(index, readablesMetadata.get(index).getJavaType()); + // Use the default type mapping if Object.class has been specified. + // This method is invoked recursively with the default mapping, so long + // as Object.class is not also the default mapping. + Class defaultType = readablesMetadata.get(index).getJavaType(); + value = Object.class.equals(defaultType) + ? jdbcReadable.getObject(index, Object.class) + : convert(index, defaultType); } else { value = jdbcReadable.getObject(index, type); diff --git a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java index 5d8f862..b0ecd74 100644 --- a/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java +++ b/src/main/java/oracle/r2dbc/impl/SqlTypeMap.java @@ -55,6 +55,7 @@ final class SqlTypeMap { */ private static final Map JDBC_TO_R2DBC_TYPE_MAP = Map.ofEntries( + entry(JDBCType.ARRAY, R2dbcType.COLLECTION), entry(JDBCType.BIGINT, R2dbcType.BIGINT), entry(JDBCType.BINARY, R2dbcType.BINARY), entry(OracleType.BINARY_DOUBLE, OracleR2dbcTypes.BINARY_DOUBLE), @@ -63,7 +64,6 @@ final class SqlTypeMap { entry(JDBCType.BOOLEAN, R2dbcType.BOOLEAN), entry(JDBCType.CHAR, R2dbcType.CHAR), entry(JDBCType.CLOB, R2dbcType.CLOB), - entry(JDBCType.ARRAY, R2dbcType.COLLECTION), entry(JDBCType.DATE, R2dbcType.DATE), entry(JDBCType.DECIMAL, R2dbcType.DECIMAL), entry(JDBCType.DOUBLE, R2dbcType.DOUBLE), @@ -75,6 +75,7 @@ final class SqlTypeMap { entry( OracleType.INTERVAL_YEAR_TO_MONTH, OracleR2dbcTypes.INTERVAL_YEAR_TO_MONTH), + entry(OracleType.JSON, OracleR2dbcTypes.JSON), entry(JDBCType.LONGVARBINARY, OracleR2dbcTypes.LONG_RAW), entry(JDBCType.LONGVARCHAR, OracleR2dbcTypes.LONG), entry(JDBCType.NCHAR, R2dbcType.NCHAR), From 1e90b9df50c5c4cf19e34e7f4c5da76fbc4ca22e Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Wed, 10 Nov 2021 18:57:19 -0800 Subject: [PATCH 5/9] Update exception types. --- .../oracle/r2dbc/impl/OracleReadableImpl.java | 13 +++--- .../impl/OracleReadableMetadataImpl.java | 8 ++-- .../r2dbc/impl/OracleStatementImpl.java | 5 ++- .../r2dbc/impl/OracleReadableImplTest.java | 45 ++++++++++--------- .../r2dbc/impl/OracleRowMetadataImplTest.java | 4 +- .../r2dbc/impl/OracleStatementImplTest.java | 26 +++++------ 6 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java index ae4b048..3288860 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableImpl.java @@ -39,6 +39,7 @@ import java.nio.ByteBuffer; import java.sql.Timestamp; import java.time.LocalDateTime; +import java.util.NoSuchElementException; import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; @@ -121,8 +122,6 @@ static OutParameters createOutParameters( * into the specified {@code type}. *

* @throws IllegalArgumentException {@inheritDoc} - * @throws IllegalArgumentException If the {@code index} is less than 0, - * or greater than the maximum value index. * @throws IllegalArgumentException If conversion to the specified * {@code type} is not supported. */ @@ -163,14 +162,14 @@ public T get(String name, Class type) { * matching values. * @param name The name of a value * @return The index of the named value within this {@code Readable} - * @throws IllegalArgumentException If no column has a matching name. + * @throws NoSuchElementException If no column has a matching name. */ private int indexOf(String name) { int columnIndex = readablesMetadata.getColumnIndex(name); if (columnIndex != -1) return columnIndex; else - throw new IllegalArgumentException("Unrecognized name: " + name); + throw new NoSuchElementException("Unrecognized name: " + name); } /** @@ -329,14 +328,14 @@ private LocalDateTime getLocalDateTime(int index) { * for this row. This method is used to verify index value parameters * supplied by user code. * @param index 0-based column index - * @throws IllegalStateException if the index is not valid. + * @throws IndexOutOfBoundsException if the index is not valid. */ private void requireValidIndex(int index) { if (index < 0) { - throw new IllegalArgumentException("Index is less than zero: " + index); + throw new IndexOutOfBoundsException("Index is less than zero: " + index); } else if (index >= readablesMetadata.getList().size()) { - throw new IllegalArgumentException( + throw new IndexOutOfBoundsException( "Index " + index + " is greater than or equal to column count: " + readablesMetadata.getList().size()); } diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java index da3c942..5f2d493 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java @@ -320,7 +320,7 @@ else if (type == R2dbcType.TIMESTAMP else if (type == R2dbcType.TIMESTAMP_WITH_TIME_ZONE) { // For the TIMESTAMP WITH TIMEZONE types, use the length of // OffsetDateTime.toString() as the precision. Use the scale from JDBC, - // even if it's 0 because a TIMESTAMP may 0 decimal digits. + // even if it's 0 because a TIMESTAMP may have 0 decimal digits. return new OracleColumnMetadataImpl(type, name, nullability, OFFSET_DATE_TIME_PRECISION, fromJdbc(() -> resultSetMetaData.getScale(jdbcIndex))); @@ -352,10 +352,10 @@ else if (type == R2dbcType.VARBINARY) { return new OracleColumnMetadataImpl( type, name, nullability, - // The getPrecision and getScale methods return 0 for types where + // The getPrecision and getScale methods return 0 or -1 for types where // precision and scale are not applicable. - precision == 0 ? null : precision, - scale == 0 ? null : scale); + precision < 1 ? null : precision, + scale < 1 ? null : scale); } } diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 74e2cd4..9d9bd00 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -45,6 +45,7 @@ import java.util.Arrays; import java.util.LinkedList; import java.util.List; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; @@ -533,7 +534,7 @@ else if (generatedColumns != null) * {@code name}. The match is case-sensitive. * @param name A parameter name. Not null. * @param value A value to bind. May be null. - * @throws IllegalArgumentException if no named parameter matches the + * @throws NoSuchElementException if no named parameter matches the * {@code identifier} */ private void bindNamedParameter(String name, Object value) { @@ -547,7 +548,7 @@ private void bindNamedParameter(String name, Object value) { } if (! isMatched) { - throw new IllegalArgumentException( + throw new NoSuchElementException( "Unrecognized parameter identifier: " + name); } } diff --git a/src/test/java/oracle/r2dbc/impl/OracleReadableImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleReadableImplTest.java index 49f9b44..f8b06fc 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleReadableImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleReadableImplTest.java @@ -26,6 +26,7 @@ import reactor.core.publisher.Mono; import java.math.BigDecimal; +import java.util.NoSuchElementException; import static java.util.Arrays.asList; import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; @@ -61,17 +62,17 @@ public void testGetByIndex() { awaitUpdate(1, connection.createStatement( "INSERT INTO testGetByIndex (x,y) VALUES (0,1)")); - // Expect IllegalArgumentException for an index less than 0 - awaitError(IllegalArgumentException.class, + // Expect IndexOutOfBoundsException for an index less than 0 + awaitError(IndexOutOfBoundsException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByIndex") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get(-1)))); - // Expect IllegalArgumentException for an index greater than or equal + // Expect IndexOutOfBoundsException for an index greater than or equal // to the number of columns - awaitError(IllegalArgumentException.class, + awaitError(IndexOutOfBoundsException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByIndex") .execute()) @@ -137,38 +138,38 @@ public void testGetByName() { .concatMap(result -> result.map((row, metadata) -> row.get(null)))); - // Expect IllegalArgumentException for unmatched names - awaitError(IllegalArgumentException.class, + // Expect NoSuchElementException for unmatched names + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByName") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get("z")))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByName") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get("xx")))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByName") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get("x ")))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByName") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get(" x")))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByName") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get(" ")))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByName") .execute()) @@ -303,17 +304,17 @@ class Unsupported {} result.map((row, metadata) -> row.get(0, Unsupported.class)))); - // Expect IllegalArgumentException for an index less than 0 - awaitError(IllegalArgumentException.class, + // Expect IndexOutOfBoundsException for an index less than 0 + awaitError(IndexOutOfBoundsException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByIndexAndType") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get(-1, Integer.class)))); - // Expect IllegalArgumentException for an index greater than or equal + // Expect IndexOutOfBoundsException for an index greater than or equal // to the number of columns - awaitError(IllegalArgumentException.class, + awaitError(IndexOutOfBoundsException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByIndexAndType") .execute()) @@ -400,38 +401,38 @@ class Unsupported {} .concatMap(result -> result.map((row, metadata) -> row.get(null, Integer.class)))); - // Expect IllegalArgumentException for unmatched names - awaitError(IllegalArgumentException.class, + // Expect NoSuchElementException for unmatched names + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByNameAndType") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get("z", Integer.class)))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByNameAndType") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get("xx", Integer.class)))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByNameAndType") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get("x ", Integer.class)))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByNameAndType") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get(" x", Integer.class)))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByNameAndType") .execute()) .concatMap(result -> result.map((row, metadata) -> row.get(" ", Integer.class)))); - awaitError(IllegalArgumentException.class, + awaitError(NoSuchElementException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetByNameAndType") .execute()) diff --git a/src/test/java/oracle/r2dbc/impl/OracleRowMetadataImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleRowMetadataImplTest.java index 33c2beb..06211a4 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleRowMetadataImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleRowMetadataImplTest.java @@ -80,7 +80,7 @@ public void testGetColumnMetadataByIndex() { "INSERT INTO testGetColumnMetadataByIndex (x,y) VALUES (0,0)")); // Expect IllegalArgumentException for an index less than 0 - awaitError(ArrayIndexOutOfBoundsException.class, + awaitError(IndexOutOfBoundsException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetColumnMetadataByIndex") .execute()) @@ -90,7 +90,7 @@ public void testGetColumnMetadataByIndex() { // Expect IllegalArgumentException for an index greater than or equal // to the number of columns - awaitError(ArrayIndexOutOfBoundsException.class, + awaitError(IndexOutOfBoundsException.class, Flux.from(connection.createStatement( "SELECT x, y FROM testGetColumnMetadataByIndex") .execute()) diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index b6794ef..e1a7160 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -36,11 +36,11 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.math.BigDecimal; import java.sql.RowId; import java.sql.SQLWarning; import java.util.Collections; import java.util.List; +import java.util.NoSuchElementException; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @@ -264,21 +264,21 @@ class UnsupportedType { IllegalArgumentException.class, () -> statement.bind("x", new UnsupportedType())); - // Expect IllegalArgumentException for an unmatched identifier + // Expect NoSuchElementException for an unmatched identifier assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> statement.bind("z", 1)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> statement.bind("xx", 1)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> statement.bind("", 1)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> statement.bind("X", 1)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> connection.createStatement("SELECT x FROM testBindByIndex") .bind("x", 0)); @@ -544,21 +544,21 @@ public void testBindNullByName() { IllegalArgumentException.class, () -> selectStatement.bindNull(null, Integer.class)); - // Expect IllegalArgumentException for an unmatched identifier + // Expect NoSuchElementException for an unmatched identifier assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> selectStatement.bindNull("z", Integer.class)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> selectStatement.bindNull("xx", Integer.class)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> selectStatement.bindNull("", Integer.class)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> selectStatement.bindNull("X", Integer.class)); assertThrows( - IllegalArgumentException.class, + NoSuchElementException.class, () -> connection.createStatement("SELECT x FROM testBindByIndex") .bind("x", 0)); From 546a35ba486350e0ca2efae27a8a98878e6a5979 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Thu, 11 Nov 2021 13:23:37 -0800 Subject: [PATCH 6/9] Remove support for trailing add()s --- .../r2dbc/impl/OracleStatementImpl.java | 16 +++++++------- .../r2dbc/impl/OracleConnectionImplTest.java | 6 +++-- .../r2dbc/impl/OracleResultImplTest.java | 2 +- .../r2dbc/impl/OracleStatementImplTest.java | 22 ++++++++++--------- .../oracle/r2dbc/impl/TypeMappingTest.java | 2 +- .../java/oracle/r2dbc/test/OracleTestKit.java | 14 ++++++++++-- 6 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 9d9bd00..0555090 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -1092,10 +1092,9 @@ private Publisher executeGeneratingValues() { /** *

* Executes this {@code Statement} as a batch DML command. The returned - * {@code Publisher} emits 1 {@code Result} for each set of bind values in - * this {@code Statement}'s {@link #batch}. Each {@code Result} has an - * update count and no row data. Update counts are floored to a maximum of - * {@link Integer#MAX_VALUE}. + * {@code Publisher} emits 1 {@code Result} having a + * {@link io.r2dbc.spi.Result.UpdateCount} segment for each set of bind + * values in the {@link #batch}. *

* This method copies any mutable state of this {@code Statement} needed to * execute the batch; Any mutations that occur after this method returns will @@ -1108,14 +1107,14 @@ private Publisher executeGeneratingValues() { * subscriber subscribes, before the subscriber emits a {@code request} * signal. *

- * @return {@code Publisher} that the {@code Result}s of executing this + * @return {@code Publisher} that emits the {@code Result}s of executing this * {@code Statement} as a batch DML command. * @throws IllegalStateException If this {@code Statement} has been * configured to return generated values with * {@link #returnGeneratedValues(String...)}. Oracle JDBC does not support * batch execution that returns generated keys. - * @throws IllegalStateException If at least one parameter has been set - * since the last call to {@link #add()}, but not all parameters have been set + * @throws IllegalStateException If not all parameters have been set since the + * last call to {@link #add()} * @throws IllegalStateException If all parameters have been set since the * last call to {@link #add()}, and an out parameter is present. JDBC does * not support batch execution with out parameters. @@ -1127,7 +1126,8 @@ Publisher executeBatch() { "Batch execution with generated values is not supported"); } - addImplicit(); + + add(); // TODO: Catch and emit IllegalStateException as R2dbcException? Queue currentBatch = batch; int batchSize = batch.size(); batch = new LinkedList<>(); diff --git a/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java index 134d1a0..6949992 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java @@ -843,7 +843,9 @@ public void testRollbackTransaction() { "Hola, Oracle", "Namaste, Oracle", "Ni hao, Oracle"); - values.forEach(value -> insert.bind("value", value).add()); + values.subList(0, values.size() - 1) + .forEach(value -> insert.bind("value", value).add()); + insert.bind("value", values.get(values.size() - 1)); awaitUpdate( values.stream().map(value -> 1).collect(Collectors.toList()), insert); @@ -982,7 +984,7 @@ public void testSetAutoCommit() { List.of(1, 1), insertInSessionA .bind(0, "D").add() - .bind(0, "D").add()); + .bind(0, "D")); awaitNone(enableAutoCommitPublisher); assertFalse(sessionA.isAutoCommit(), "Unexpected value returned by isAutoCommit() after subscribing to" diff --git a/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java index 13428b9..7b8b7db 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java @@ -407,7 +407,7 @@ public void testBatchUpdateError() { awaitNone(Mono.from(connection.createStatement( "INSERT INTO testBatchUpdateError VALUES (?)") .bind(0, 0).add() - .bind(0, 0).add() + .bind(0, 0) .execute()) .flatMapMany(result -> result.flatMap(segment -> { diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index e1a7160..a919944 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -95,7 +95,7 @@ public void testBindByIndex() { .bind(0, 0).bind(1, 0).add() .bind(0, 1).bind(1, 0).add() .bind(0, 1).bind(1, 1).add() - .bind(0, 1).bind(1, 2).add()); + .bind(0, 1).bind(1, 2)); // Expect bind values to be applied in WHERE clause as: // SELECT x, y FROM testBindByIndex WHERE x = 1 AND y > 0 @@ -217,7 +217,7 @@ public void testBindByName() { .bind("X", 0).bind("Y", 0).add() .bind("X", 1).bind("Y", 0).add() .bind("X", 1).bind("Y", 1).add() - .bind("X", 1).bind("Y", 2).add()); + .bind("X", 1).bind("Y", 2)); // Expect bind values to be applied in WHERE clause as: // SELECT x, y FROM testBindByName WHERE x = 1 AND y > 0 @@ -430,7 +430,7 @@ public void testBindNullByIndex() { .bindNull(0, Integer.class).bind(1, 1).add() .bindNull(0, Integer.class).bind(1, 2).add() .bind(0, 0).bind(1, 3).add() - .bind(0, 0).bindNull(1, Integer.class).add()); + .bind(0, 0).bindNull(1, Integer.class)); awaitQuery( asList( asList(null, 0), @@ -574,7 +574,7 @@ public void testBindNullByName() { .bindNull("x", Integer.class).bind("y", 1).add() .bindNull("x", Integer.class).bind("y", 2).add() .bind("x", 0).bind("y", 3).add() - .bind("x", 0).bindNull("y", Integer.class).add()); + .bind("x", 0).bindNull("y", Integer.class)); awaitQuery( asList( asList(null, 0), @@ -725,7 +725,7 @@ public void testAdd() { awaitUpdate( asList(1, 1, 1), connection.createStatement("INSERT INTO testAdd VALUES(0, 0)") - .add().add().add()); + .add().add()); awaitQuery( asList(asList(0, 0), asList(0, 0), asList(0, 0)), row -> asList(row.get(0, Integer.class), row.get(1, Integer.class)), @@ -737,7 +737,7 @@ public void testAdd() { connection.createStatement("INSERT INTO testAdd VALUES(:x, :y)") .bind("x", 1).bind("y", 1).add() .bind("x", 1).bind("y", 2).add() - .bind("x", 1).bind("y", 3).add()); + .bind("x", 1).bind("y", 3)); awaitQuery( asList(asList(1, 1), asList(1, 2), asList(1, 3)), row -> asList(row.get(0, Integer.class), row.get(1, Integer.class)), @@ -768,7 +768,7 @@ public void testAdd() { Mono.from(connection.createStatement("SELECT ? FROM dual") .bind(0, 1).add() .bind(0, 2).add() - .bind(0, 3).add() + .bind(0, 3) .execute()) .flatMapMany(Result::getRowsUpdated)); @@ -860,7 +860,7 @@ public void testExecute() { asList(1, 1), updateStatement .bind("oldValue", 1).bind("newValue", 2).add() - .bind("oldValue", 0).bind("newValue", 1).add()); + .bind("oldValue", 0).bind("newValue", 1)); // Expect bind values to be cleared after execute with explicit add() assertThrows(IllegalStateException.class, updateStatement::execute); @@ -1757,8 +1757,9 @@ public void testNoOutImplicitResult() { // Load [0,100] into the table Statement insert = connection.createStatement( "INSERT INTO testNoOutImplicitResult VALUES (?)"); - IntStream.rangeClosed(0, 100) + IntStream.range(0, 100) .forEach(i -> insert.bind(0, i).add()); + insert.bind(0, 100); awaitOne(101, Flux.from(insert.execute()) .flatMap(Result::getRowsUpdated) .reduce(0, (total, updateCount) -> total + updateCount)); @@ -1867,8 +1868,9 @@ public void testOutAndImplicitResult() { // Load [0,100] into the table Statement insert = connection.createStatement( "INSERT INTO testOutAndImplicitResult VALUES (?)"); - IntStream.rangeClosed(0, 100) + IntStream.range(0, 100) .forEach(i -> insert.bind(0, i).add()); + insert.bind(0, 100); awaitOne(101, Flux.from(insert.execute()) .flatMap(Result::getRowsUpdated) .reduce(0, (total, updateCount) -> total + updateCount)); diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index 6550e58..0524321 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -543,7 +543,7 @@ private static void verifyTypeMapping( awaitUpdate(asList(1,1), connection.createStatement( "INSERT INTO "+table+"(javaValue) VALUES(:javaValue)") .bind("javaValue", javaValue).add() - .bindNull("javaValue", javaValue.getClass()).add()); + .bindNull("javaValue", javaValue.getClass())); verifyEquals.accept(javaValue, awaitOne(Flux.from(connection.createStatement( diff --git a/src/test/java/oracle/r2dbc/test/OracleTestKit.java b/src/test/java/oracle/r2dbc/test/OracleTestKit.java index 028fda5..e79c98e 100755 --- a/src/test/java/oracle/r2dbc/test/OracleTestKit.java +++ b/src/test/java/oracle/r2dbc/test/OracleTestKit.java @@ -28,6 +28,7 @@ import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.R2dbcNonTransientException; import io.r2dbc.spi.Result; import io.r2dbc.spi.Row; import io.r2dbc.spi.Statement; @@ -269,7 +270,7 @@ public void duplicateColumnNames() { * segments from a single {@code Result}. The default implementation expects * 10 {@code Result}s each with a single {@code UpdateCount}. Batch DML * execution is a single call to Oracle Database, and so Oracle R2DBC - * returns a signle {@code Result} + * returns a single {@code Result} *

*/ @Override @@ -280,8 +281,17 @@ public void prepareStatement() { Statement statement = connection.createStatement(expand(TestStatement.INSERT_VALUE_PLACEHOLDER, getPlaceholder(0))); IntStream.range(0, 10) - .forEach(i -> TestKit.bind(statement, getIdentifier(0), i).add()); + .forEach(i -> { + TestKit.bind(statement, getIdentifier(0), i); + if (i != 9) { + statement.add(); + } + }); + + // The original TestKit implementation is modified below to call + // Result.getRowsUpdated(), which returns a Publisher of 10 + // UpdateCount segments. return Flux.from(statement .execute()) .flatMap(Result::getRowsUpdated); From f4cdca0c49683fed016c56d88096fd8c5d965e6e Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 12 Nov 2021 12:33:53 -0800 Subject: [PATCH 7/9] Emit IllegalStateException for missing parameters of batch --- .../r2dbc/impl/OracleStatementImpl.java | 17 ++++++++++++-- .../r2dbc/impl/OracleStatementImplTest.java | 23 ++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index 0555090..ab8c443 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -25,6 +25,7 @@ import io.r2dbc.spi.OutParameters; import io.r2dbc.spi.Parameter; import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientException; import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; import io.r2dbc.spi.Type; @@ -1126,8 +1127,19 @@ Publisher executeBatch() { "Batch execution with generated values is not supported"); } + // If parameters are not set, then capture the error and then emit it after + // the result of executing with all previously added binds + IllegalStateException missingParameters = null; + try { + add(); + } + catch (IllegalStateException illegalStateException) { + missingParameters = illegalStateException; + } + Mono missingParametersMono = missingParameters == null + ? Mono.empty() + : Mono.error(missingParameters); - add(); // TODO: Catch and emit IllegalStateException as R2dbcException? Queue currentBatch = batch; int batchSize = batch.size(); batch = new LinkedList<>(); @@ -1171,7 +1183,8 @@ Publisher executeBatch() { // Close the cursor before emitting the Result runJdbc(preparedStatement::close); return resultPublisher; - }); + }) + .concatWith(missingParametersMono); }); } diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index a919944..5b1d827 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -25,6 +25,7 @@ import io.r2dbc.spi.Parameter; import io.r2dbc.spi.Parameters; import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientException; import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Result; import io.r2dbc.spi.Result.Message; @@ -35,6 +36,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; import java.sql.RowId; import java.sql.SQLWarning; @@ -806,12 +808,21 @@ public void testAdd() { connection.createStatement("INSERT INTO table VALUES(:x, :y)") .bind("x", 0).bind("y", 1).add() .bind("x", 0).add()); - assertThrows( - IllegalStateException.class, - () -> - connection.createStatement("INSERT INTO table VALUES(:x, :y)") - .bind("x", 0).bind("y", 1).add() - .bind("y", 1).execute()); // implicit add() + + // Expect the statement to execute with previously added binds, and + // then emit an error if binds are missing in the final set of binds. + List> signals = + awaitOne(Flux.from(connection.createStatement( + "INSERT INTO testAdd VALUES (:x, :y)") + .bind("x", 0).bind("y", 1).add() + .bind("y", 1).execute()) + .flatMap(Result::getRowsUpdated) + .materialize() + .collectList()); + assertEquals(2, signals.size()); + assertEquals(1, signals.get(0).get()); + assertTrue( + signals.get(1).getThrowable() instanceof IllegalStateException); } finally { From 8c9c61c7a62a0901c545c474adea0bbce4237f0b Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Mon, 15 Nov 2021 12:07:16 -0800 Subject: [PATCH 8/9] Revert precision/scale == 0 check --- .../r2dbc/impl/OracleReadableMetadataImpl.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java index 5f2d493..5917e47 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java @@ -296,8 +296,10 @@ static ColumnMetadata createColumnMetadata( Nullability nullability = getNullability(fromJdbc(() -> resultSetMetaData.isNullable(jdbcIndex))); - if (type == R2dbcType.BLOB || type == R2dbcType.CLOB - || type == R2dbcType.NCLOB) { + if (type == R2dbcType.BLOB + || type == R2dbcType.CLOB + || type == R2dbcType.NCLOB + || type == OracleR2dbcTypes.JSON) { // For LOB types, use null as the precision. The actual maximum length // is (4GB x database-block-size), which can not be stored as an Integer return new OracleColumnMetadataImpl(type, name, nullability, null, null); @@ -352,10 +354,10 @@ else if (type == R2dbcType.VARBINARY) { return new OracleColumnMetadataImpl( type, name, nullability, - // The getPrecision and getScale methods return 0 or -1 for types where + // The getPrecision and getScale methods return 0 for types where // precision and scale are not applicable. - precision < 1 ? null : precision, - scale < 1 ? null : scale); + precision == 0 ? null : precision, + scale == 0 ? null : scale); } } From 89dfaefd21005b184e43af23135e8094bbbb63fc Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Tue, 16 Nov 2021 10:11:56 -0800 Subject: [PATCH 9/9] Allow getScale to return 0 for NUMBER --- .../java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java | 6 ++++++ .../oracle/r2dbc/impl/OracleReadableMetadataImplTest.java | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java index 5917e47..ab5c0a8 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReadableMetadataImpl.java @@ -296,6 +296,12 @@ static ColumnMetadata createColumnMetadata( Nullability nullability = getNullability(fromJdbc(() -> resultSetMetaData.isNullable(jdbcIndex))); + if (type == R2dbcType.NUMERIC) { + // For NUMBER, allow the scale to be 0 + return new OracleColumnMetadataImpl(type, name, nullability, + fromJdbc(() -> resultSetMetaData.getPrecision(jdbcIndex)), + fromJdbc(() -> resultSetMetaData.getScale(jdbcIndex))); + } if (type == R2dbcType.BLOB || type == R2dbcType.CLOB || type == R2dbcType.NCLOB diff --git a/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java index 8faddd0..55147ad 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleReadableMetadataImplTest.java @@ -158,6 +158,11 @@ public void testNumericTypes() { connection, "NUMBER(5,3)", JDBCType.NUMERIC, R2dbcType.NUMERIC, 5, 3, BigDecimal.class, BigDecimal.valueOf(12.345)); + // Expect getScale() to return 0 for NUMBER . + verifyColumnMetadata( + connection, "NUMBER(5,0)", JDBCType.NUMERIC, R2dbcType.NUMERIC, 5, 0, + BigDecimal.class, BigDecimal.valueOf(12345)); + // Expect FLOAT and Double to map. verifyColumnMetadata( connection, "FLOAT(6)", JDBCType.FLOAT, R2dbcType.FLOAT, 6, null,