Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

32 lob prefetch #36

Merged
merged 9 commits into from
Jul 20, 2021
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ Options. For Options having any of the following names, a CharSequence value may
- [oracle.net.ssl_context_protocol](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_SSL_CONTEXT_PROTOCOL)
- [oracle.jdbc.fanEnabled](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_FAN_ENABLED)
- [oracle.jdbc.implicitStatementCacheSize](https://docs.oracle.com/en/database/oracle/oracle-database/21/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/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE)
- Oracle Net Descriptors of the form ```(DESCRIPTION=...)``` may be specified as an io.r2dbc.spi.Option having the name `oracleNetDescriptor`.
- If `oracleNetDescriptor` is specified, then it is invalid to specify any other options that might conflict with information in the descriptor, such as: `HOST`, `PORT`, `DATABASE`, and `SSL`.
- The `oracleNetDescriptor` option may appear in the query section of an R2DBC URL: `r2dbc:oracle://?oracleNetDescriptor=(DESCRIPTION=...)`
Expand Down Expand Up @@ -269,15 +270,6 @@ values for a non-empty set of column names.
```Result``` for each returned cursor.

### Type Mappings
- Blob and Clob objects are the default mapping implemented by Row.get(...) for
BLOB and CLOB columns. ByteBuffer and String mappings are not supported for BLOB
and CLOB.
- Oracle Database allows BLOBs and CLOBs to store terabytes of data; This
amount would exceed the capacity of a ByteBuffer or String.
- Blob and Clob objects stream data over a series of ByteBuffers or Strings.
- Requiring content to be streamed over multiple buffers is necessary for Oracle
R2DBC to avoid a potentially memory exhausting implementation in which BLOBs and
CLOBs must be fully materialized as a return value for Row.get(...).
- javax.json.JsonObject and oracle.sql.json.OracleJsonObject are supported as
Java type mappings for JSON column values.
- java.time.Duration is supported as a Java type mapping for INTERVAL DAY TO SECOND
Expand All @@ -288,6 +280,27 @@ column values.
The Oracle Database type named "DATE" stores the same information as a LocalDateTime:
year, month, day, hour, minute, and second.

### BLOB, CLOB, and NCLOB
When a SQL query returns a LOB value, a
portion of that value is prefetched from the database and the remaining portion
must be fetched with additional database calls. The number of prefetched
bytes is configured by an ```Option``` named [oracle.jdbc.defaultLobPrefetchSize](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE)
. The default value of this ```Option``` is 1 GB.

The ```Row.get(...)``` method allows LOB values to be mapped into materialized
types like ```ByteBuffer``` and ```String```. If the prefetch size is large
enough to have fetched the entire LOB value, then ```Row.get(...)``` can
return a ```ByteBuffer/String``` without any additional database calls.
Otherwise, if the LOB value is larger than the prefetch size, then
```Row.get(...)``` must execute a **blocking database call** to fetch the
remainder of that value.

For systems in which LOB values are too large to be prefetched, a smaller
prefetch size can be configured, and LOB values may be mapped into ```Blob```
or ```Clob``` objects rather than ```ByteBuffer``` or ```String```. ```Blob```
and ```Clob``` objects allow the LOB value to be streamed using non-blocking
database calls.

# Secure Programming Guidelines
The following security guidelines should be followed when programming with the Oracle R2DBC Driver.
### Defend Against SQL Injection Attacks
Expand Down
27 changes: 0 additions & 27 deletions src/main/java/oracle/r2dbc/OracleR2dbcTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,24 +49,6 @@ private OracleR2dbcTypes() {}
public static final Type BINARY_FLOAT =
new TypeImpl(Float.class, "BINARY_FLOAT");

/**
* A Binary Large Object (BLOB) as implemented by Oracle Database. The default
* Java type mapping is {@link io.r2dbc.spi.Blob} rather than
* {@link java.nio.ByteBuffer}, which is the mapping of the standard
* {@link io.r2dbc.spi.R2dbcType#BLOB}.
*/
public static final Type BLOB =
new TypeImpl(io.r2dbc.spi.Blob.class, "BLOB");

/**
* A Character Large Object (BLOB) as implemented by Oracle Database. The
* default Java type mapping is {@link io.r2dbc.spi.Clob} rather than
* {@link String}, which is the mapping of the standard
* {@link io.r2dbc.spi.R2dbcType#CLOB}.
*/
public static final Type CLOB =
new TypeImpl(io.r2dbc.spi.Clob.class, "CLOB");

/**
* Stores a period of time in days, hours, minutes, and seconds.
*/
Expand Down Expand Up @@ -94,15 +76,6 @@ private OracleR2dbcTypes() {}
public static final Type LONG_RAW =
new TypeImpl(ByteBuffer.class, "LONG RAW");

/**
* A National Character Large Object (NCLOB) as implemented by Oracle
* Database. The default Java type mapping is {@link io.r2dbc.spi.Clob}
* rather than {@link String}, which is the mapping of the standard
* {@link io.r2dbc.spi.R2dbcType#NCLOB}.
*/
public static final Type NCLOB =
new TypeImpl(io.r2dbc.spi.Clob.class, "NCLOB");

/**
* Base 64 string representing the unique address of a row in its table.
*/
Expand Down
67 changes: 48 additions & 19 deletions src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,14 @@ final class OracleReactiveJdbcAdapter implements ReactiveJdbcAdapter {

// Support statement cache configuration
Option.valueOf(
OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE)
OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE),

// Support LOB prefetch size configuration. A large size is configured
// by default to support cases where memory is available to store entire
// LOB values. A non-default size may be configured when LOB values are
// too large to be prefetched and must be streamed from Blob/Clob objects.
Option.valueOf(
OracleConnection.CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE)

);

Expand Down Expand Up @@ -599,30 +606,52 @@ private static void configureJdbcDefaults(OracleDataSource oracleDataSource) {
runOrHandleSQLException(() ->
oracleDataSource.setConnectionProperty(enableJdbcSpecCompliance, "true"));

// Have the Oracle JDBC Driver cache PreparedStatements by default.
runOrHandleSQLException(() -> {
// Don't override a value set by user code
String userValue = oracleDataSource.getConnectionProperty(
OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE);

if (userValue == null) {
// The default value of the OPEN_CURSORS parameter in the 21c
// and 19c databases is 50:
// https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/OPEN_CURSORS.html#GUID-FAFD1247-06E5-4E64-917F-AEBD4703CF40
// Assuming this default, then a default cache size of 25 will keep
// each session at or below 50% of it's cursor capacity, which seems
// reasonable.
oracleDataSource.setConnectionProperty(
OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE,
"25");
}
});
// Cache PreparedStatements by default. The default value of the
// OPEN_CURSORS parameter in the 21c and 19c databases is 50:
// https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/OPEN_CURSORS.html#GUID-FAFD1247-06E5-4E64-917F-AEBD4703CF40
// Assuming this default, then a default cache size of 25 will keep
// each session at or below 50% of it's cursor capacity, which seems
// reasonable.
setPropertyIfAbsent(oracleDataSource,
OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE, "25");

// Prefetch LOB values by default. The database's maximum supported
// prefetch size, 1GB, is configured by default. This is done so that
// Row.get(...) can map LOB values into ByteBuffer/String without a
// blocking database call. If the entire value is prefetched, then JDBC
// won't need to fetch the remainder from the database when the entire is
// value requested as a ByteBuffer or String.
setPropertyIfAbsent(oracleDataSource,
OracleConnection.CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE,
"1048576");

// TODO: Disable the result set cache? This is needed to support the
// SERIALIZABLE isolation level, which requires result set caching to be
// disabled.
}

/**
* Sets a JDBC connection {@code property} to a provided {@code value} if an
* {@code oracleDataSource} has not already been configured with a
* {@code value} for that {@code property}. This method is used to set
* default values for properties that may otherwise be configured with user
* defined values.
* @param oracleDataSource DataSource to configure. Not null.
* @param property Name of property to set. Not null.
* @param value Value of {@code property} to set. Not null.
*/
private static void setPropertyIfAbsent(
OracleDataSource oracleDataSource, String property, String value) {

runOrHandleSQLException(() -> {
String userValue = oracleDataSource.getConnectionProperty(property);

// Don't override a value set by user code
if (userValue == null)
oracleDataSource.setConnectionProperty(property, value);
});
}

/**
* {@inheritDoc}
* <p>
Expand Down
108 changes: 0 additions & 108 deletions src/main/java/oracle/r2dbc/impl/OracleRowImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;

import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull;

Expand Down Expand Up @@ -85,20 +84,8 @@ final class OracleRowImpl implements Row {
* Implements the R2DBC SPI method by using the {@code JdbcRow} that backs
* this row to convert the specified column value into the Oracle R2DBC
* Driver's default Java type mapping for the column's SQL type.
* </p><p>
* This implementation does not support mapping the {@code BLOB} SQL type to
* {@code ByteBuffer}, nor does it support mapping the {@code CLOB} SQL
* type to {@code String}. This implementation will map {@code BLOB} to
* {@link Blob} and map {@code CLOB} to {@link Clob}.
* </p>
*
* @implNote Mapping {@code BLOB/CLOB} to {@code ByteBuffer/String} is not
* supported because Oracle Database allows LOBs to store terabytes of
* data. If the Oracle R2DBC Driver were to fully materialize a LOB
* prior to emitting this row, the amount of memory necessary to do so
* might exceed the capacity of {@code ByteBuffer/String}, and could even
* exceed the amount of memory available to the Java Virtual Machine.
*
* @throws IllegalArgumentException If the {@code index} is less than 0,
* or greater than the maximum column index.
*/
Expand All @@ -115,20 +102,7 @@ public Object get(int index) {
* Implements the R2DBC SPI method by using the JDBC ResultSet that backs
* this row to convert the specified column value into the specified {@code
* type}.
* </p><p>
* This implementation does not support mapping the {@code BLOB} SQL type to
* {@code ByteBuffer}, nor does it support mapping the {@code CLOB} SQL
* type to {@code String}. This implementation only supports mapping
* {@code BLOB} to {@link Blob} and {@code CLOB} to {@link Clob}.
* </p>
*
* @implNote Mapping {@code BLOB/CLOB} to {@code ByteBuffer/String} is not
* supported because Oracle Database allows LOBs to store terabytes of data.
* If the Oracle R2DBC Driver were to fully materialize a LOB
* prior to emitting this row, the amount of memory necessary to do so
* might exceed the capacity of {@code ByteBuffer/String}, and could even
* exceed the amount of memory available to the Java Virtual Machine.
*
* @throws IllegalArgumentException {@inheritDoc}
* @throws IllegalArgumentException If the {@code index} is less than 0,
* or greater than the maximum column index.
Expand All @@ -152,20 +126,7 @@ public <T> T get(int index, Class<T> type) {
* This method uses a case-insensitive column name match. If more than one
* column has a matching name, this method returns the value of the
* matching column with the lowest index.
* </p><p>
* This implementation does not support mapping the {@code BLOB} SQL type to
* {@code ByteBuffer}, nor does it support mapping the {@code CLOB} SQL
* type to {@code String}. This implementation will map {@code BLOB} to
* {@link Blob} and map {@code CLOB} to {@link Clob}.
* </p>
*
* @implNote Mapping {@code BLOB/CLOB} to {@code ByteBuffer/String} is not
* supported because Oracle Database allows LOBs to store terabytes of data.
* If the Oracle R2DBC Driver were to fully materialize a LOB
* prior to emitting this row, the amount of memory necessary to do so
* might exceed the capacity of {@code ByteBuffer/String}, and could even
* exceed the amount of memory available to the Java Virtual Machine.
*
* @throws IllegalArgumentException {@inheritDoc}
* @throws IllegalArgumentException If there is no column with a matching
* {@code name}.
Expand All @@ -188,20 +149,7 @@ public Object get(String name) {
* This method uses a case-insensitive column name match. If more than one
* column has a matching name, this method returns the value of the
* matching column with the lowest index.
* </p><p>
* This implementation does not support mapping the {@code BLOB} SQL type to
* {@code ByteBuffer}, nor does it support mapping the {@code CLOB} SQL
* type to {@code String}. This implementation only supports mapping
* {@code BLOB} to {@link Blob} and {@code CLOB} to {@link Clob}.
* </p>
*
* @implNote Mapping {@code BLOB/CLOB} to {@code ByteBuffer/String} is not
* supported because Oracle Database allows LOBs to store terabytes of data.
* If the Oracle R2DBC Driver were to fully materialize a LOB
* prior to emitting this row, the amount of memory necessary to do so
* might exceed the capacity of {@code ByteBuffer/String}, and could even
* exceed the amount of memory available to the Java Virtual Machine.
*
* @throws IllegalArgumentException {@inheritDoc}
* @throws IllegalArgumentException If conversion to the specified
* {@code type} is not supported.
Expand Down Expand Up @@ -252,9 +200,6 @@ private int getColumnIndex(String name) {
* @throws R2dbcException If the conversion is not supported.
*/
private <T> T convertColumnValue(int index, Class<T> type) {
requireSupportedTypeMapping(index, type);

//TODO Should support type.isAssignableFrom(...) here?
if (type.equals(ByteBuffer.class))
return type.cast(getByteBuffer(index));
else if (type.equals(io.r2dbc.spi.Blob.class))
Expand All @@ -275,10 +220,6 @@ else if (type.equals(LocalDateTime.class))
* A JDBC driver is not required to support {@code ByteBuffer} conversions
* for any SQL type, so this method is necessary to implement the
* conversion to {@code ByteBuffer} from a type that is supported by JDBC.
* </p><p>
* This method should NOT be called when the database column type is BLOB.
* The JDBC driver may require blocking network I/O in order to materialize a
* BLOB as a ByteBuffer.
* </p>
* @param index 0 based column index
* @return A column value as a {@code ByteBuffer}, or null if the column
Expand Down Expand Up @@ -394,37 +335,6 @@ else if (index >= rowMetadata.getColumnNames().size()) {
}
}

/**
* <p>
* Checks if the Oracle R2DBC Driver supports mapping the database type
* of the column at the specified {@code index} to the Java type specified
* as {@code type}.
* </p><p>
* This method handles cases where the JDBC driver may support a mapping
* that the Oracle R2DBC Driver does not support. For instance, the JDBC
* driver may support mapping CLOB columns to String, but the Oracle R2DBC
* Driver does not support this as the JDBC driver may require blocking
* network I/O to convert a CLOB into a String.
* </p>
*
* @param index 0-based column index
* @param type Class of the type that the column value is converted to
* @throws R2dbcException if the type mapping is not supported
*/
private void requireSupportedTypeMapping(int index, Class<?> type) {

switch (getColumnTypeNumber(index)) {
case Types.BLOB:
if (! type.equals(Blob.class))
throw unsupportedTypeMapping("BLOB", index, type);
break;
case Types.CLOB:
if (! type.equals(Clob.class))
throw unsupportedTypeMapping("CLOB", index, type);
break;
}
}

/**
* Returns the SQL type number of the column at a given {@code index}. The
* returned number identifies either a standard type defined by
Expand All @@ -442,22 +352,4 @@ private int getColumnTypeNumber(int index) {
return typeNumber == null ? Types.OTHER : typeNumber;
}

/**
* Returns an exception indicating that the Oracle R2DBC Driver does
* not support mapping the database type specified as {@code sqlTypeName}
* to the Java type specified as {@code type}.
* @param sqlTypeName Name of a SQL type, like "BLOB" or "NUMBER"
* @param index Column index having a SQL type named {@code sqlTypeName}
* @param type Java type to which mapping is not supported
* @return An exception that expresses the unsupported mapping
*/
private static R2dbcException unsupportedTypeMapping(
String sqlTypeName, int index, Class<?> type) {
return OracleR2dbcExceptions.newNonTransientException(
String.format("Unsupported SQL to Java type mapping. " +
"SQL Type: %s, Column Index: %d, Java Type: %s",
sqlTypeName, index, type.getName()),
null);
}

}