Skip to content

Commit 6ebca64

Browse files
Merge branch 'main' of https://github.com/oracle/oracle-r2dbc into 40-ldap-support
2 parents e526745 + e8fc172 commit 6ebca64

File tree

7 files changed

+136
-72
lines changed

7 files changed

+136
-72
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,17 @@ supported by Oracle R2DBC:
306306
- Out of band (OOB) breaks effect statement timeouts. Set this to "true" if statement timeouts are not working correctly.
307307
- [oracle.jdbc.enableQueryResultCache](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_ENABLE_QUERY_RESULT_CACHE)
308308
- Cached query results can cause phantom reads even if the serializable
309-
transaction isolation level is set. Set this to "false" if using the
309+
transaction isolation level is set. Set this to "false" if using the
310310
serializable isolation level.
311311
- [v$session.terminal](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_TERMINAL)
312312
- [v$session.machine](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_MACHINE)
313313
- [v$session.osuser](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_OSUSER)
314314
- [v$session.program](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_PROGRAM)
315315
- [v$session.process](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_PROCESS)
316+
- (For inclusion in the next release) [oracle.jdbc.timeZoneAsRegion](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_TIMEZONE_AS_REGION)
317+
- Setting this option to "false" may resolve "ORA-01882: timezone region not
318+
found". The ORA-01882 error happens when Oracle Database doesn't recognize
319+
the name returned by `java.util.TimeZone.getDefault().getId()`.
316320

317321
### Thread Safety and Parallel Execution
318322
Oracle R2DBC's `ConnectionFactory` and `ConnectionFactoryProvider` are the only

src/main/java/oracle/r2dbc/OracleR2dbcOptions.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,13 @@ private OracleR2dbcOptions() {}
229229
* {@link OracleConnection#CONNECTION_PROPERTY_THIN_VSESSION_PROCESS}
230230
*/
231231
public static final Option<CharSequence> VSESSION_PROCESS;
232+
233+
/**
234+
* Configures the Oracle JDBC Connection used by Oracle R2DBC as specified by:
235+
* {@link OracleConnection#CONNECTION_PROPERTY_TIMEZONE_AS_REGION}
236+
*/
237+
public static final Option<CharSequence> TIMEZONE_AS_REGION;
238+
232239
/**
233240
* Configures the Oracle JDBC Connection used by Oracle R2DBC as specified by:
234241
* {@link OracleConnection#CONNECTION_PROPERTY_THIN_LDAP_SECURITY_AUTHENTICATION}
@@ -398,6 +405,8 @@ private OracleR2dbcOptions() {}
398405
OracleConnection.CONNECTION_PROPERTY_THIN_VSESSION_PROGRAM),
399406
VSESSION_PROCESS = Option.valueOf(
400407
OracleConnection.CONNECTION_PROPERTY_THIN_VSESSION_PROCESS),
408+
TIMEZONE_AS_REGION = Option.valueOf(
409+
OracleConnection.CONNECTION_PROPERTY_TIMEZONE_AS_REGION),
401410
LDAP_SECURITY_AUTHENTICATION = Option.valueOf(
402411
OracleConnection.CONNECTION_PROPERTY_THIN_LDAP_SECURITY_AUTHENTICATION),
403412
LDAP_SECURITY_PRINCIPAL = Option.valueOf(
@@ -446,4 +455,5 @@ private OracleR2dbcOptions() {}
446455
public static Set<Option<?>> options() {
447456
return OPTIONS;
448457
}
458+
449459
}

src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@
5353
import java.time.Duration;
5454
import java.util.Arrays;
5555
import java.util.Objects;
56-
import java.util.Set;
5756
import java.util.concurrent.CompletableFuture;
5857
import java.util.concurrent.Executor;
5958
import java.util.concurrent.Flow;
6059
import java.util.concurrent.atomic.AtomicBoolean;
6160
import java.util.function.Function;
6261
import java.util.function.Supplier;
62+
import java.util.stream.Stream;
6363

6464
import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT;
6565
import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
@@ -316,17 +316,12 @@ public AsyncLock getLock() {
316316
* </li><li>
317317
* {@linkplain OracleConnection#CONNECTION_PROPERTY_THIN_VSESSION_MACHINE
318318
* v$session.machine}
319+
* </li><li>
320+
* {@linkplain OracleConnection#CONNECTION_PROPERTY_TIMEZONE_AS_REGION
321+
* oracle.jdbc.timezoneAsRegion}
319322
* </li>
320323
* </ul>
321324
*
322-
* @implNote The returned {@code DataSource} is configured to create
323-
* connections that encode character bind values using the National
324-
* Character Set of an Oracle Database. In 21c, the National Character Set
325-
* must be either UTF-8 or UTF-16; This ensures that unicode bind data is
326-
* properly encoded by Oracle JDBC. If the data source is not configured
327-
* this way, the Oracle JDBC Driver uses the default character set of the
328-
* database, which may not support Unicode characters.
329-
*
330325
* @throws IllegalArgumentException If the {@code oracleNetDescriptor}
331326
* {@code Option} is provided with any other options that might have
332327
* conflicting values, such as {@link ConnectionFactoryOptions#HOST}.
@@ -391,7 +386,7 @@ private static String composeJdbcUrl(ConnectionFactoryOptions options) {
391386

392387
if (descriptor != null) {
393388
validateDescriptorOptions(options);
394-
return "jdbc:oracle:thin:@" + descriptor.toString();
389+
return "jdbc:oracle:thin:@" + descriptor;
395390
}
396391
else {
397392
Object protocol =
@@ -423,8 +418,7 @@ private static String composeJdbcUrl(ConnectionFactoryOptions options) {
423418
private static void validateDescriptorOptions(
424419
ConnectionFactoryOptions options) {
425420
Option<?>[] conflictingOptions =
426-
Set.of(HOST, PORT, DATABASE, SSL)
427-
.stream()
421+
Stream.of(HOST, PORT, DATABASE, SSL)
428422
.filter(options::hasOption)
429423
.filter(option ->
430424
// Ignore options having a value that can be represented as a

src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import java.sql.Connection;
4040
import java.sql.PreparedStatement;
4141
import java.sql.ResultSet;
42+
import java.sql.SQLException;
4243
import java.sql.SQLType;
4344
import java.sql.SQLWarning;
4445
import java.time.Duration;
@@ -55,6 +56,7 @@
5556
import java.util.function.Function;
5657
import java.util.stream.IntStream;
5758

59+
import static java.sql.Statement.CLOSE_ALL_RESULTS;
5860
import static java.sql.Statement.KEEP_CURRENT_RESULT;
5961
import static java.sql.Statement.RETURN_GENERATED_KEYS;
6062
import static java.util.Objects.requireNonNullElse;
@@ -1158,26 +1160,56 @@ private OracleResultImpl getWarnings(OracleResultImpl result) {
11581160
*/
11591161
private Publisher<Void> deallocate(Collection<OracleResultImpl> results) {
11601162

1161-
// Close the statement after all results are consumed
1163+
// Set up a counter that is decremented as each result is consumed.
11621164
AtomicInteger unconsumed = new AtomicInteger(results.size());
1165+
1166+
// Set up a publisher that decrements the counter, and closes the
1167+
// statement when it reaches zero
11631168
Publisher<Void> closeStatement = adapter.getLock().run(() -> {
11641169
if (unconsumed.decrementAndGet() == 0)
1165-
preparedStatement.close();
1170+
closeStatement();
11661171
});
11671172

1173+
// Tell each unconsumed result to decrement the unconsumed count, and then
1174+
// close the statement when the count reaches zero.
11681175
for (OracleResultImpl result : results) {
11691176
if (!result.onConsumed(closeStatement))
11701177
unconsumed.decrementAndGet();
11711178
}
11721179

1173-
// If all results have already been consumed, the returned
1174-
// publisher closes the statement
1180+
// If there are no results, or all results have already been consumed,
1181+
// then the returned publisher closes the statement.
11751182
if (unconsumed.get() == 0)
1176-
addDeallocation(adapter.getLock().run(preparedStatement::close));
1183+
addDeallocation(adapter.getLock().run(this::closeStatement));
11771184

11781185
return deallocators;
11791186
}
11801187

1188+
/**
1189+
* Closes the JDBC {@link #preparedStatement}. This method should only be
1190+
* called while holding the
1191+
* {@linkplain ReactiveJdbcAdapter#getLock() connection lock}
1192+
* @throws SQLException If the statement fails to close.
1193+
*/
1194+
private void closeStatement() throws SQLException {
1195+
try {
1196+
// Workaround Oracle JDBC bug #34545179: ResultSet references are
1197+
// retained even when the statement is closed. Calling getMoreResults
1198+
// with the CLOSE_ALL_RESULTS argument forces the driver to
1199+
// de-reference them.
1200+
preparedStatement.getMoreResults(CLOSE_ALL_RESULTS);
1201+
}
1202+
catch (SQLException sqlException) {
1203+
// It may be the case that the JDBC connection was closed, and so the
1204+
// statement was closed with it. Check for this, and ignore the
1205+
// SQLException if so.
1206+
if (!jdbcConnection.isClosed())
1207+
throw sqlException;
1208+
}
1209+
1210+
preparedStatement.close();
1211+
}
1212+
11811213
/**
11821214
* Sets the {@code value} of a {@code preparedStatement} parameter at the
11831215
* specified {@code index}. If a non-null {@code type} is provided, then it is
@@ -1454,7 +1486,7 @@ private JdbcBatch(
14541486
*/
14551487
@Override
14561488
protected Publisher<Void> bind() {
1457-
@SuppressWarnings({"unchecked","rawtypes"})
1489+
@SuppressWarnings({"unchecked"})
14581490
Publisher<Void>[] bindPublishers = new Publisher[batchSize];
14591491
for (int i = 0; i < batchSize; i++) {
14601492
bindPublishers[i] = Flux.concat(

src/oracle-r2dbc-master.iml

Lines changed: 0 additions & 49 deletions
This file was deleted.

src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import reactor.core.publisher.Flux;
3838
import reactor.core.publisher.Mono;
3939

40-
import javax.naming.spi.NamingManager;
4140
import java.io.IOException;
4241
import java.nio.channels.ServerSocketChannel;
4342
import java.nio.channels.SocketChannel;
@@ -46,10 +45,12 @@
4645
import java.nio.file.StandardOpenOption;
4746
import java.sql.SQLException;
4847
import java.time.Duration;
48+
import java.time.ZonedDateTime;
4949
import java.util.Objects;
5050
import java.util.Optional;
5151
import java.util.Properties;
5252
import java.util.Set;
53+
import java.util.TimeZone;
5354
import java.util.concurrent.CompletableFuture;
5455
import java.util.concurrent.CompletionException;
5556
import java.util.concurrent.ExecutionException;
@@ -616,6 +617,53 @@ private static String createDescriptor() {
616617
serviceName());
617618
}
618619

620+
/**
621+
* Verifies the {@link OracleR2dbcOptions#TIMEZONE_AS_REGION} option
622+
*/
623+
@Test
624+
public void testTimezoneAsRegion() {
625+
// Set the timezone to that of Warsaw. When JDBC opens a connection, it will
626+
// read the Warsaw timezone from TimeZone.getDefault().
627+
TimeZone warsawTimeZone = TimeZone.getTimeZone("Europe/Warsaw");
628+
TimeZone timeZoneRestored = TimeZone.getDefault();
629+
TimeZone.setDefault(warsawTimeZone);
630+
try {
631+
632+
// Configure the JDBC connection property with a URL parameter. This has
633+
// JDBC express the session timezone as an offset of UTC (+02:00), rather
634+
// than a name (Europe/Warsaw).
635+
Connection connection = awaitOne(ConnectionFactories.get(
636+
ConnectionFactoryOptions.parse(format(
637+
"r2dbc:oracle://%s:%d/%s?oracle.jdbc.timezoneAsRegion=false",
638+
host(), port(), serviceName()))
639+
.mutate()
640+
.option(USER, user())
641+
.option(PASSWORD, password())
642+
.build())
643+
.create());
644+
try {
645+
646+
// Query the session timezone, and expect it to be expressed as an
647+
// offset, rather than a name.
648+
assertEquals(
649+
ZonedDateTime.now(warsawTimeZone.toZoneId())
650+
.getOffset()
651+
.toString(),
652+
awaitOne(awaitOne(connection.createStatement(
653+
"SELECT sessionTimeZone FROM sys.dual")
654+
.execute())
655+
.map(row ->
656+
row.get(0, String.class))));
657+
}
658+
finally {
659+
tryAwaitNone(connection.close());
660+
}
661+
}
662+
finally {
663+
TimeZone.setDefault(timeZoneRestored);
664+
}
665+
}
666+
619667
/**
620668
* Verifies that an attempt to connect with a {@code listeningChannel}
621669
* results in an {@link R2dbcTimeoutException}.

src/test/java/oracle/r2dbc/impl/TypeMappingTest.java

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.r2dbc.spi.Blob;
2525
import io.r2dbc.spi.Clob;
2626
import io.r2dbc.spi.Connection;
27+
import io.r2dbc.spi.Parameters;
2728
import io.r2dbc.spi.Row;
2829
import io.r2dbc.spi.Statement;
2930
import oracle.sql.json.OracleJsonFactory;
@@ -50,6 +51,9 @@
5051
import java.util.stream.Collectors;
5152
import java.util.stream.Stream;
5253

54+
import static io.r2dbc.spi.R2dbcType.NCHAR;
55+
import static io.r2dbc.spi.R2dbcType.NCLOB;
56+
import static io.r2dbc.spi.R2dbcType.NVARCHAR;
5357
import static java.util.Arrays.asList;
5458
import static oracle.r2dbc.test.DatabaseConfig.connectTimeout;
5559
import static oracle.r2dbc.test.DatabaseConfig.databaseVersion;
@@ -95,6 +99,16 @@ public class TypeMappingTest {
9599
* <a href="https://r2dbc.io/spec/0.8.3.RELEASE/spec/html/#datatypes.mapping">
96100
* Table 4 of Section 12 of the R2DBC 0.8.3 Specification.
97101
* </a>
102+
* </p><p>
103+
* This test method makes use of {@link io.r2dbc.spi.R2dbcType#NCHAR} and
104+
* {@link io.r2dbc.spi.R2dbcType#NVARCHAR} when binding Strings that contain
105+
* non-ascii characters. By default, a String bind is mapped to the VARCHAR
106+
* SQL type. This default mapping has the driver encode the value using the
107+
* database character set. The database character set may not support
108+
* non-ascii characters. Binding Strings with the NCHAR/NVARCHAR type
109+
* configures the driver to encode the string using the national character set
110+
* of the database. The national character set is either UTF16 or UTF8, and so
111+
* it must support non-ascii characters.
98112
* </p>
99113
*/
100114
@Test
@@ -112,11 +126,18 @@ public void testCharacterTypeMappings() {
112126

113127
// Expect NCHAR and String to map
114128
verifyTypeMapping(connection,
115-
String.format("%100s", "你好, Oracle"), "NCHAR(100)");
129+
Parameters.in(NCHAR, String.format("%100s", "你好, Oracle")),
130+
"NCHAR(100)",
131+
(expected, actual) ->
132+
assertEquals(expected.getValue(), actual));
116133

117134
// Expect NVARCHAR and String to map. The Oracle type named "NVARCHAR2" is
118135
// equivalent to the standard type named "NVARCHAR"
119-
verifyTypeMapping(connection, "नमस्कार, Oracle", "NVARCHAR2(100)");
136+
verifyTypeMapping(connection,
137+
Parameters.in(NVARCHAR, "नमस्कार, Oracle"),
138+
"NVARCHAR2(100)",
139+
(expected, actual) ->
140+
assertEquals(expected.getValue(), actual));
120141

121142
// Expect CLOB and String to map
122143
verifyTypeMapping(connection, "Hola, Oracle", "CLOB");
@@ -130,7 +151,11 @@ public void testCharacterTypeMappings() {
130151

131152
// Expect NCLOB and String to map for bind values, but not for row values.
132153
// For row values, expect Oracle CLOB to be mapped to io.r2dbc.spi.Clob
133-
verifyTypeMapping(connection, "こんにちは, Oracle", "NCLOB");
154+
verifyTypeMapping(connection,
155+
Parameters.in(NVARCHAR, "こんにちは, Oracle"),
156+
"NCLOB",
157+
(expected, actual) ->
158+
assertEquals(expected.getValue(), actual));
134159

135160
// Expect NCLOB and io.r2dbc.spi.Clob to map
136161
verifyTypeMapping(connection,

0 commit comments

Comments
 (0)