├── .github └── workflows │ ├── build-and-test.yml │ ├── startup │ └── 01_createUser.sql │ └── test.sh ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.txt ├── NOTICE.txt ├── README.md ├── SECURITY.md ├── THIRD_PARTY_LICENSES.txt ├── pom.xml ├── sample ├── README.md ├── example-config.properties ├── pom.xml └── src │ └── main │ └── java │ └── oracle │ └── r2dbc │ └── samples │ ├── DatabaseConfig.java │ ├── DescriptorURL.java │ ├── JdbcToR2dbc.java │ └── TcpsConnectDemo.java └── src ├── main ├── java │ ├── module-info.java │ └── oracle │ │ └── r2dbc │ │ ├── OracleR2dbcObject.java │ │ ├── OracleR2dbcObjectMetadata.java │ │ ├── OracleR2dbcOptions.java │ │ ├── OracleR2dbcTypes.java │ │ ├── OracleR2dbcWarning.java │ │ ├── impl │ │ ├── AsyncLock.java │ │ ├── AsyncLockImpl.java │ │ ├── DependentCounter.java │ │ ├── Main.java │ │ ├── NoOpAsyncLock.java │ │ ├── OracleBatchImpl.java │ │ ├── OracleConnectionFactoryImpl.java │ │ ├── OracleConnectionFactoryMetadataImpl.java │ │ ├── OracleConnectionFactoryProviderImpl.java │ │ ├── OracleConnectionImpl.java │ │ ├── OracleConnectionMetadataImpl.java │ │ ├── OracleLargeObjects.java │ │ ├── OracleR2dbcExceptions.java │ │ ├── OracleReactiveJdbcAdapter.java │ │ ├── OracleReadableImpl.java │ │ ├── OracleReadableMetadataImpl.java │ │ ├── OracleResultImpl.java │ │ ├── OracleStatementImpl.java │ │ ├── Publishers.java │ │ ├── ReactiveJdbcAdapter.java │ │ ├── ReadablesMetadata.java │ │ ├── SqlParameterParser.java │ │ ├── SqlTypeMap.java │ │ ├── SuppliedOptionConnectionFactory.java │ │ └── package-info.java │ │ └── package-info.java └── resources │ └── META-INF │ └── services │ └── io.r2dbc.spi.ConnectionFactoryProvider └── test ├── java └── oracle │ └── r2dbc │ ├── impl │ ├── OracleBatchImplTest.java │ ├── OracleConnectionFactoryImplTest.java │ ├── OracleConnectionFactoryProviderImplTest.java │ ├── OracleConnectionImplTest.java │ ├── OracleLargeObjectsTest.java │ ├── OracleR2dbcExceptionsTest.java │ ├── OracleReactiveJdbcAdapterTest.java │ ├── OracleReadableImplTest.java │ ├── OracleReadableMetadataImplTest.java │ ├── OracleResultImplTest.java │ ├── OracleRowMetadataImplTest.java │ ├── OracleStatementImplTest.java │ ├── SqlParameterParserTest.java │ └── TypeMappingTest.java │ ├── test │ ├── DatabaseConfig.java │ ├── OracleTestKit.java │ └── TestUtils.java │ └── util │ ├── Awaits.java │ ├── SharedConnectionFactory.java │ └── TestContextFactory.java └── resources └── example-config.properties /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020, 2021, Oracle and/or its affiliates. 2 | # 3 | # This software is dual-licensed to you under the Universal Permissive License 4 | # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 5 | # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 6 | # either license. 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # https://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | 20 | # This script will start an Oracle Database inside a Docker container and then 21 | # execute the Oracle R2DBC test suite with a configuration that has it connect 22 | # to that database. 23 | # 24 | # This script makes no attempt to clean up. The docker container is left 25 | # running, and the database retains the test user and any other modifications 26 | # that the test suite may have performed. 27 | # It is assumed that the Github Runner will clean up any state this script 28 | # leaves behind. 29 | 30 | name: Build and Test Oracle R2DBC 31 | 32 | on: 33 | push: 34 | branches: 35 | - main 36 | - development 37 | pull_request: 38 | branches: 39 | - main 40 | - development 41 | 42 | jobs: 43 | # Builds the Oracle R2DBC Driver using Maven 44 | build: 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v2 48 | - name: Set up JDK 11 49 | uses: actions/setup-java@v1 50 | with: 51 | java-version: 11 52 | - name: Build with Maven 53 | run: mvn -B package --file pom.xml -DskipTests=true 54 | # Tests the Oracle R2DBC Driver with an Oracle Database 55 | test: 56 | needs: build 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v2 60 | - name: Test with Oracle Database 61 | run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 62 | -------------------------------------------------------------------------------- /.github/workflows/startup/01_createUser.sql: -------------------------------------------------------------------------------- 1 | -- Copyright (c) 2020, 2021, Oracle and/or its affiliates. 2 | -- 3 | -- This software is dual-licensed to you under the Universal Permissive License 4 | -- (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 5 | -- 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 6 | -- either license. 7 | -- 8 | -- Licensed under the Apache License, Version 2.0 (the "License"); 9 | -- you may not use this file except in compliance with the License. 10 | -- You may obtain a copy of the License at 11 | -- 12 | -- https://www.apache.org/licenses/LICENSE-2.0 13 | -- 14 | -- Unless required by applicable law or agreed to in writing, software 15 | -- distributed under the License is distributed on an "AS IS" BASIS, 16 | -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | -- See the License for the specific language governing permissions and 18 | -- limitations under the License. 19 | 20 | -- This script will start an Oracle Database inside a Docker container and then 21 | -- execute the Oracle R2DBC test suite with a configuration that has it connect 22 | -- to that database. 23 | -- 24 | -- This script makes no attempt to clean up. The docker container is left 25 | -- running, and the database retains the test user and any other modifications 26 | -- that the test suite may have performed. 27 | -- It is assumed that the Github Runner will clean up any state this script 28 | -- leaves behind. 29 | 30 | 31 | -- This script will be run as sysdba connected to the container database. This 32 | -- script will create a test user in the xepdb1 pluggable database. The test 33 | -- user is granted permission to connect to the database, create/query/modify 34 | -- tables, and to query some V$ views: 35 | -- v$open_cursor (to verify if cursors are being closed). 36 | -- v$transaction (to verify if TransactionDefinitions are applied). 37 | -- v$session (to verify if VSESSION_* Options are applied). 38 | ALTER SESSION SET CONTAINER=xepdb1; 39 | CREATE ROLE r2dbc_test_role; 40 | GRANT SELECT ON v_$open_cursor TO r2dbc_test_role; 41 | GRANT SELECT ON v_$transaction TO r2dbc_test_role; 42 | GRANT SELECT ON v_$session TO r2dbc_test_role; 43 | GRANT CREATE VIEW TO r2dbc_test_role; 44 | 45 | CREATE USER test IDENTIFIED BY test; 46 | GRANT connect, resource, unlimited tablespace, r2dbc_test_role TO test; 47 | ALTER USER test DEFAULT TABLESPACE users; 48 | ALTER USER test TEMPORARY TABLESPACE temp; 49 | -------------------------------------------------------------------------------- /.github/workflows/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright (c) 2020, 2021, Oracle and/or its affiliates. 4 | # 5 | # This software is dual-licensed to you under the Universal Permissive License 6 | # (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 7 | # 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 8 | # either license. 9 | # 10 | # Licensed under the Apache License, Version 2.0 (the "License"); 11 | # you may not use this file except in compliance with the License. 12 | # You may obtain a copy of the License at 13 | # 14 | # https://www.apache.org/licenses/LICENSE-2.0 15 | # 16 | # Unless required by applicable law or agreed to in writing, software 17 | # distributed under the License is distributed on an "AS IS" BASIS, 18 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | # See the License for the specific language governing permissions and 20 | # limitations under the License. 21 | 22 | # This script will start an Oracle Database inside a Docker container and then 23 | # execute the Oracle R2DBC test suite with a configuration that has it connect 24 | # to that database. 25 | # 26 | # This script makes no attempt to clean up. The docker container is left 27 | # running, and the database retains the test user and any other modifications 28 | # that the test suite may have performed. 29 | # It is assumed that the Github Runner will clean up any state this script 30 | # leaves behind. 31 | 32 | 33 | # The startup directory is mounted as a volume inside the docker container. 34 | # The container's entry point script will execute any .sh and .sql scripts 35 | # it finds under /opt/oracle/scripts/startup. The startup scripts are run 36 | # after the database instance is active.A numeric prefix on the script name 37 | # determines the order in which scripts are run. The final script, prefixed 38 | # with "99_" will create a file named "done" in the mounted volumn. When the 39 | # "done" file exists, this signals that the database is active and that all 40 | # startup scripts have completed. 41 | startUpScripts=$PWD/startup 42 | startUpMount=/opt/oracle/scripts/startup 43 | echo "touch $startUpMount/done" > $startUpScripts/99_done.sh 44 | 45 | 46 | # The oracle/docker-images repo is cloned. This repo provides Dockerfiles along 47 | # with a handy script to build images of Oracle Database. For now, this script 48 | # is just going to build an 18.4.0 XE image, because this can be done in an 49 | # automated fashion, without having to accept license agreements required by 50 | # newer versions like 19 and 21. 51 | # TODO: Also test with newer database versions 52 | git clone https://github.com/oracle/docker-images.git 53 | cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ 54 | ./buildContainerImage.sh -v 18.4.0 -x 55 | 56 | # Run the image in a detached container 57 | # The startup directory is mounted. It contains a createUser.sql script that 58 | # creates a test user. The docker container will run this script once the 59 | # database has started. 60 | # The database port number, 1521, is mapped to the host system. The Oracle 61 | # R2DBC test suite is configured to connect with this port. 62 | docker run --name test_db --detach --rm -p 1521:1521 -v $startUpScripts:$startUpMount oracle/database:18.4.0-xe 63 | 64 | # Wait for the database instance to start. The final startup script will create 65 | # a file named "done" in the startup directory. When that file exists, it means 66 | # the database is ready for testing. 67 | echo "Waiting for database to start..." 68 | until [ -f $startUpScripts/done ] 69 | do 70 | docker logs --since 3s test_db 71 | sleep 3 72 | done 73 | 74 | # Create a configuration file and run the tests. The service name, "xepdb1", 75 | # is always created for the 18.4.0 XE database, but it would probably change 76 | # for other database versions (TODO). The test user is created by the 77 | # startup/01_createUser.sql script 78 | cd $GITHUB_WORKSPACE 79 | echo "DATABASE=xepdb1" > src/test/resources/config.properties 80 | echo "HOST=localhost" >> src/test/resources/config.properties 81 | echo "PORT=1521" >> src/test/resources/config.properties 82 | echo "USER=test" >> src/test/resources/config.properties 83 | echo "PASSWORD=test" >> src/test/resources/config.properties 84 | echo "CONNECT_TIMEOUT=240" >> src/test/resources/config.properties 85 | echo "SQL_TIMEOUT=240" >> src/test/resources/config.properties 86 | mvn clean compile test 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .DS_Store 3 | *.iml 4 | /src/test/resources/config.properties 5 | /sample/target/ 6 | /sample/config.properties 7 | .github/workflows/startup/99_done.sh 8 | .github/workflows/startup/done 9 | .idea/ 10 | *.swp 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this repository 2 | 3 | We welcome your contributions! There are multiple ways to contribute. 4 | 5 | ## Opening issues 6 | 7 | For bugs or enhancement requests, please file a GitHub issue unless it's 8 | security related. When filing a bug remember that the better written the bug is, 9 | the more likely it is to be fixed. If you think you've found a security 10 | vulnerability, do not raise a GitHub issue and follow the instructions in our 11 | [security policy](./SECURITY.md). 12 | 13 | ## Contributing code 14 | 15 | We welcome your code contributions. Before submitting code via a pull request, 16 | you will need to have signed the [Oracle Contributor Agreement][OCA] (OCA) and 17 | your commits need to include the following line using the name and e-mail 18 | address you used to sign the OCA: 19 | 20 | ```text 21 | Signed-off-by: Your Name 22 | ``` 23 | 24 | This can be automatically added to pull requests by committing with `--sign-off` 25 | or `-s`, e.g. 26 | 27 | ```text 28 | git commit --signoff 29 | ``` 30 | 31 | Only pull requests from committers that can be verified as having signed the OCA 32 | can be accepted. 33 | 34 | ## Pull request process 35 | 36 | 1. Ensure there is an issue created to track and discuss the fix or enhancement 37 | you intend to submit. 38 | 1. Fork this repository. 39 | 1. Create a branch in your fork to implement the changes. We recommend using 40 | the issue number as part of your branch name, e.g. `1234-fixes`. 41 | 1. Ensure that any documentation is updated with the changes that are required 42 | by your change. 43 | 1. Ensure that any samples are updated if the base image has been changed. 44 | 1. Submit the pull request. *Do not leave the pull request blank*. Explain exactly 45 | what your changes are meant to do and provide simple steps on how to validate. 46 | your changes. Ensure that you reference the issue you created as well. 47 | 1. We will assign the pull request to 2-3 people for review before it is merged. 48 | 49 | ## Code of conduct 50 | 51 | Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd 52 | like more specific guidelines, see the [Contributor Covenant Code of Conduct][COC]. 53 | 54 | [OCA]: https://oca.opensource.oracle.com 55 | [COC]: https://www.contributor-covenant.org/version/1/4/code-of-conduct/ 56 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 2 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security vulnerabilities 2 | 3 | Oracle values the independent security research community and believes that 4 | responsible disclosure of security vulnerabilities helps us ensure the security 5 | and privacy of all our users. 6 | 7 | Please do NOT raise a GitHub Issue to report a security vulnerability. If you 8 | believe you have found a security vulnerability, please submit a report to 9 | [secalert_us@oracle.com][1] preferably with a proof of concept. Please review 10 | some additional information on [how to report security vulnerabilities to Oracle][2]. 11 | We encourage people who contact Oracle Security to use email encryption using 12 | [our encryption key][3]. 13 | 14 | We ask that you do not use other channels or contact the project maintainers 15 | directly. 16 | 17 | Non-vulnerability related security issues including ideas for new or improved 18 | security features are welcome on GitHub Issues. 19 | 20 | ## Security updates, alerts and bulletins 21 | 22 | Security updates will be released on a regular cadence. Many of our projects 23 | will typically release security fixes in conjunction with the 24 | Oracle Critical Patch Update program. Additional 25 | information, including past advisories, is available on our [security alerts][4] 26 | page. 27 | 28 | ## Security-related information 29 | 30 | We will provide security related information such as a threat model, considerations 31 | for secure use, or any known security issues in our documentation. Please note 32 | that labs and sample code are intended to demonstrate a concept and may not be 33 | sufficiently hardened for production use. 34 | 35 | [1]: mailto:secalert_us@oracle.com 36 | [2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html 37 | [3]: https://www.oracle.com/security-alerts/encryptionkey.html 38 | [4]: https://www.oracle.com/security-alerts/ 39 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 25 | 4.0.0 26 | 27 | com.oracle.database.r2dbc 28 | oracle-r2dbc 29 | 1.3.0 30 | oracle-r2dbc 31 | 32 | Oracle R2DBC Driver implementing version 1.0.0 of the R2DBC SPI for Oracle Database. 33 | 34 | 35 | https://github.com/oracle/oracle-r2dbc 36 | 37 | 2019 38 | 39 | 40 | Universal Permissive License v1.0 41 | https://opensource.org/licenses/UPL 42 | 43 | 44 | Apache License, Version 2.0 45 | https://www.apache.org/licenses/LICENSE-2.0 46 | 47 | 48 | 49 | 50 | Oracle America, Inc. 51 | http://www.oracle.com 52 | 53 | 54 | 55 | https://github.com/oracle/oracle-r2dbc.git 56 | 57 | scm:git:https://github.com/oracle/oracle-r2dbc.git 58 | 59 | scm:git:git@github.com:oracle/oracle-r2dbc.git 60 | 61 | 62 | GitHub 63 | https://github.com/oracle/oracle-r2dbc/issues 64 | 65 | 66 | 67 | 11 68 | 23.6.0.24.10 69 | 1.0.0.RELEASE 70 | 3.6.11 71 | 1.0.3 72 | 5.9.1 73 | 5.3.19 74 | UTF-8 75 | 76 | 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-compiler-plugin 82 | 3.8.1 83 | 84 | 85 | -Xlint:all 86 | -Xlint:-options 87 | -Xlint:-processing 88 | -Xlint:-serial 89 | 90 | --add-modules 91 | java.naming 92 | 93 | true 94 | ${java.version} 95 | 96 | 97 | 98 | org.apache.maven.plugins 99 | maven-jar-plugin 100 | 3.2.0 101 | 102 | 103 | 104 | Oracle R2DBC 105 | 106 | ${project.version} 107 | 108 | Oracle Corporation 109 | 110 | R2DBC - Reactive Relational Database Connectivity 111 | 112 | ${r2dbc.version} 113 | 114 | Pivotal Software, Inc 115 | 116 | 117 | Oracle R2DBC ${project.version} compiled with JDK ${java.vm.version} from ${java.vm.vendor} on ${maven.build.timestamp} 118 | 119 | 120 | oracle.r2dbc.impl.Main 121 | 122 | 123 | 124 | 125 | 126 | 127 | org.apache.maven.plugins 128 | maven-source-plugin 129 | 3.2.1 130 | 131 | 132 | org.apache.maven.plugins 133 | maven-deploy-plugin 134 | 3.0.0-M1 135 | 136 | 137 | org.apache.maven.plugins 138 | maven-install-plugin 139 | 3.0.0-M1 140 | 141 | 142 | org.apache.maven.plugins 143 | maven-clean-plugin 144 | 3.1.0 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-resources-plugin 149 | 3.2.0 150 | 151 | 152 | org.apache.maven.plugins 153 | maven-surefire-plugin 154 | 3.0.0-M5 155 | 156 | 157 | 158 | **/*Test.java 159 | **/*TestKit.java 160 | 161 | 162 | --add-reads com.oracle.database.r2dbc=java.naming 163 | 164 | 165 | 166 | org.apache.maven.plugins 167 | maven-javadoc-plugin 168 | 3.2.0 169 | 170 |
Oracle R2DBC ${project.version}
171 |
172 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 173 |
174 | package 175 | 181 | src/main/java 182 | 183 | https://r2dbc.io/spec/${r2dbc.version}/api/ 184 | https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/ 185 | https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/ 186 | 187 | 188 | 189 | implNote 190 | a 191 | Implementation Note: 192 | 193 | 194 | implSpec 195 | a 196 | Implementation Requirements: 197 | 198 | 199 |
200 |
201 |
202 | 203 | 204 | ${project.basedir} 205 | 206 | NOTICE.txt 207 | LICENSE.txt 208 | THIRD_PARTY_LICENSES.txt 209 | 210 | META-INF 211 | 212 | 213 | ${project.basedir}/src/main/resources/META-INF/services/ 214 | 215 | io.r2dbc.spi.ConnectionFactoryProvider 216 | 217 | META-INF/services/ 218 | 219 | 220 |
221 | 222 | 223 | 224 | 225 | 226 | io.r2dbc 227 | r2dbc-spi 228 | ${r2dbc.version} 229 | 230 | 231 | com.oracle.database.jdbc 232 | ojdbc11 233 | ${ojdbc.version} 234 | 235 | 236 | io.projectreactor 237 | reactor-core 238 | ${reactor.version} 239 | 240 | 241 | org.reactivestreams 242 | reactive-streams 243 | ${reactive-streams.version} 244 | 245 | 246 | 247 | 248 | io.r2dbc 249 | r2dbc-spi-test 250 | ${r2dbc.version} 251 | test 252 | 253 | 254 | org.junit.jupiter 255 | junit-jupiter-api 256 | ${junit.version} 257 | test 258 | 259 | 260 | org.junit.jupiter 261 | junit-jupiter-engine 262 | ${junit.version} 263 | test 264 | 265 | 266 | org.springframework 267 | spring-jdbc 268 | ${spring-jdbc.version} 269 | test 270 | 271 | 272 | 273 | 274 | 275 | publication 276 | 277 | 278 | release 279 | 280 | 281 | 282 | 283 | 284 | org.apache.maven.plugins 285 | maven-javadoc-plugin 286 | 287 | 288 | attach-javadocs 289 | 290 | jar 291 | 292 | 293 | true 294 | 295 | 296 | 297 | 298 | 299 | org.apache.maven.plugins 300 | maven-source-plugin 301 | 302 | 303 | attach-sources 304 | 305 | jar 306 | 307 | 308 | true 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 |
319 | 320 | 326 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | # Oracle R2DBC code examples 2 | A simple R2DBC program that connects to Autonomous Database and executes a query 3 | 4 | # Building manually 5 | 6 | | | Task | Command | 7 | | ------ | ----- | ------------------------------------------------ | 8 | | Maven | Build | `mvn clean install` | 9 | | | Run | `mvn exec:java` | 10 | | Gradle | Build | `gradle build` | 11 | | | Run | `gradle run` | 12 | 13 | -------------------------------------------------------------------------------- /sample/example-config.properties: -------------------------------------------------------------------------------- 1 | # Values in this properties file configure how sample code connects to a # database. # This file contains example values. Create a copy named config.properties in 2 | # /sample and change the example values to actual values for your test database. 3 | 4 | # Host name of a test database 5 | HOST=db.host.example.com 6 | 7 | # Port number of a test database 8 | PORT=1521 9 | 10 | # Service name of a test database 11 | DATABASE=db.service.name 12 | 13 | # User name authenticated by a test database 14 | USER=db_user 15 | 16 | # Password authenticated by a test database 17 | PASSWORD=db_password 18 | 19 | # File system path to a directory containing an Oracle Wallet file 20 | WALLET_LOCATION=/path/to/wallet 21 | -------------------------------------------------------------------------------- /sample/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 25 | 4.0.0 26 | com.oracle.database.r2dbc 27 | oracle-r2dbc-samples 28 | 1.2.0 29 | oracle-r2dbc-samples 30 | 31 | Code examples for the Oracle R2DBC Driver 32 | 33 | 34 | https://github.com/oracle/oracle-r2dbc 35 | 36 | 2019 37 | 38 | 39 | Universal Permissive License v1.0 40 | https://opensource.org/licenses/UPL 41 | 42 | 43 | Apache License, Version 2.0 44 | https://www.apache.org/licenses/LICENSE-2.0 45 | 46 | 47 | 48 | 49 | Oracle America, Inc. 50 | http://www.oracle.com 51 | 52 | 53 | 54 | https://github.com/oracle/oracle-r2dbc.git 55 | 56 | scm:git:https://github.com/oracle/oracle-r2dbc.git 57 | 58 | scm:git:git@github.com:oracle/oracle-r2dbc.git 59 | 60 | 61 | GitHub 62 | https://github.com/oracle/oracle-r2dbc/issues 63 | 64 | 65 | 66 | 11 67 | 1.2.0 68 | 3.5.11 69 | 70 | 71 | 72 | 73 | 74 | org.apache.maven.plugins 75 | maven-compiler-plugin 76 | 3.8.1 77 | 78 | 79 | -Xlint:all 80 | -Xlint:-options 81 | -Xlint:-processing 82 | -Xlint:-serial 83 | 84 | true 85 | ${java.version} 86 | 87 | 88 | 89 | org.apache.maven.plugins 90 | maven-clean-plugin 91 | 3.1.0 92 | 93 | 94 | 95 | 96 | 97 | 98 | com.oracle.database.r2dbc 99 | oracle-r2dbc 100 | ${oracle-r2dbc.version} 101 | 102 | 103 | io.projectreactor 104 | reactor-test 105 | ${reactor.version} 106 | test 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /sample/src/main/java/oracle/r2dbc/samples/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.samples; 23 | 24 | import java.io.IOException; 25 | import java.io.UncheckedIOException; 26 | import java.nio.file.Files; 27 | import java.nio.file.Path; 28 | import java.util.Properties; 29 | 30 | /** 31 | *

32 | * Configuration for connecting code samples to an Oracle Database instance. 33 | *

34 | * The configuration is read from a properties file in the current directory 35 | * by default, or from a file specified as 36 | * -DCONFIG_FILE=/path/to/your/config.properties 37 | *

38 | */ 39 | public class DatabaseConfig { 40 | 41 | /** Path to a configuration file: config.properties */ 42 | private static final Path CONFIG_PATH = 43 | Path.of(System.getProperty("CONFIG_FILE", "config.properties")); 44 | 45 | /** Configuration that is read from a file at {@link #CONFIG_PATH} */ 46 | private static final Properties CONFIG; 47 | static { 48 | try (var fileStream = Files.newInputStream(CONFIG_PATH)) { 49 | CONFIG = new Properties(); 50 | CONFIG.load(fileStream); 51 | } 52 | catch (IOException readFailure) { 53 | throw new UncheckedIOException(readFailure); 54 | } 55 | } 56 | 57 | /** Host name where an Oracle Database instance is running */ 58 | static final String HOST = CONFIG.getProperty("HOST"); 59 | 60 | /** Port number where an Oracle Database instance is listening */ 61 | static final int PORT = Integer.parseInt(CONFIG.getProperty("PORT")); 62 | 63 | /** Service name of an Oracle Database */ 64 | static final String SERVICE_NAME = CONFIG.getProperty("DATABASE"); 65 | 66 | /** User name that connects to an Oracle Database */ 67 | static final String USER = CONFIG.getProperty("USER"); 68 | 69 | /** Password of the user that connects to an Oracle Database */ 70 | static final String PASSWORD = CONFIG.getProperty("PASSWORD"); 71 | 72 | /** The file system path of a wallet directory */ 73 | static final String WALLET_LOCATION = 74 | CONFIG.getProperty("WALLET_LOCATION"); 75 | } 76 | -------------------------------------------------------------------------------- /sample/src/main/java/oracle/r2dbc/samples/DescriptorURL.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.samples; 23 | 24 | import io.r2dbc.spi.ConnectionFactories; 25 | import io.r2dbc.spi.ConnectionFactoryOptions; 26 | import oracle.r2dbc.OracleR2dbcOptions; 27 | import reactor.core.publisher.Mono; 28 | 29 | import static oracle.r2dbc.samples.DatabaseConfig.HOST; 30 | import static oracle.r2dbc.samples.DatabaseConfig.PORT; 31 | import static oracle.r2dbc.samples.DatabaseConfig.SERVICE_NAME; 32 | import static oracle.r2dbc.samples.DatabaseConfig.USER; 33 | import static oracle.r2dbc.samples.DatabaseConfig.PASSWORD; 34 | 35 | /** 36 | * This code example shows how to use TNS descriptor URLs with Oracle R2DBC. 37 | * The TNS descriptor has the form: 38 | *
39 |  *   (DESCRIPTION=...)
40 |  * 
41 | * The full syntax of the TNS descriptor is described in the 42 | * 43 | * Oracle Net Services Administrator's Guide 44 | * 45 | */ 46 | public class DescriptorURL { 47 | 48 | /** 49 | * A TNS descriptor specifying the HOST, PORT, and SERVICE_NAME read from 50 | * {@link DatabaseConfig}. These values can be configured in a 51 | * config.properties file of the current directory. 52 | */ 53 | private static final String DESCRIPTOR = "(DESCRIPTION=" + 54 | "(ADDRESS=(HOST="+HOST+")(PORT="+PORT+")(PROTOCOL=tcp))" + 55 | "(CONNECT_DATA=(SERVICE_NAME="+SERVICE_NAME+")))"; 56 | 57 | public static void main(String[] args) { 58 | // A descriptor may appear in the query section of an R2DBC URL: 59 | String r2dbcUrl = "r2dbc:oracle://?oracle.r2dbc.descriptor="+DESCRIPTOR; 60 | Mono.from(ConnectionFactories.get(ConnectionFactoryOptions.parse(r2dbcUrl) 61 | .mutate() 62 | .option(ConnectionFactoryOptions.USER, USER) 63 | .option(ConnectionFactoryOptions.PASSWORD, PASSWORD) 64 | .build()) 65 | .create()) 66 | .flatMapMany(connection -> 67 | Mono.from(connection.createStatement( 68 | "SELECT 'Connected with TNS descriptor' FROM sys.dual") 69 | .execute()) 70 | .flatMapMany(result -> 71 | result.map(row -> row.get(0, String.class))) 72 | .concatWith(Mono.from(connection.close()).cast(String.class))) 73 | .toStream() 74 | .forEach(System.out::println); 75 | 76 | // A descriptor may also be specified as an Option 77 | Mono.from(ConnectionFactories.get(ConnectionFactoryOptions.builder() 78 | .option(ConnectionFactoryOptions.DRIVER, "oracle") 79 | .option(OracleR2dbcOptions.DESCRIPTOR, DESCRIPTOR) 80 | .option(ConnectionFactoryOptions.USER, USER) 81 | .option(ConnectionFactoryOptions.PASSWORD, PASSWORD) 82 | .build()) 83 | .create()) 84 | .flatMapMany(connection -> 85 | Mono.from(connection.createStatement( 86 | "SELECT 'Connected with TNS descriptor' FROM sys.dual") 87 | .execute()) 88 | .flatMapMany(result -> 89 | result.map((row, metadata) -> row.get(0, String.class))) 90 | .concatWith(Mono.from(connection.close()).cast(String.class))) 91 | .toStream() 92 | .forEach(System.out::println); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /sample/src/main/java/oracle/r2dbc/samples/TcpsConnectDemo.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.samples; 23 | 24 | import io.r2dbc.spi.ConnectionFactories; 25 | import io.r2dbc.spi.ConnectionFactory; 26 | import io.r2dbc.spi.ConnectionFactoryOptions; 27 | import io.r2dbc.spi.Option; 28 | import oracle.jdbc.OracleConnection; 29 | import org.reactivestreams.Publisher; 30 | import reactor.core.publisher.Flux; 31 | import reactor.core.publisher.Mono; 32 | 33 | import java.net.URI; 34 | import java.net.URISyntaxException; 35 | import java.time.Duration; 36 | 37 | import static oracle.r2dbc.samples.DatabaseConfig.HOST; 38 | import static oracle.r2dbc.samples.DatabaseConfig.PASSWORD; 39 | import static oracle.r2dbc.samples.DatabaseConfig.PORT; 40 | import static oracle.r2dbc.samples.DatabaseConfig.SERVICE_NAME; 41 | import static oracle.r2dbc.samples.DatabaseConfig.USER; 42 | import static oracle.r2dbc.samples.DatabaseConfig.WALLET_LOCATION; 43 | 44 | /** 45 | * Sample code that uses an Oracle Wallet to authenticate with an Oracle 46 | * Database instance. To read the wallet, the Oracle PKI library must be on the 47 | * JVM classpath when this demo is run. The maven coordinates for Oracle PKI 48 | * are: 49 | *
 50 |  *  
 51 |  *    com.oracle.database.security
 52 |  *    oraclepki
 53 |  *    21.11.0.0
 54 |  *  
 55 |  *  
 56 |  *    com.oracle.database.security
 57 |  *    osdt_cert
 58 |  *    21.11.0.0
 59 |  *  
 60 |  *  
 61 |  *    com.oracle.database.security
 62 |  *    osdt_core
 63 |  *    21.11.0.0
 64 |  *  
 65 |  * 
66 | * 67 | * Database connection configuration, including the wallet location, must be 68 | * written to a file named "config.properties" that exists in the current 69 | * directory when this demo is run. There is an example config.properties 70 | * file in the /sample directory (relative to the root this repository). 71 | */ 72 | public class TcpsConnectDemo { 73 | 74 | 75 | public static void main(String[] args) throws URISyntaxException { 76 | 77 | // The R2DBC URL format can configure a TCPS/SSL/TLS enabled connection 78 | // by specifying the "r2dbcs" schema (rather than "r2dbc"). The path to a 79 | // wallet file is specified in the query section of the URL as the value of 80 | // an Oracle JDBC connection property: "oracle.net.wallet_location" 81 | // 82 | // Special characters that appear in an R2DBC URL must be escaped using 83 | // percent encoding. Characters that don't require an escape are limited 84 | // to a-z, A-Z, 0-9, and a few other symbols. All other characters must be 85 | // encoded as a percent sign (%) followed by the hexadecimal digits of 86 | // the character's byte value. Multi-byte characters are escaped as a 87 | // sequence percent encodings, each representing one byte. 88 | // 89 | // Fortunately, the java.net.URI class implements percent encoding, so it 90 | // is used by the code below to generate an R2DBC URL. 91 | String r2dbcsUrl = new URI("r2dbcs:oracle", // schema 92 | USER + ":" + PASSWORD, // userInfo 93 | HOST, // host 94 | PORT, // port 95 | "/" + SERVICE_NAME, // path 96 | "oracle.net.wallet_location=" + WALLET_LOCATION 97 | + "&oracle.jdbc.fanEnabled=false", // query 98 | null) // fragment 99 | .toASCIIString(); 100 | 101 | // The URI.toASCIIString() call above returns a URL like this: 102 | // r2dbcs:oracle:user:p%40ssword!:host.example.com:1522/service.name?oracle.net.wallet_location=/path/to/wallet/&oracle.jdbc.fanEnabled=false 103 | 104 | // With the r2dbcs URL, open a connection and execute SQL 105 | Flux.from(sayHello(ConnectionFactories.get(r2dbcsUrl))) 106 | .doOnNext(System.out::println) 107 | .blockLast(Duration.ofSeconds(60)); 108 | 109 | // As an alternative to the URL, a ConnectionFactoryOptions.Builder 110 | // offers a programmatic API to configure a ConnectionFactory. If the 111 | // wallet is password protected, then the Builder API must be used to 112 | // configure the password. Passwords should never be configured in the URL. 113 | ConnectionFactoryOptions options = 114 | ConnectionFactoryOptions.builder() 115 | .option(ConnectionFactoryOptions.DRIVER, "oracle") 116 | .option(ConnectionFactoryOptions.HOST, HOST) 117 | .option(ConnectionFactoryOptions.PORT, PORT) 118 | .option(ConnectionFactoryOptions.DATABASE, SERVICE_NAME) 119 | .option(ConnectionFactoryOptions.USER, USER) 120 | .option(ConnectionFactoryOptions.PASSWORD, PASSWORD) 121 | // To configure a TCPS/SSL/TLS enabled ConnectionFactory, set the SSL 122 | // option to true, and then specify the path to a wallet location... 123 | .option(ConnectionFactoryOptions.SSL, true) 124 | // The Oracle JDBC wallet location connection property can be 125 | // configured as an R2DBC Option. The connection property name, 126 | // "oracle.net.wallet_location", is the argument to the 127 | // Option.valueOf(String) factory method. 128 | // This is an extended option; It is only recognized by the Oracle 129 | // R2DBC Driver. Other R2DBC drivers might use a different Option to 130 | // configure their TLS certificates. 131 | .option(Option.valueOf( 132 | OracleConnection.CONNECTION_PROPERTY_WALLET_LOCATION), 133 | WALLET_LOCATION) 134 | .option(Option.valueOf( 135 | OracleConnection.CONNECTION_PROPERTY_FAN_ENABLED), 136 | "false") 137 | // To set a wallet password, the Option.sensitiveValueOf(String) 138 | // factory method is used. Any property that stores a password in 139 | // clear text needs to be handled carefully; This factory method is 140 | // used for Options that are configured with sensitive information. 141 | // .option(Option.sensitiveValueOf( 142 | // OracleConnection.CONNECTION_PROPERTY_WALLET_PASSWORD), 143 | // readPasswordSecurely()) // TODO: Prompt user to type password? 144 | .build(); 145 | 146 | // Open a connection and execute SQL 147 | Flux.from(sayHello( 148 | ConnectionFactories.get(options))) 149 | .doOnNext(System.out::println) 150 | .blockLast(Duration.ofSeconds(60)); 151 | } 152 | 153 | /** 154 | * Publishes the result of opening a connection and executing a SQL query 155 | * that returns a greeting message. 156 | * @param connectionFactory Factory configured to open connections 157 | * @return A publisher that emits a greeting 158 | */ 159 | static Publisher sayHello(ConnectionFactory connectionFactory) { 160 | return Mono.from(connectionFactory.create()) 161 | .flatMapMany(connection -> 162 | Flux.from(connection.createStatement( 163 | "SELECT 'Hello, Oracle' FROM sys.dual") 164 | .execute()) 165 | .flatMap(result -> 166 | result.map((row, metadata) -> row.get(0, String.class))) 167 | .concatWith(Mono.from( 168 | connection.close()).cast(String.class))); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | /** 22 | * Implements the R2DBC SPI for Oracle Database. 23 | * 24 | * @provides io.r2dbc.spi.ConnectionFactoryProvider 25 | * @since 0.1.0 26 | */ 27 | module com.oracle.database.r2dbc { 28 | 29 | provides io.r2dbc.spi.ConnectionFactoryProvider 30 | with oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl; 31 | 32 | requires java.sql; 33 | requires com.oracle.database.jdbc; 34 | requires reactor.core; 35 | requires transitive org.reactivestreams; 36 | requires transitive r2dbc.spi; 37 | 38 | exports oracle.r2dbc; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/OracleR2dbcObject.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc; 22 | 23 | /** 24 | *

25 | * A {@link io.r2dbc.spi.Readable} that represents an instance of a user 26 | * defined OBJECT type. 27 | *

28 | * An OBJECT returned by a {@link io.r2dbc.spi.Result} may be mapped to an 29 | * {@code OracleR2dbcObject}: 30 | *

{@code
31 |  * Publisher objectMapExample(Result result) {
32 |  *   return result.map(row -> {
33 |  *
34 |  *     OracleR2dbcObject oracleObject = row.get(0, OracleR2dbcObject.class); 
35 |  *
36 |  *     return new Pet(
37 |  *       oracleObject.get("name", String.class),
38 |  *       oracleObject.get("species", String.class),
39 |  *       oracleObject.get("weight", Float.class),
40 |  *       oracleObject.get("birthday", LocalDate.class));
41 |  *   });
42 |  * }
43 |  *
44 |  * }
45 | * As seen in the example above, the values of an OBJECT's attributes may be 46 | * accessed by name with {@link #get(String)} or {@link #get(String, Class)}. 47 | * Alternatively, attribute values may be accessed by index with {@link #get(int)} or 48 | * {@link #get(int, Class)}. The {@code get} methods support all standard 49 | * SQL-to-Java type mappings defined by the 50 | * 51 | * R2DBC Specification. 52 | * 53 | *

54 | * Instances of {@code OracleR2dbcObject} may be set as a bind value when 55 | * passed to {@link io.r2dbc.spi.Statement#bind(int, Object)} or 56 | * {@link io.r2dbc.spi.Statement#bind(String, Object)}: 57 | *

{@code
58 |  * Publisher objectBindExample(
59 |  *   OracleR2dbcObject oracleObject, Connection connection) {
60 |  *
61 |  *   Statement statement =
62 |  *     connection.createStatement("INSERT INTO petTable VALUES (:petObject)");
63 |  *   
64 |  *   statement.bind("petObject", oracleObject);
65 |  * 
66 |  *   return statement.execute();
67 |  * }
68 |  * }
69 | */ 70 | public interface OracleR2dbcObject extends io.r2dbc.spi.Readable { 71 | 72 | /** 73 | * Returns metadata for the attributes of this OBJECT. 74 | * @return The metadata of this OBJECT's attributes. Not null. 75 | */ 76 | OracleR2dbcObjectMetadata getMetadata(); 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/OracleR2dbcObjectMetadata.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc; 22 | 23 | import io.r2dbc.spi.ReadableMetadata; 24 | 25 | import java.util.List; 26 | import java.util.NoSuchElementException; 27 | 28 | /** 29 | * Represents the metadata for attributes of an OBJECT. Metadata for attributes 30 | * can either be retrieved by index or by name. Attribute indexes are 31 | * {@code 0}-based. Retrieval by attribute name is case-insensitive. 32 | */ 33 | public interface OracleR2dbcObjectMetadata { 34 | 35 | /** 36 | * Returns the type of the OBJECT which metadata is provided for. 37 | * @return The type of the OBJECT. Not null. 38 | */ 39 | OracleR2dbcTypes.ObjectType getObjectType(); 40 | 41 | /** 42 | * Returns the {@link ReadableMetadata} for one attribute. 43 | * 44 | * @param index the attribute index starting at 0 45 | * @return the {@link ReadableMetadata} for one attribute. Not null. 46 | * @throws IndexOutOfBoundsException if {@code index} is out of range 47 | * (negative or equals/exceeds {@code getParameterMetadatas().size()}) 48 | */ 49 | ReadableMetadata getAttributeMetadata(int index); 50 | 51 | /** 52 | * Returns the {@link ReadableMetadata} for one attribute. 53 | * 54 | * @param name the name of the attribute. Not null. Parameter names are 55 | * case-insensitive. 56 | * @return the {@link ReadableMetadata} for one attribute. Not null. 57 | * @throws IllegalArgumentException if {@code name} is {@code null} 58 | * @throws NoSuchElementException if there is no attribute with the 59 | * {@code name} 60 | */ 61 | ReadableMetadata getAttributeMetadata(String name); 62 | 63 | /** 64 | * Returns the {@link ReadableMetadata} for all attributes. 65 | * 66 | * @return the {@link ReadableMetadata} for all attributes. Not null. 67 | */ 68 | List getAttributeMetadatas(); 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/OracleR2dbcTypes.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc; 22 | 23 | import io.r2dbc.spi.Parameter; 24 | import io.r2dbc.spi.R2dbcType; 25 | import io.r2dbc.spi.Result; 26 | import io.r2dbc.spi.Statement; 27 | import io.r2dbc.spi.Type; 28 | import oracle.sql.json.OracleJsonObject; 29 | 30 | import java.nio.ByteBuffer; 31 | import java.sql.RowId; 32 | import java.time.Duration; 33 | import java.time.LocalDateTime; 34 | import java.time.Period; 35 | import java.util.Objects; 36 | 37 | /** 38 | * SQL types supported by Oracle Database that are not defined as standard types 39 | * by {@link io.r2dbc.spi.R2dbcType}. 40 | */ 41 | public final class OracleR2dbcTypes { 42 | 43 | private OracleR2dbcTypes() {} 44 | 45 | /** 46 | * A 64-bit, double-precision floating-point number data type. 47 | */ 48 | public static final Type BINARY_DOUBLE = 49 | new TypeImpl(Double.class, "BINARY_DOUBLE"); 50 | 51 | /** 52 | * A 32-bit, single-precision floating-point number data type. 53 | */ 54 | public static final Type BINARY_FLOAT = 55 | new TypeImpl(Float.class, "BINARY_FLOAT"); 56 | 57 | /** 58 | * Stores a period of time in days, hours, minutes, and seconds. 59 | */ 60 | public static final Type INTERVAL_DAY_TO_SECOND = 61 | new TypeImpl(Duration.class, "INTERVAL DAY TO SECOND"); 62 | 63 | /** 64 | * Stores a period of time in years and months. 65 | */ 66 | public static final Type INTERVAL_YEAR_TO_MONTH = 67 | new TypeImpl(Period.class, "INTERVAL YEAR TO MONTH"); 68 | 69 | /** 70 | * Stores a JSON value. 71 | */ 72 | public static final Type JSON = 73 | new TypeImpl(OracleJsonObject.class, "JSON"); 74 | 75 | /** 76 | * Character data of variable length up to 2 gigabytes. 77 | */ 78 | public static final Type LONG = 79 | new TypeImpl(String.class, "LONG"); 80 | 81 | /** 82 | * Raw binary data of variable length up to 2 gigabytes. 83 | */ 84 | public static final Type LONG_RAW = 85 | new TypeImpl(ByteBuffer.class, "LONG RAW"); 86 | 87 | /** 88 | * Base 64 string representing the unique address of a row in its table. 89 | */ 90 | public static final Type ROWID = 91 | new TypeImpl(RowId.class, "ROWID"); 92 | 93 | /** 94 | * Timestamp that is converted to the database's timezone when stored, and 95 | * converted to the local timezone (the session timezone) when retrieved. 96 | */ 97 | public static final Type TIMESTAMP_WITH_LOCAL_TIME_ZONE = 98 | new TypeImpl(LocalDateTime.class, "TIMESTAMP WITH LOCAL TIME ZONE"); 99 | 100 | /** 101 | * A cursor that is returned by a procedural call. 102 | */ 103 | public static final Type REF_CURSOR = 104 | new TypeImpl(Result.class, "SYS_REFCURSOR"); 105 | 106 | /** 107 | * A vector of 64-bit floating point numbers, 32-bit floating point numbers, 108 | * or 8-bit signed integers. Maps to double[] by default, as a 109 | * double can store all the possible number formats without 110 | * losing information. 111 | */ 112 | public static final Type VECTOR = 113 | new TypeImpl(oracle.sql.VECTOR.class, "VECTOR"); 114 | 115 | /** 116 | *

117 | * Creates an {@link ArrayType} representing a user defined {@code ARRAY} 118 | * type. The {@code name} passed to this method must identify the name of a 119 | * user defined {@code ARRAY} type. 120 | *

121 | * Typically, the name passed to this method should be UPPER CASE, unless the 122 | * {@code CREATE TYPE} command that created the type used an "enquoted" type 123 | * name. 124 | *

125 | * The {@code ArrayType} object returned by this method may be used to create 126 | * a {@link Parameter} that binds an array value to a {@link Statement}: 127 | *

{@code
128 |    * Publisher arrayBindExample(Connection connection) {
129 |    *   Statement statement =
130 |    *     connection.createStatement("INSERT INTO example VALUES (:array_bind)");
131 |    *
132 |    *   // Use the name defined for an ARRAY type:
133 |    *   // CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER
134 |    *   ArrayType arrayType = OracleR2dbcTypes.arrayType("MY_ARRAY");
135 |    *   Integer[] arrayValues = {1, 2, 3};
136 |    *   statement.bind("arrayBind", Parameters.in(arrayType, arrayValues));
137 |    *
138 |    *   return statement.execute();
139 |    * }
140 |    * }
141 | * @param name Name of a user defined ARRAY type. Not null. 142 | * @return A {@code Type} object representing the user defined ARRAY type. Not 143 | * null. 144 | */ 145 | public static ArrayType arrayType(String name) { 146 | return new ArrayTypeImpl(Objects.requireNonNull(name, "name is null")); 147 | } 148 | 149 | /** 150 | *

151 | * Creates an {@link ObjectType} representing a user defined {@code OBJECT} 152 | * type. The {@code name} passed to this method must identify the name of a 153 | * user defined {@code OBJECT} type. 154 | *

155 | * Typically, the name passed to this method should be UPPER CASE, unless the 156 | * {@code CREATE TYPE} command that created the type used an "enquoted" type 157 | * name. 158 | *

159 | * The {@code ObjectType} object returned by this method may be used to create 160 | * a {@link Parameter} that binds an OBJECT value to a {@link Statement}: 161 | *

{@code
162 |    * Publisher objectMapBindExample(Connection connection) {
163 |    *   Statement statement =
164 |    *     connection.createStatement("INSERT INTO petTable VALUES (:petObject)");
165 |    * 
166 |    *   // Bind the attributes of the PET OBJECT defined above
167 |    *   ObjectType objectType = OracleR2dbcTypes.objectType("PET");
168 |    *   Map attributeValues = Map.of(
169 |    *     "name", "Derby",
170 |    *     "species", "Dog",
171 |    *     "weight", 22.8,
172 |    *     "birthday", LocalDate.of(2015, 11, 07));
173 |    *   statement.bind("petObject", Parameters.in(objectType, attributeValues));
174 |    * 
175 |    *   return statement.execute();
176 |    * }
177 |    * }
178 | * @param name Name of a user defined OBJECT type. Not null. 179 | * @return A {@code Type} object representing the user defined OBJECT type. 180 | * Not null. 181 | */ 182 | public static ObjectType objectType(String name) { 183 | return new ObjectTypeImpl(Objects.requireNonNull(name, "name is null")); 184 | } 185 | 186 | /** 187 | * Extension of the standard {@link Type} interface used to represent user 188 | * defined ARRAY types. An instance of {@code ArrayType} must be used when 189 | * binding an array value to a {@link Statement} created by the Oracle R2DBC 190 | * Driver. 191 | *

192 | * Oracle Database does not support an anonymous {@code ARRAY} type, which is 193 | * what the standard {@link R2dbcType#COLLECTION} type represents. Oracle 194 | * Database only supports {@code ARRAY} types which are declared as a user 195 | * defined type, as in: 196 | *

{@code
197 |    * CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER
198 |    * }
199 | * In order to bind an array, the name of a user defined ARRAY type must 200 | * be known to Oracle R2DBC. Instances of {@code ArrayType} retain the name 201 | * that is provided to the {@link #arrayType(String)} factory method. 202 | */ 203 | public interface ArrayType extends Type { 204 | 205 | /** 206 | * {@inheritDoc} 207 | * Returns {@code Object[].class}, which is the standard mapping for 208 | * {@link R2dbcType#COLLECTION}. The true default type mapping is the array 209 | * variant of the default mapping for the element type of the {@code ARRAY}. 210 | * For instance, an {@code ARRAY} of {@code VARCHAR} maps to a 211 | * {@code String[]} by default. 212 | */ 213 | @Override 214 | Class getJavaType(); 215 | 216 | /** 217 | * {@inheritDoc} 218 | * Returns the name of this user defined {@code ARRAY} type. For instance, 219 | * this method returns "MY_ARRAY" if the type is declared as: 220 | *
{@code
221 |      * CREATE TYPE MY_ARRAY AS ARRAY(8) OF NUMBER
222 |      * }
223 | */ 224 | @Override 225 | String getName(); 226 | } 227 | 228 | /** 229 | * Extension of the standard {@link Type} interface used to represent user 230 | * defined OBJECT types. An instance of {@code ObjectType} must be used when 231 | * binding an OBJECT value to a {@link Statement} created by the Oracle R2DBC 232 | * Driver. 233 | */ 234 | public interface ObjectType extends Type { 235 | 236 | /** 237 | * {@inheritDoc} 238 | * Returns the class of {@link OracleR2dbcObject}, which is the default mapping 239 | * of OBJECT types returned by Oracle R2DBC. 240 | */ 241 | @Override 242 | Class getJavaType(); 243 | 244 | /** 245 | * {@inheritDoc} 246 | * Returns the name of this user defined {@code OBJECT} type. For instance, 247 | * this method returns "PET" if the type is declared as: 248 | *
{@code
249 |      * CREATE TYPE PET AS OBJECT(
250 |      *   name VARCHAR(128),
251 |      *   species VARCHAR(128),
252 |      *   weight NUMBER,
253 |      *   birthday DATE)
254 |      * }
255 | */ 256 | @Override 257 | String getName(); 258 | } 259 | 260 | /** Concrete implementation of the {@code ArrayType} interface */ 261 | private static final class ArrayTypeImpl 262 | extends TypeImpl implements ArrayType { 263 | 264 | /** 265 | * Constructs an ARRAY type with the given {@code name}. {@code Object[]} is 266 | * the default mapping of the constructed type. This is consistent with the 267 | * standard {@link R2dbcType#COLLECTION} type. 268 | * @param name User defined name of the type. Not null. 269 | */ 270 | ArrayTypeImpl(String name) { 271 | super(Object[].class, name); 272 | } 273 | } 274 | 275 | /** Concrete implementation of the {@code ObjectType} interface */ 276 | private static final class ObjectTypeImpl 277 | extends TypeImpl implements ObjectType { 278 | 279 | /** 280 | * Constructs an OBJECT type with the given {@code name}. 281 | * {@code OracleR2dbcObject} is the default mapping of the constructed type. 282 | * @param name User defined name of the type. Not null. 283 | */ 284 | ObjectTypeImpl(String name) { 285 | super(OracleR2dbcObject.class, name); 286 | } 287 | } 288 | 289 | /** 290 | * Implementation of the {@link Type} SPI. 291 | */ 292 | private static class TypeImpl implements Type { 293 | 294 | /** 295 | * The Java Language mapping of this SQL type. 296 | */ 297 | private final Class javaType; 298 | 299 | /** 300 | * The name of this SQL type, as it would appear in a DDL expression. 301 | */ 302 | private final String sqlName; 303 | 304 | /** 305 | * Constructs a {@code Type} having a {@code javaType} mapping and 306 | * {@code sqlName}. 307 | * @param javaType Java type 308 | * @param sqlName SQL type name 309 | */ 310 | TypeImpl(Class javaType, String sqlName) { 311 | this.javaType = javaType; 312 | this.sqlName = sqlName; 313 | } 314 | 315 | /** 316 | * {@inheritDoc} 317 | *

318 | * Implements the R2DBC SPI method by returning the default Java type 319 | * mapping for values of this SQL type. The Java type returned by this 320 | * method is the type of {@code Object} returned by {@code Row.get 321 | * (String/int)} when accessing a value of this SQL type. 322 | *

323 | */ 324 | @Override 325 | public Class getJavaType() { 326 | return javaType; 327 | } 328 | 329 | /** 330 | * {@inheritDoc} 331 | *

332 | * Implements the R2DBC SPI method by returning the name of this SQL type. 333 | * The name returned by this method is recognized in expressions of a SQL 334 | * command, for instance: A column definition of a {@code CREATE TABLE} 335 | * command. 336 | *

337 | * 338 | * @return 339 | */ 340 | @Override 341 | public String getName() { 342 | return sqlName; 343 | } 344 | 345 | /** 346 | * Returns the name of this type. 347 | * @return Type name 348 | */ 349 | @Override 350 | public String toString() { 351 | return getName(); 352 | } 353 | 354 | @Override 355 | public boolean equals(Object other) { 356 | if (! (other instanceof Type)) 357 | return false; 358 | 359 | return sqlName.equals(((Type)other).getName()); 360 | } 361 | 362 | @Override 363 | public int hashCode() { 364 | return sqlName.hashCode(); 365 | } 366 | } 367 | 368 | } 369 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/OracleR2dbcWarning.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc; 22 | 23 | import io.r2dbc.spi.Result; 24 | 25 | import java.util.function.Function; 26 | import java.util.function.Predicate; 27 | 28 | /** 29 | *

30 | * A subtype of {@link Result.Message} that provides information on warnings 31 | * raised by Oracle Database. 32 | *

33 | * When a SQL command results in a warning, Oracle R2DBC emits a {@link Result} 34 | * with an {@code OracleR2dbcWarning} segment in addition to any other segments 35 | * that resulted from the SQL command. For example, if a SQL {@code SELECT} 36 | * command results in a warning, then an {@code OracleR2dbcWarning} segment is 37 | * included with the result, along with any {@link Result.RowSegment}s returned 38 | * by the {@code SELECT}. 39 | *

40 | * R2DBC drivers typically emit {@code onError} signals for {@code Message} 41 | * segments that are not consumed by {@link Result#filter(Predicate)} or 42 | * {@link Result#flatMap(Function)}. Oracle R2DBC does not apply this behavior 43 | * for warning messages. If an {@code OracleR2dbcWarning} 44 | * segment is not consumed by the {@code filter} or {@code flatMap} methods of 45 | * a {@code Result}, then the warning is discarded and the result may be 46 | * consumed as normal with with the {@code map} or {@code getRowsUpdated} 47 | * methods. 48 | *

49 | * Warning messages may be consumed with {@link Result#flatMap(Function)}: 50 | *

{@code
51 |  * result.flatMap(segment -> {
52 |  *   if (segment instanceof OracleR2dbcWarning) {
53 |  *     logWarning(((OracleR2dbcWarning)segment).getMessage());
54 |  *     return emptyPublisher();
55 |  *   }
56 |  *   else {
57 |  *     ... handle other segment types ...
58 |  *   }
59 |  * })
60 |  * }

61 | * A {@code flatMap} function may also be used to convert a warning into an 62 | * {@code onError} signal: 63 | *

{@code
64 |  * result.flatMap(segment -> {
65 |  *   if (segment instanceof OracleR2dbcWarning) {
66 |  *     return errorPublisher(((OracleR2dbcWarning)segment).warning());
67 |  *   }
68 |  *   else {
69 |  *     ... handle other segment types ...
70 |  *   }
71 |  * })
72 |  * }
73 | * @since 1.1.0 74 | */ 75 | public interface OracleR2dbcWarning extends Result.Message { 76 | 77 | } -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/AsyncLock.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc.impl; 22 | 23 | import oracle.r2dbc.impl.OracleR2dbcExceptions.JdbcRunnable; 24 | import oracle.r2dbc.impl.OracleR2dbcExceptions.JdbcSupplier; 25 | import org.reactivestreams.Publisher; 26 | import org.reactivestreams.Subscriber; 27 | import org.reactivestreams.Subscription; 28 | 29 | /** 30 | *

31 | * A lock that is acquired and unlocked asynchronously. An instance of this lock 32 | * is used to guard access to the Oracle JDBC Connection, without blocking 33 | * threads that contend for it. 34 | *

35 | * Since the 23.1 release, Oracle JDBC no longer blocks threads during 36 | * asynchronous calls. The remainder of this JavaDoc describes behavior which 37 | * was present in 21.x releases of Oracle JDBC. If a 23.1 or newer version 38 | * of Oracle JDBC is installed, then Oracle R2DBC will use the 39 | * {@link NoOpAsyncLock} rather than {@link AsyncLockImpl}. 40 | *

41 | * Any time Oracle R2DBC invokes a synchronous API of Oracle JDBC, it will 42 | * acquire an instance of this lock before doing so. Synchronous method calls 43 | * will block a thread if JDBC has a database call in progress, and this can 44 | * lead a starvation of the thread pool. If no pooled threads are available, 45 | * then JDBC can not execute callbacks that complete the database call, and 46 | * so JDBC will never release it's blocking lock. 47 | *

48 | * Any time Oracle R2DBC creates a publisher that is implemented by the 49 | * Oracle JDBC driver, it will acquire an instance of this lock before 50 | * subscribing to that publisher. The lock is only released if it is known 51 | * that neither onNext nor onSubscribe signals are pending. If these signals 52 | * are not pending, then the driver should not be executing any database 53 | * call; As long as no database call is in progress, JDBC should not block 54 | * threads that call any method of its API. 55 | *

56 | *

Locking for Asynchronous JDBC Calls

57 | *

58 | * Wrapping a JDBC Publisher with {@link #lock(Publisher)} will have signals 59 | * from a downstream subscriber proxied, such that the lock is held whenever 60 | * {@code onSubscribe} or {@code onNext} signals are pending. 61 | *

62 | * Invoking {@link #lock(Runnable)} will have a {@code Runnable} executed 63 | * after the lock becomes available. The {@code Runnable} is executed 64 | * immediately, before {@code lock(Runnable)} returns if the lock is 65 | * available. Otherwise, the {@code Runnable} is executed asynchronously 66 | * after the lock becomes available. 67 | *

68 | *

Locking for Synchronous JDBC Calls

69 | *

70 | * If the lock simply needs to be acquired before making a synchronous call, 71 | * and then released after that call returns, then methods 72 | * {@link #run(JdbcRunnable)}, {@link #get(JdbcSupplier)}, or 73 | * {@link #flatMap(JdbcSupplier)} may be used. These methods return a 74 | * {@code Publisher} that completes after the lock is acquired and the provided 75 | * task has been run. These methods will automatically release the lock after 76 | * the provided task has run. 77 | *

78 | * Rather than invoke the {@code get/run} methods for each and every JDBC 79 | * method call, it is preferable to invoke them with a single task that 80 | * performs many synchronous JDBC API calls. This will reduce the computational 81 | * and memory costs of creating and subscribing to publishers. It will also 82 | * allow most of the code base to be written in a synchronous style, with the 83 | * assumption that it is executing within a task provided to the {@code get/run} 84 | * methods. 85 | *

86 | */ 87 | interface AsyncLock { 88 | 89 | /** 90 | * Acquires this lock, executes a {@code callback}, and then releases this 91 | * lock. The {@code callback} may be executed before this method returns if 92 | * this lock is currently available. Otherwise, the {@code callback} is 93 | * executed asynchronously after this lock becomes available. 94 | * 95 | * @param callback Task to be executed with lock ownership. Not null. 96 | */ 97 | void lock(Runnable callback); 98 | 99 | /** 100 | * Returns a {@code Publisher} that acquires this lock and executes a 101 | * {@code jdbcRunnable} when a subscriber subscribes. The {@code Publisher} 102 | * emits {@code onComplete} if the runnable completes normally, or emits 103 | * {@code onError} if the runnable throws an exception. 104 | * 105 | * @param jdbcRunnable Runnable to execute. Not null. 106 | * @return A publisher that emits the result of the {@code jdbcRunnable}. 107 | */ 108 | Publisher run(JdbcRunnable jdbcRunnable); 109 | 110 | /** 111 | * Returns a {@code Publisher} that acquires this lock and executes a 112 | * {@code jdbcSupplier} when a subscriber subscribes. The {@code Publisher} 113 | * emits {@code onNext} if the supplier returns a non-null value, and then 114 | * emits {@code onComplete}. Or, the {@code Publisher} emits {@code onError} 115 | * with any {@code Throwable} thrown by the supplier. 116 | * 117 | * @param The class of object output by the {@code jdbcSupplier} 118 | * @param jdbcSupplier Supplier to execute. Not null. 119 | * @return A publisher that emits the result of the {@code jdbcSupplier}. 120 | */ 121 | Publisher get(JdbcSupplier jdbcSupplier); 122 | 123 | /** 124 | * Returns a {@code Publisher} that acquires this lock and executes a 125 | * {@code publisherSupplier} when a subscriber subscribes. The 126 | * {@code Publisher} output by the {@code publisherSupplier} is flat mapped 127 | * into the {@code Publisher} returned by this method. If the supplier outputs 128 | * {@code null}, the returned publisher just emits {@code onComplete}. If the 129 | * supplier throws an error, the returned publisher emits that as 130 | * {@code onError}. 131 | * 132 | * @param The class of object emitted by the supplied publisher 133 | * @param publisherSupplier Supplier to execute. Not null. 134 | * @return A flat-mapping of the publisher output by the 135 | * {@code publisherSupplier}. 136 | */ 137 | Publisher flatMap(JdbcSupplier> publisherSupplier); 138 | 139 | /** 140 | * Returns a {@code Publisher} that proxies signals to and from a 141 | * provided {@code publisher} in order to guard access to the JDBC 142 | * {@code Connection} associated with this adapter. Invocations of 143 | * {@link Publisher#subscribe(Subscriber)} and 144 | * {@link Subscription#request(long)} will only occur when the JDBC connection 145 | * is not being used by another thread or another publisher. 146 | * 147 | * @param The class of object emitted by the {@code publisher} 148 | * @param publisher A publisher that uses the JDBC connection 149 | * @return A Publisher that 150 | */ 151 | Publisher lock(Publisher publisher); 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/DependentCounter.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc.impl; 22 | 23 | import org.reactivestreams.Publisher; 24 | import reactor.core.publisher.Mono; 25 | 26 | import java.util.concurrent.atomic.AtomicInteger; 27 | 28 | /** 29 | *

30 | * A count of resources that depend on another resource to remain open. A 31 | * dependent resource registers itself by incrementing the count, and 32 | * deregisters itself by decrementing the count. The last dependent to 33 | * deregister has the responsibility of subscribing to a {@code Publisher} that 34 | * closes the resource it depended upon. 35 | *

36 | * This class is conceptually similar to a {@code java.util.concurrent.Phaser}. 37 | * Parties register by calling {@link #increment()}, and deregister by calling 38 | * {@link #decrement()}. Asynchronous "phase advancement" is then handled by 39 | * the {@code Publisher} which {@code decrement} returns. 40 | *

41 | * This class offers a solution for tracking the consumption of 42 | * {@link io.r2dbc.spi.Result} objects that depend on a JDBC statement to remain 43 | * open until each result is consumed. Further explanations can be found in the 44 | * JavaDocs of {@link OracleStatementImpl} and {@link OracleResultImpl}. 45 | *

46 | */ 47 | class DependentCounter { 48 | 49 | /** Count of dependents */ 50 | private final AtomicInteger count = new AtomicInteger(0); 51 | 52 | /** Publisher that closes the depended upon resource */ 53 | private final Publisher closePublisher; 54 | 55 | /** 56 | * Constructs a new counter that returns a resource closing publisher to the 57 | * last dependent which unregisters. The counter is initialized with a count 58 | * of zero. 59 | * @param closePublisher Publisher that closes a resource. Not null. 60 | */ 61 | DependentCounter(Publisher closePublisher) { 62 | this.closePublisher = closePublisher; 63 | } 64 | 65 | /** 66 | * Increments the count of dependents by one. 67 | * 68 | * A corresponding call to {@link #decrement()} MUST occur by the dependent 69 | * which has called {@code increment()} 70 | * 71 | */ 72 | void increment() { 73 | count.incrementAndGet(); 74 | } 75 | 76 | /** 77 | *

78 | * Returns a publisher that decrements the count of dependents by one when 79 | * subscribed to. 80 | * 81 | * A corresponding call to {@link #increment()} MUST have previously occurred 82 | * by the dependent which has called {@code decrement()} 83 | * 84 | *

85 | * The dependent which has called this method MUST subscribe to the returned 86 | * published. If the dependent that calls this method is the last dependent to 87 | * do so, then the returned publisher will close the depended upon resource. 88 | * Otherwise, if more dependents remain, the returned publisher does nothing. 89 | * The caller of this method has no way to tell which is the case, so it must 90 | * subscribe to be safe. 91 | *

92 | * @return A publisher that closes the depended upon resource after no 93 | * dependents remain. Not null. 94 | */ 95 | Publisher decrement() { 96 | return Mono.defer(() -> 97 | count.decrementAndGet() == 0 98 | ? Mono.from(closePublisher) 99 | : Mono.empty()); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/Main.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.util.Objects; 27 | import java.util.jar.Manifest; 28 | 29 | /** 30 | *

31 | * A public class implementing a main method that may be executed from a 32 | * command line. This class is specified as the Main-Class attribute in 33 | * the META-INF/MANIFEST.MF of the Oracle R2DBC jar. 34 | *

35 | * The behavior implemented by this class may change between minor and patch 36 | * version release updates. 37 | *

38 | * The following command, in 39 | * which "x.y.z" is the semantic version number of the jar, 40 | * executes the main method of this class: 41 | *

42 |  *   java -jar oracle-r2dbc-x.y.z.jar
43 |  * 

44 | * Since version 0.1.1, the main method is implemented to exit after printing 45 | * a message to the standard output stream. The message includes the version 46 | * numbers of the Oracle R2DBC Driver along with the JDK that compiled it. A 47 | * timestamp captured at the moment when the jar was compiled is also included. 48 | *

49 | * 50 | * @since 0.1.1 51 | * @author Michael-A-McMahon 52 | */ 53 | public final class Main { 54 | 55 | private Main() {/*This class has no instance fields*/} 56 | 57 | /** 58 | * Prints information about this build of Oracle R2DBC. This method attempts 59 | * to read a "Build-Info" attribute from META-INF/MANIFEST.MF. If the 60 | * manifest is not available to the class loader, or if the manifest does 61 | * not contain a Build-Info attribute, then this method prints a message 62 | * indicating that build information can not be located. 63 | * @param args ignored 64 | * @throws IOException If the META-INF/MANIFEST.MF resource can not be read. 65 | */ 66 | public static void main(String[] args) throws IOException { 67 | 68 | InputStream manifestStream = 69 | Main.class.getModule().getResourceAsStream("META-INF/MANIFEST.MF"); 70 | 71 | if (manifestStream == null) { 72 | System.out.println("META-INF/MANIFEST.MF not found"); 73 | return; 74 | } 75 | 76 | try (manifestStream) { 77 | System.out.println(Objects.requireNonNullElse( 78 | new Manifest(manifestStream) 79 | .getMainAttributes() 80 | .getValue("Build-Info"), 81 | "Build-Info is missing from the manifest")); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/NoOpAsyncLock.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import org.reactivestreams.Publisher; 25 | import reactor.core.publisher.Flux; 26 | import reactor.core.publisher.Mono; 27 | 28 | /** 29 | * A no-op implementation of {@link AsyncLock} for use with 23.1 and newer 30 | * versions of Oracle JDBC. All methods are implemented by immediately executing 31 | * operations without acquiring a lock. 32 | */ 33 | final class NoOpAsyncLock implements AsyncLock { 34 | 35 | @Override 36 | public void lock(Runnable callback) { 37 | callback.run(); 38 | } 39 | 40 | @Override 41 | public Publisher run(OracleR2dbcExceptions.JdbcRunnable jdbcRunnable) { 42 | return Mono.fromRunnable(jdbcRunnable); 43 | } 44 | 45 | @Override 46 | public Publisher get( 47 | OracleR2dbcExceptions.JdbcSupplier jdbcSupplier) { 48 | return Mono.fromSupplier(jdbcSupplier); 49 | } 50 | 51 | @Override 52 | public Publisher flatMap( 53 | OracleR2dbcExceptions.JdbcSupplier> publisherSupplier) { 54 | return Flux.defer(publisherSupplier); 55 | } 56 | 57 | @Override 58 | public Publisher lock(Publisher publisher) { 59 | return publisher; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.Batch; 25 | import io.r2dbc.spi.R2dbcException; 26 | import org.reactivestreams.Publisher; 27 | import reactor.core.publisher.Flux; 28 | 29 | import java.sql.Connection; 30 | import java.time.Duration; 31 | import java.util.LinkedList; 32 | import java.util.Queue; 33 | 34 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; 35 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireOpenConnection; 36 | 37 | /** 38 | *

39 | * Implementation of the {@link Batch} SPI for Oracle Database. This SPI 40 | * implementation executes an ordered sequence of arbitrary SQL statements 41 | * using a JDBC connection. JDBC API calls are adapted into Reactive Streams 42 | * APIs using a {@link ReactiveJdbcAdapter}. 43 | *

44 | * Oracle Database supports batch execution of parameterized DML statements, 45 | * but does not support batch execution of arbitrary SQL statements. This 46 | * implementation reflects the capabilities of Oracle Database; It does not 47 | * offer any performance benefit compared to individually executing each 48 | * statement in the batch. 49 | *

50 | * 51 | * @author harayuanwang, michael-a-mcmahon 52 | * @since 0.1.0 53 | */ 54 | final class OracleBatchImpl implements Batch { 55 | 56 | /** The OracleConnectionImpl that created this Batch */ 57 | private final OracleConnectionImpl r2dbcConnection; 58 | 59 | /** 60 | * JDBC connection to an Oracle Database which executes this batch. 61 | */ 62 | private final Connection jdbcConnection; 63 | 64 | /** 65 | * Timeout applied to each statement this {@code Batch} executes; 66 | */ 67 | private final Duration timeout; 68 | 69 | /** 70 | * Ordered sequence of SQL commands that have been added to this batch. May 71 | * be empty. 72 | */ 73 | private Queue statements = new LinkedList<>(); 74 | 75 | /** 76 | * Constructs a new batch that uses the specified {@code adapter} to execute 77 | * SQL statements with a {@code jdbcConnection}. 78 | * @param timeout Timeout applied to each statement this batch executes. 79 | * Not null. Not negative. 80 | * @param r2dbcConnection R2DBC connection that created this batch. Not null. 81 | */ 82 | OracleBatchImpl(Duration timeout, OracleConnectionImpl r2dbcConnection) { 83 | this.timeout = timeout; 84 | this.r2dbcConnection = r2dbcConnection; 85 | this.jdbcConnection = r2dbcConnection.jdbcConnection(); 86 | } 87 | 88 | /** 89 | * {@inheritDoc} 90 | *

91 | * Implements the R2DBC SPI method by adding a {@code sql} command to the 92 | * end of the command sequence of the current batch. 93 | *

94 | */ 95 | @Override 96 | public Batch add(String sql) { 97 | requireOpenConnection(jdbcConnection); 98 | requireNonNull(sql, "sql is null"); 99 | statements.add( 100 | new OracleStatementImpl(sql, timeout, r2dbcConnection)); 101 | return this; 102 | } 103 | 104 | /** 105 | * {@inheritDoc} 106 | *

107 | * Implements the R2DBC SPI method by executing the SQL statements that have 108 | * been added to the current batch since the previous execution. Statements 109 | * are executed in the order they were added. Calling this method clears all 110 | * statements that have been added to the current batch. 111 | *

112 | * If the execution of any statement in the sequence results in a failure, 113 | * then the returned publisher emits {@code onError} with an 114 | * {@link R2dbcException} that describes the failure, and all subsequent 115 | * statements in the sequence are not executed. 116 | *

117 | * The returned publisher begins executing the batch after a 118 | * subscriber subscribes, before the subscriber emits a {@code 119 | * request} signal. The returned publisher does not support multiple 120 | * subscribers. After one subscriber has subscribed, the returned publisher 121 | * signals {@code onError} with {@code IllegalStateException} to any 122 | * subsequent subscribers. 123 | *

124 | */ 125 | @Override 126 | public Publisher execute() { 127 | requireOpenConnection(jdbcConnection); 128 | Queue currentStatements = statements; 129 | statements = new LinkedList<>(); 130 | return Flux.fromIterable(currentStatements) 131 | .flatMapSequential(OracleStatementImpl::execute); 132 | } 133 | 134 | } 135 | 136 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/OracleConnectionFactoryImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.Connection; 25 | import io.r2dbc.spi.ConnectionFactory; 26 | import io.r2dbc.spi.ConnectionFactoryMetadata; 27 | import io.r2dbc.spi.ConnectionFactoryOptions; 28 | import io.r2dbc.spi.IsolationLevel; 29 | import io.r2dbc.spi.R2dbcException; 30 | import io.r2dbc.spi.Statement; 31 | import oracle.r2dbc.OracleR2dbcOptions; 32 | import org.reactivestreams.Publisher; 33 | import reactor.core.publisher.Mono; 34 | 35 | import javax.sql.DataSource; 36 | import java.time.Duration; 37 | import java.util.Optional; 38 | import java.util.concurrent.Executor; 39 | import java.util.concurrent.ForkJoinPool; 40 | 41 | /** 42 | *

43 | * Implementation of the {@link ConnectionFactory} SPI for Oracle Database. 44 | *

45 | * Instances of this class open database connections using a JDBC 46 | * {@link javax.sql.DataSource}. JDBC API calls are adapted into Reactive 47 | * Streams APIs using a {@link ReactiveJdbcAdapter}. 48 | *

49 | * {@code Connections} created by this factory are initially 50 | * configured with {@linkplain Connection#isAutoCommit() auto-commit} mode 51 | * enabled and have a {@linkplain Connection#getTransactionIsolationLevel() 52 | * transaction isolation level} of {@linkplain IsolationLevel#READ_COMMITTED 53 | * READ_COMMITTED}. 54 | *

55 | *

Required Options

56 | * This implementation requires the following options for connection creation: 57 | *

58 | *
{@link ConnectionFactoryOptions#DRIVER}
59 | *
Must have the value "oracle"
60 | *
{@link ConnectionFactoryOptions#HOST}
61 | *
IP address or hostname of an Oracle Database
62 | *
63 | *

Supported Options

64 | * This implementation supports the following options for connection creation: 65 | *

66 | *
{@link ConnectionFactoryOptions#PORT}
67 | *
Port number of an Oracle Database
68 | *
{@link ConnectionFactoryOptions#DATABASE}
69 | *
Service name (not an SID) of an Oracle Database
70 | *
{@link ConnectionFactoryOptions#USER}
71 | *
Name of an Oracle Database user
72 | *
{@link ConnectionFactoryOptions#PASSWORD}
73 | *
74 | * Password of an Oracle Database user. The value may be an instance 75 | * of a mutable {@link CharSequence}, such {@link java.nio.CharBuffer}, 76 | * that may be cleared after creating an instance of 77 | * {@code OracleConnectionFactoryImpl}. 78 | *
79 | *
{@link ConnectionFactoryOptions#CONNECT_TIMEOUT}
80 | *
81 | * Maximum wait time when requesting a {@code Connection}. If the 82 | * duration expires, a {@code Connection} {@code Subscriber} receives 83 | * {@code onError} with an {@link io.r2dbc.spi.R2dbcTimeoutException}. 84 | * The duration is rounded up to the nearest whole second. The query 85 | * section of an R2DBC URL may provide a value in the format specified by 86 | * {@link Duration#parse(CharSequence)}. 87 | *
88 | *
{@link ConnectionFactoryOptions#SSL}
89 | *
90 | * If set to {@code true}, the driver connects to Oracle Database using 91 | * TCPS (ie: SSL/TLS). 92 | *
93 | *
94 | *

Supported Options

95 | * This implementation supports extended options having the name of a 96 | * subset of Oracle JDBC connection properties. The list of supported 97 | * connection properties is specified by {@link OracleReactiveJdbcAdapter}. 98 | *

99 | * 100 | * @author harayuanwang, michael-a-mcmahon 101 | * @since 0.1.0 102 | */ 103 | final class OracleConnectionFactoryImpl implements ConnectionFactory { 104 | 105 | /** 106 | *

107 | * The default executor when {@link OracleR2dbcOptions#EXECUTOR} is not 108 | * configured. It will use the common {@code ForkJoinPool}, unless it has 109 | * a maximum pool size of 0. See: 110 | * https://github.com/oracle/oracle-r2dbc/issues/129 111 | *

112 | */ 113 | private static final Executor DEFAULT_EXECUTOR = 114 | "0".equals(System.getProperty( 115 | "java.util.concurrent.ForkJoinPool.common.parallelism")) 116 | ? new ForkJoinPool(1) 117 | : ForkJoinPool.commonPool(); 118 | 119 | /** JDBC data source that this factory uses to open connections */ 120 | private final DataSource dataSource; 121 | 122 | /** 123 | * Executor configured by {@link oracle.r2dbc.OracleR2dbcOptions#EXECUTOR}, 124 | * or a default one if none was configured. 125 | */ 126 | private final Executor executor; 127 | 128 | /** 129 | *

130 | * Timeout applied to the execution of {@link Statement}s created by 131 | * {@link Connection}s created by this {@code ConnectionFactory}. 132 | *

133 | * The {@link #dataSource} is not configured with this value because Oracle 134 | * JDBC does not have a connection property to set a statement execution 135 | * timeout. This value is retained by an instance of 136 | * {@code OracleConnectionFactoryImpl} so that it may be applied to each 137 | * {@code Connection} it creates. 138 | *

139 | */ 140 | private final Duration statementTimeout; 141 | 142 | /** 143 | *

144 | * Constructs a new factory that applies the values specified by the {@code 145 | * options} parameter when opening a database connection. This constructor 146 | * fails if any required options 147 | * are not specified by the {@code options} parameter. 148 | *

149 | * Where curly brackets {enclose} the name of a required {@code Option}, 150 | * and angle brackets [enclose] the name of an optional {@code Option}, the 151 | * values specified by the {@code options} parameter are used to compose a 152 | * JDBC URL for database connectivity as: 153 | *

154 |    * jdbc:oracle:thin:@{HOST}[:PORT][/DATABASE]
155 |    * 

156 | * Note that the syntax used is {@code /{DATABASE}} and not 157 | * {@code :{DATABASE}}. This forward slash syntax has the {@code DATABASE} 158 | * option interpreted as a service name. The {@code DATABASE} option is not 159 | * interpreted as a system ID (SID) using the colon syntax. 160 | *

161 | * Traditional database authentication is configured by option values for 162 | * {@link ConnectionFactoryOptions#USER} with 163 | * {@link ConnectionFactoryOptions#PASSWORD}. These options are not 164 | * required because Oracle Database supports alternative methods of 165 | * authentication that do not require a user name and password, such as 166 | * trusted TLS certificates. 167 | *

168 | * Well-known options {@link ConnectionFactoryOptions#CONNECT_TIMEOUT} and 169 | * {@link ConnectionFactoryOptions#SSL} are supported. 170 | *

171 | * Any extended options are applied as Oracle JDBC connection properties. 172 | * An extended option is any option that is not declared by 173 | * {@link ConnectionFactoryOptions}. See 174 | * {@link OracleReactiveJdbcAdapter#createDataSource(ConnectionFactoryOptions)} 175 | * for a list of Oracle JDBC connection properties that are supported. 176 | *

177 | * 178 | * @param options Options applied when opening a connection to a database. 179 | * 180 | * @throws IllegalArgumentException If the value of a required option is 181 | * null. 182 | * 183 | * @throws IllegalStateException If the value of a required option is not 184 | * specified. 185 | * 186 | * @throws IllegalArgumentException If the {@code oracleNetDescriptor} 187 | * {@code Option} is provided with any other options that might have 188 | * conflicting values, such as {@link ConnectionFactoryOptions#HOST}. 189 | * 190 | * @throws IllegalArgumentException If the 191 | * {@link ConnectionFactoryOptions#STATEMENT_TIMEOUT} {@code Option} specifies 192 | * a negative {@code Duration} 193 | */ 194 | OracleConnectionFactoryImpl(ConnectionFactoryOptions options) { 195 | OracleR2dbcExceptions.requireNonNull(options, "options is null."); 196 | dataSource = ReactiveJdbcAdapter.getOracleAdapter() 197 | .createDataSource(options); 198 | 199 | // Handle any Options that Oracle JDBC doesn't 200 | if (options.hasOption(ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT)) { 201 | throw new UnsupportedOperationException( 202 | "Unsupported Option: " 203 | + ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT.name() 204 | + ". Oracle Database does not support a lock wait timeout session " + 205 | "parameter."); 206 | } 207 | 208 | statementTimeout = Optional.ofNullable( 209 | options.getValue(ConnectionFactoryOptions.STATEMENT_TIMEOUT)) 210 | .map(timeout -> (timeout instanceof Duration) 211 | ? (Duration)timeout 212 | : Duration.parse(timeout.toString())) 213 | .orElse(Duration.ZERO); 214 | 215 | Object executor = options.getValue(OracleR2dbcOptions.EXECUTOR); 216 | if (executor == null) { 217 | this.executor = DEFAULT_EXECUTOR; 218 | } 219 | else if (executor instanceof Executor) { 220 | this.executor = (Executor) executor; 221 | } 222 | else { 223 | throw new IllegalArgumentException( 224 | "Value of " + OracleR2dbcOptions.EXECUTOR 225 | + " is not an instance of Executor: " + executor.getClass()); 226 | } 227 | 228 | } 229 | 230 | /** 231 | * {@inheritDoc} 232 | *

233 | * Implements the R2DBC SPI method by opening a database connection using 234 | * the JDBC {@link DataSource} that this factory initialized when it was 235 | * constructed. 236 | *

237 | * The returned publisher does not attempt to open a JDBC connection until 238 | * a subscriber has signalled demand, and either emits a single connection or 239 | * emits {@code onError} with an {@link R2dbcException}. The returned 240 | * publisher releases any resources allocated to a JDBC connection if a 241 | * subscriber cancels it's subscription before the returned 242 | * publisher has emitted a connection. Subscribers MUST eventually 243 | * {@linkplain Connection#close() close} any connection that is emitted by 244 | * the returned publisher, so that the database can reclaim the resources 245 | * allocated for that connection. 246 | *

247 | * The returned publisher supports multiple subscribers. One {@code 248 | * Connection} is emitted to each subscriber that subscribes and signals 249 | * demand. 250 | *

251 | */ 252 | @Override 253 | public Publisher create() { 254 | return Mono.defer(() -> { 255 | 256 | // Create a new adapter for each connection. The adapter guards access 257 | // to a particular connection. 258 | ReactiveJdbcAdapter adapter = ReactiveJdbcAdapter.getOracleAdapter(); 259 | 260 | return Mono.fromDirect(adapter.publishConnection(dataSource, executor)) 261 | .flatMap(conn -> { 262 | OracleConnectionImpl connection = 263 | new OracleConnectionImpl(conn, adapter); 264 | 265 | return Mono.from(connection.setStatementTimeout(statementTimeout)) 266 | .thenReturn(connection); 267 | }); 268 | }); 269 | } 270 | 271 | /** 272 | * {@inheritDoc} 273 | *

274 | * Implements the R2DBC SPI method by returning an implementation of the 275 | * {@code ConnectionFactoryMetaData} SPI that names "Oracle Database" as the 276 | * database product that this factory's JDBC datasource can open 277 | * connections to. 278 | *

279 | */ 280 | @Override 281 | public ConnectionFactoryMetadata getMetadata() { 282 | return OracleConnectionFactoryMetadataImpl.INSTANCE; 283 | } 284 | 285 | } 286 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/OracleConnectionFactoryMetadataImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.ConnectionFactoryMetadata; 25 | 26 | /** 27 | * Implementation of {@code ConnectionFactoryMetaData} which names 28 | * "Oracle Database" as the database product that a 29 | * {@link io.r2dbc.spi.ConnectionFactory} connects to. 30 | */ 31 | final class OracleConnectionFactoryMetadataImpl 32 | implements ConnectionFactoryMetadata { 33 | 34 | static final OracleConnectionFactoryMetadataImpl INSTANCE = 35 | new OracleConnectionFactoryMetadataImpl(); 36 | 37 | private OracleConnectionFactoryMetadataImpl() {} 38 | 39 | @Override 40 | public String getName() { 41 | return "Oracle Database"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/OracleConnectionFactoryProviderImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.ConnectionFactory; 25 | import io.r2dbc.spi.ConnectionFactoryOptions; 26 | import io.r2dbc.spi.ConnectionFactoryProvider; 27 | 28 | import java.util.ServiceLoader; 29 | 30 | import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; 31 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; 32 | 33 | /** 34 | *

35 | * Implementation of the {@link ConnectionFactoryProvider} SPI for the Oracle 36 | * Database. This provider 37 | * {@linkplain #supports(ConnectionFactoryOptions) supports} 38 | * {@link ConnectionFactoryOptions} that specify {@code DRIVER} as the value 39 | * {@code "oracle"}. 40 | *

41 | * The Oracle R2DBC Driver JAR identifies this class as a service provider for 42 | * the R2DBC SPI. Identification is specified with a 43 | * provider-configuration-file included under {@code META-INF/services}. A 44 | * {@link ServiceLoader} uses the provider-configuration-file to locate this 45 | * class. 46 | *

47 | * 48 | * @author harayuanwang, michael-a-mcmahon 49 | * @since 0.1.0 50 | */ 51 | public final class OracleConnectionFactoryProviderImpl 52 | implements ConnectionFactoryProvider { 53 | 54 | /** 55 | * Identifier of the Oracle R2DBC Driver that can appear in a R2DBC URL or 56 | * connection factory options. 57 | */ 58 | private static final String DRIVER_IDENTIFIER = "oracle"; 59 | 60 | /** 61 | *

62 | * Constructs a new connection factory provider. 63 | *

64 | * This public no-arg constructor is explicitly declared in conformance 65 | * with the discovery mechanism described in 66 | * 67 | * Section 5.2 of the 0.8.2 R2DBC Specification 68 | * 69 | *

70 | * 71 | * This constructor is not supported for general application programming. 72 | * 73 | * Application programmers should use the 74 | * {@link io.r2dbc.spi.ConnectionFactories} APIs to obtain an instance of 75 | * the Oracle R2DBC {@link ConnectionFactory}. 76 | *

77 | */ 78 | public OracleConnectionFactoryProviderImpl() { } 79 | 80 | /** 81 | * {@inheritDoc} 82 | *

83 | * Implements the R2DBC SPI method by returning a new 84 | * {@code ConnectionFactory} that applies values specified by the {@code 85 | * options} parameter, when opening connections to an Oracle Database. 86 | *

87 | * 88 | * @throws IllegalStateException If any option required by 89 | * {@link OracleConnectionFactoryImpl} is not specified by {@code options}. 90 | * 91 | * @throws IllegalArgumentException If the {@code oracleNetDescriptor} 92 | * {@code Option} is provided with any other options that might have 93 | * conflicting values, such as {@link ConnectionFactoryOptions#HOST}. 94 | */ 95 | @Override 96 | public ConnectionFactory create(ConnectionFactoryOptions options) { 97 | assert supports(options) : "Options are not supported: " + options; 98 | requireNonNull(options, "options must not be null."); 99 | 100 | if (SuppliedOptionConnectionFactory.containsSuppliedValue(options)) 101 | return new SuppliedOptionConnectionFactory(options); 102 | else 103 | return new OracleConnectionFactoryImpl(options); 104 | } 105 | 106 | /** 107 | * {@inheritDoc} 108 | *

109 | * Implements the R2DBC SPI method by returning {@code true} if the {@code 110 | * options} parameter specifies the value of {@code DRIVER} as "oracle". 111 | *

112 | */ 113 | @Override 114 | public boolean supports(ConnectionFactoryOptions options) { 115 | requireNonNull(options, "options must not be null."); 116 | return DRIVER_IDENTIFIER.equals(options.getValue(DRIVER)); 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | *

122 | * Implements the R2DBC SPI method by returning the identifier of the Oracle 123 | * R2DBC Driver, which is "oracle". 124 | *

125 | */ 126 | @Override 127 | public String getDriver() { 128 | return DRIVER_IDENTIFIER; 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/OracleConnectionMetadataImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import java.sql.DatabaseMetaData; 25 | import io.r2dbc.spi.ConnectionMetadata; 26 | 27 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.fromJdbc; 28 | 29 | 30 | /** 31 | *

32 | * Implementation of the {@link ConnectionMetadata} SPI for Oracle Database. 33 | *

34 | * Instances of this class supply metadata values of a JDBC 35 | * {@link DatabaseMetaData} object. 36 | *

37 | * 38 | * @author harayuanwang 39 | * @since 0.1.0 40 | */ 41 | final class OracleConnectionMetadataImpl implements ConnectionMetadata { 42 | 43 | /** Metadata from a JDBC connection */ 44 | private final DatabaseMetaData dbMetaData; 45 | 46 | /** 47 | * Constructs a new instance that supplies metadata from the specified 48 | * {@code dbMetaData}. 49 | * @param dbMetaData Metadata to supply. 50 | */ 51 | OracleConnectionMetadataImpl(DatabaseMetaData dbMetaData) { 52 | this.dbMetaData = dbMetaData; 53 | } 54 | 55 | /** 56 | * {@inheritDoc} 57 | *

58 | * Implements the R2DBC SPI method by returning the database product name 59 | * supplied by the JDBC {@code DatabaseMetaData}. 60 | *

61 | */ 62 | @Override 63 | public String getDatabaseProductName() { 64 | return fromJdbc(dbMetaData::getDatabaseProductName); 65 | } 66 | 67 | /** 68 | * {@inheritDoc} 69 | *

70 | * Implements the R2DBC SPI method by returning the database product version 71 | * supplied by the JDBC {@code DatabaseMetaData}. 72 | *

73 | */ 74 | @Override 75 | public String getDatabaseVersion() { 76 | return fromJdbc(dbMetaData::getDatabaseProductVersion); 77 | } 78 | } -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/OracleLargeObjects.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.Blob; 25 | import io.r2dbc.spi.Clob; 26 | import org.reactivestreams.Publisher; 27 | import reactor.core.publisher.Flux; 28 | import reactor.core.publisher.Mono; 29 | 30 | import java.nio.ByteBuffer; 31 | import java.util.Objects; 32 | import java.util.concurrent.atomic.AtomicBoolean; 33 | 34 | /** 35 | * Factory for implementations of the R2DBC {@link Blob} and {@link Clob} 36 | * SPIs. The Oracle R2DBC Driver uses this factory to produce column mappings 37 | * for the BLOB and CLOB database types. 38 | * @since 0.1.0 39 | * @author michael-a-mcmahon 40 | */ 41 | final class OracleLargeObjects { 42 | 43 | /** 44 | * This class has no instance methods, so this constructor should never be 45 | * called. 46 | */ 47 | private OracleLargeObjects(){ } 48 | 49 | /** 50 | * Creates a new {@code Blob} that streams binary data from a 51 | * {@code contentPublisher} and subscribes to a {@code releasePublisher}, 52 | * when the {@code Blob} is discarded. The {@code contentPublisher} is 53 | * subscribed to one or zero times. The {@code releasePublisher} must 54 | * support multiple subscribers, and must publish the same result to each 55 | * subscriber. 56 | * @param contentPublisher Publishes the contents of a BLOB 57 | * @param releasePublisher Publishes the result of releasing a BLOB. 58 | * @return A new {@code Blob} 59 | */ 60 | static Blob createBlob( 61 | Publisher contentPublisher, Publisher releasePublisher) { 62 | 63 | Publisher streamPublisher = 64 | createStreamPublisher(contentPublisher, releasePublisher); 65 | 66 | return new Blob() { 67 | @Override 68 | public Publisher stream() { 69 | return streamPublisher; 70 | } 71 | 72 | @Override 73 | public Publisher discard() { 74 | return releasePublisher; 75 | } 76 | }; 77 | } 78 | 79 | /** 80 | * Creates a new {@code Clob} that streams character data from a 81 | * {@code contentPublisher} and subscribes to a {@code releasePublisher}, 82 | * when the {@code Clob} is discarded. The {@code contentPublisher} is 83 | * subscribed to one or zero times. The {@code releasePublisher} must 84 | * support multiple subscribers, and must publish the same result to each 85 | * subscriber. 86 | * @param contentPublisher Publishes the contents of a CLOB 87 | * @param releasePublisher Publishes the result of releasing a CLOB. 88 | * @return A new {@code Clob} 89 | */ 90 | static Clob createClob( 91 | Publisher contentPublisher, 92 | Publisher releasePublisher) { 93 | 94 | Publisher streamPublisher = 95 | createStreamPublisher(contentPublisher, releasePublisher); 96 | 97 | return new Clob() { 98 | @Override 99 | public Publisher stream() { 100 | return streamPublisher; 101 | } 102 | 103 | @Override 104 | public Publisher discard() { 105 | return releasePublisher; 106 | } 107 | }; 108 | } 109 | 110 | /** 111 | *

112 | * Returns a publisher of LOB content that implements the behavior specified 113 | * for {@link Blob#stream()} and {@link Clob#stream()} R2DBC SPIs. 114 | *

115 | * This publisher will publish LOB content to the first subscriber that 116 | * subscribes, and will throw an {@link IllegalStateException} if 117 | * subscribed to more than once. 118 | *

119 | * If a subscription to this publisher is cancelled, this publisher will 120 | * initiate the release of LOB resources. 121 | *

122 | * If LOB content publishing terminates with an {@code onComplete} or 123 | * {@code onError}, this publisher will initiate the release of LOB resources. 124 | * This behavior is specified in the R2DBC 0.8.1 Specification, 125 | * Section 12.2.5: 126 | *

127 |    * Applications may release Blob and Clob by either consuming the
128 |    * content stream or disposing of resources by calling the discard()
129 |    * method.
130 |    * 
131 | *

132 | * When, upon it's termination, the returned publisher subscribes to the 133 | * {@code releasePublisher}, the result emitted by the release publisher is 134 | * not emitted to the subscriber of the returned publisher. To process signals 135 | * that result from releasing the LOB, the {@code releasePublisher} must 136 | * emit the same result to subscribers of {@link Blob#discard()} or 137 | * {@link Clob#discard()}. 138 | *

139 | * @param contentPublisher Publishes a LOB's contents 140 | * @param releasePublisher Publishes the result of releasing a LOB. 141 | * @param The type of published content 142 | * @return A LOB content publisher that releases resources upon it's 143 | * termination. 144 | */ 145 | private static Publisher createStreamPublisher( 146 | Publisher contentPublisher, Publisher releasePublisher) { 147 | 148 | AtomicBoolean isSubscribed = new AtomicBoolean(false); 149 | 150 | return subscriber -> { 151 | Objects.requireNonNull(subscriber, "Subscriber is null"); 152 | 153 | if (isSubscribed.compareAndSet(false, true)) { 154 | Flux.from(contentPublisher) 155 | // Call to free the LOB should happen *after* the LOB content 156 | // publisher terminates, so that calls to Blob/Clob.freeAsyncOracle 157 | // do not block. 158 | .doFinally(signalType -> Mono.from(releasePublisher).subscribe()) 159 | .subscribe(subscriber); 160 | } 161 | else { 162 | throw new IllegalStateException( 163 | "A content stream can not be consumed more than once"); 164 | } 165 | }; 166 | } 167 | } -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/Publishers.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import org.reactivestreams.Publisher; 25 | import reactor.core.publisher.Flux; 26 | import reactor.core.publisher.Mono; 27 | 28 | /** 29 | * Factory methods that create a {@code Publisher}. These methods cover special 30 | * cases which are not already supported by Project Reactor. 31 | */ 32 | class Publishers { 33 | 34 | private Publishers() {} 35 | 36 | /** 37 | * A publisher that immediately emits onNext and onComplete to subscribers 38 | */ 39 | private static final Publisher COMPLETED_PUBLISHER = 40 | Mono.just(new Object()); 41 | 42 | /** 43 | *

44 | * Returns a publisher that emits the concatenated signals of a 45 | * {@code publisher} and {@code onTerminationPublisher}. If the 46 | * {@code onTerminationPublisher} emits an error, it will suppress any error 47 | * emitted by the first {@code publisher}. If a subscription to the returned 48 | * publisher is cancelled, the {@code onTerminationPublisher} is subscribed to 49 | * but it can not emit any error through the cancelled subscription. 50 | *

51 | * The returned publisher behaves similarly to:

{@code
52 |    * Flux.concatDelayError(
53 |    *   publisher,
54 |    *   onTerminationPublisher)
55 |    *   .doOnCancel(onTerminationPublisher::subscribe)
56 |    * }

57 | * However, the code above can result in: 58 | *

59 |    *   reactor.core.Exceptions$StaticThrowable: Operator has been terminated
60 |    * 

61 | * This seems to happen when the concatDelayError publisher receives a cancel 62 | * from a downstream subscriber after it has already received onComplete from 63 | * a upstream publisher. 64 | *

65 | * @param publisher First publisher which is subscribed to. 66 | * @param onTerminationPublisher Publisher which is subscribed to when the 67 | * first publisher terminates, or a subcription is cancelled. 68 | * @return The concatenated publisher. 69 | * @param Type of objects emitted to onNext 70 | */ 71 | static Publisher concatTerminal( 72 | Publisher publisher, Publisher onTerminationPublisher) { 73 | return Flux.usingWhen( 74 | COMPLETED_PUBLISHER, 75 | ignored -> publisher, 76 | ignored -> onTerminationPublisher); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/SqlTypeMap.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc.impl; 22 | 23 | import io.r2dbc.spi.R2dbcType; 24 | import io.r2dbc.spi.Type; 25 | import oracle.jdbc.OracleType; 26 | import oracle.r2dbc.OracleR2dbcObject; 27 | import oracle.r2dbc.OracleR2dbcTypes; 28 | import oracle.sql.VECTOR; 29 | import oracle.sql.json.OracleJsonObject; 30 | 31 | import java.math.BigDecimal; 32 | import java.math.BigInteger; 33 | import java.nio.ByteBuffer; 34 | import java.sql.JDBCType; 35 | import java.sql.RowId; 36 | import java.sql.SQLException; 37 | import java.sql.SQLType; 38 | import java.sql.Types; 39 | import java.time.Duration; 40 | import java.time.LocalDate; 41 | import java.time.LocalDateTime; 42 | import java.time.LocalTime; 43 | import java.time.OffsetDateTime; 44 | import java.time.OffsetTime; 45 | import java.time.Period; 46 | import java.util.Map; 47 | import java.util.stream.Collectors; 48 | 49 | import static java.util.Map.entry; 50 | 51 | /** 52 | * Defines the SQL to Java type mappings used by Oracle R2DBC. 53 | */ 54 | final class SqlTypeMap { 55 | 56 | /** 57 | * Mapping of JDBC's {@link SQLType} to R2DBC {@link io.r2dbc.spi.Type}s. 58 | */ 59 | private static final Map JDBC_TO_R2DBC_TYPE_MAP = 60 | Map.ofEntries( 61 | entry(JDBCType.ARRAY, R2dbcType.COLLECTION), 62 | entry(JDBCType.BIGINT, R2dbcType.BIGINT), 63 | entry(JDBCType.BINARY, R2dbcType.BINARY), 64 | entry(OracleType.BINARY_DOUBLE, OracleR2dbcTypes.BINARY_DOUBLE), 65 | entry(OracleType.BINARY_FLOAT, OracleR2dbcTypes.BINARY_FLOAT), 66 | entry(JDBCType.BLOB, R2dbcType.BLOB), 67 | entry(JDBCType.BOOLEAN, R2dbcType.BOOLEAN), 68 | entry(JDBCType.CHAR, R2dbcType.CHAR), 69 | entry(JDBCType.CLOB, R2dbcType.CLOB), 70 | entry(JDBCType.DATE, R2dbcType.DATE), 71 | entry(JDBCType.DECIMAL, R2dbcType.DECIMAL), 72 | entry(JDBCType.DOUBLE, R2dbcType.DOUBLE), 73 | entry(JDBCType.FLOAT, R2dbcType.FLOAT), 74 | entry(JDBCType.INTEGER, R2dbcType.INTEGER), 75 | entry( 76 | OracleType.INTERVAL_DAY_TO_SECOND, 77 | OracleR2dbcTypes.INTERVAL_DAY_TO_SECOND), 78 | entry( 79 | OracleType.INTERVAL_YEAR_TO_MONTH, 80 | OracleR2dbcTypes.INTERVAL_YEAR_TO_MONTH), 81 | entry(OracleType.JSON, OracleR2dbcTypes.JSON), 82 | entry(JDBCType.LONGVARBINARY, OracleR2dbcTypes.LONG_RAW), 83 | entry(JDBCType.LONGVARCHAR, OracleR2dbcTypes.LONG), 84 | entry(JDBCType.NCHAR, R2dbcType.NCHAR), 85 | entry(JDBCType.NCLOB, R2dbcType.NCLOB), 86 | entry(JDBCType.NUMERIC, R2dbcType.NUMERIC), 87 | entry(JDBCType.NVARCHAR, R2dbcType.NVARCHAR), 88 | entry(JDBCType.REAL, R2dbcType.REAL), 89 | entry(JDBCType.REF_CURSOR, OracleR2dbcTypes.REF_CURSOR), 90 | entry(JDBCType.ROWID, OracleR2dbcTypes.ROWID), 91 | entry(JDBCType.SMALLINT, R2dbcType.SMALLINT), 92 | entry(JDBCType.TIME, R2dbcType.TIME), 93 | entry(JDBCType.TIME_WITH_TIMEZONE, R2dbcType.TIME_WITH_TIME_ZONE), 94 | entry(JDBCType.TIMESTAMP, R2dbcType.TIMESTAMP), 95 | entry( 96 | OracleType.TIMESTAMP_WITH_LOCAL_TIME_ZONE, 97 | OracleR2dbcTypes.TIMESTAMP_WITH_LOCAL_TIME_ZONE), 98 | entry( 99 | // Map the OracleType value, which has a different type code than the 100 | // JDBCType value. 101 | OracleType.TIMESTAMP_WITH_TIME_ZONE, 102 | R2dbcType.TIMESTAMP_WITH_TIME_ZONE), 103 | entry(JDBCType.TINYINT, R2dbcType.TINYINT), 104 | entry(JDBCType.VARBINARY, R2dbcType.VARBINARY), 105 | entry(JDBCType.VARCHAR, R2dbcType.VARCHAR), 106 | entry(OracleType.VECTOR, OracleR2dbcTypes.VECTOR) 107 | ); 108 | 109 | /** 110 | * Mapping of R2DBC {@link Type}s to JDBC's {@link SQLType}. 111 | */ 112 | private static final Map R2DBC_TO_JDBC_TYPE_MAP = 113 | // Swap R2DBC key and JDBC value 114 | JDBC_TO_R2DBC_TYPE_MAP.entrySet().stream() 115 | .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)); 116 | 117 | /** 118 | * Mapping of Java classes to JDBC's {@link SQLType}. 119 | */ 120 | private static final Map, SQLType> JAVA_TO_SQL_TYPE_MAP = 121 | Map.ofEntries( 122 | // Standard mappings listed in the R2DBC Specification 123 | entry(String.class, JDBCType.VARCHAR), 124 | entry(Boolean.class, JDBCType.BOOLEAN), 125 | entry(ByteBuffer.class, JDBCType.VARBINARY), 126 | entry(Integer.class, JDBCType.INTEGER), 127 | entry(Byte.class, JDBCType.TINYINT), 128 | entry(Short.class, JDBCType.SMALLINT), 129 | entry(Long.class, JDBCType.BIGINT), 130 | entry(BigDecimal.class, JDBCType.NUMERIC), 131 | entry(Float.class, JDBCType.REAL), 132 | entry(Double.class, JDBCType.DOUBLE), 133 | entry(LocalDate.class, JDBCType.DATE), 134 | entry(LocalTime.class, JDBCType.TIME), 135 | entry(OffsetTime.class, JDBCType.TIME_WITH_TIMEZONE), 136 | entry(LocalDateTime.class, JDBCType.TIMESTAMP), 137 | entry(OffsetDateTime.class, JDBCType.TIMESTAMP_WITH_TIMEZONE), 138 | entry(io.r2dbc.spi.Blob.class, JDBCType.BLOB), 139 | entry(io.r2dbc.spi.Clob.class, JDBCType.CLOB), 140 | entry(Object[].class, JDBCType.ARRAY), 141 | 142 | // JDBC 4.3 mappings not included in R2DBC Specification. Types like 143 | // java.sql.Blob/Clob/NClob/Array can be accessed from Row.get(...) 144 | // and then used as bind values, so these types are included. 145 | // TODO: JDBC 4.3 lists a "Java class" to JAVA_OBJECT mapping. It's not 146 | // clear what that is for, or if Oracle JDBC supports it. That mapping 147 | // is not included here. 148 | entry(byte[].class, JDBCType.VARBINARY), 149 | entry(BigInteger.class, JDBCType.BIGINT), 150 | entry(java.sql.Date.class, JDBCType.DATE), 151 | entry(java.sql.Time.class, JDBCType.TIME), 152 | entry(java.sql.Timestamp.class, JDBCType.TIMESTAMP), 153 | entry(java.sql.Array.class, JDBCType.ARRAY), 154 | entry(java.sql.Blob.class, JDBCType.BLOB), 155 | entry(java.sql.Clob.class, JDBCType.CLOB), 156 | entry(java.sql.Struct.class, JDBCType.STRUCT), 157 | entry(java.sql.Ref.class, JDBCType.REF), 158 | entry(java.net.URL.class, JDBCType.DATALINK), 159 | entry(java.sql.RowId.class, JDBCType.ROWID), 160 | entry(java.sql.NClob.class, JDBCType.NCLOB), 161 | entry(java.sql.SQLXML.class, JDBCType.SQLXML), 162 | entry(java.util.Calendar.class, JDBCType.TIMESTAMP), 163 | entry(java.util.Date.class, JDBCType.TIMESTAMP), 164 | 165 | // Extended mappings supported by Oracle 166 | entry(Duration.class, OracleType.INTERVAL_DAY_TO_SECOND), 167 | entry(Period.class, OracleType.INTERVAL_YEAR_TO_MONTH), 168 | entry(OracleJsonObject.class, OracleType.JSON), 169 | 170 | // Primitive array mappings supported by OracleArray. Primitive arrays are 171 | // not subtypes of Object[], which is listed for SQLType.ARRAY above. The 172 | // primitive array types must be explicitly listed here. 173 | entry(boolean[].class, JDBCType.ARRAY), 174 | // byte[] is mapped to RAW by default 175 | // entry(byte[].class, JDBCType.ARRAY), 176 | entry(short[].class, JDBCType.ARRAY), 177 | entry(int[].class, JDBCType.ARRAY), 178 | entry(long[].class, JDBCType.ARRAY), 179 | entry(float[].class, JDBCType.ARRAY), 180 | entry(double[].class, JDBCType.ARRAY), 181 | 182 | // Support binding Map and OracleR2dbcObject to OBJECT 183 | // (ie: STRUCT) 184 | entry(Map.class, JDBCType.STRUCT), 185 | entry(OracleR2dbcObject.class, JDBCType.STRUCT), 186 | 187 | // Support binding oracle.sql.VECTOR to VECTOR 188 | entry(VECTOR.class, OracleType.VECTOR) 189 | ); 190 | 191 | /** 192 | * Returns the R2DBC {@code Type} identifying the same SQL type as an JDBC 193 | * {@code SQLType}, or {@code null} if no R2DBC {@code Type} is known to 194 | * identify same SQL type as the {@code jdbcType}. 195 | * @param jdbcType A JDBC SQL type 196 | * @return An R2DBC SQL type. May be null. 197 | */ 198 | static Type toR2dbcType(SQLType jdbcType) { 199 | return JDBC_TO_R2DBC_TYPE_MAP.get(jdbcType); 200 | } 201 | 202 | /** 203 | * Returns the R2DBC {@code Type} identifying the same SQL type as a JDBC 204 | * type number, or {@code null} if no R2DBC {@code Type} is known to 205 | * identify same SQL type as the {@code jdbcTypeNumber}. 206 | * @param jdbcTypeNumber A JDBC type number 207 | * @return An R2DBC SQL type. May be null. 208 | */ 209 | static Type toR2dbcType(int jdbcTypeNumber) { 210 | 211 | // Search for a JDBCType with a matching type number 212 | for (JDBCType jdbcType : JDBCType.values()) { 213 | Integer vendorTypeNumber = jdbcType.getVendorTypeNumber(); 214 | 215 | if (vendorTypeNumber != null && vendorTypeNumber == jdbcTypeNumber) 216 | return toR2dbcType(jdbcType); 217 | } 218 | 219 | // If no JDBCType matches, search for a matching OracleType 220 | try { 221 | OracleType oracleType = 222 | oracle.jdbc.OracleType.toOracleType(jdbcTypeNumber); 223 | 224 | if (oracleType != null) 225 | return toR2dbcType(oracleType); 226 | else 227 | return null; 228 | } 229 | catch (SQLException typeNotFound) { 230 | // toOracleType is specified to throw SQLException if no match is found 231 | return null; 232 | } 233 | } 234 | 235 | /** 236 | * Returns the JDBC {@code SQLType} identifying the same SQL type as an 237 | * R2DBC {@code Type}, or {@code null} if no JDBC {@code SQLType} is known to 238 | * identify same SQL type as the {@code r2dbcType}. 239 | * @param r2dbcType An R2DBC SQL type 240 | * @return A JDBC SQL type 241 | */ 242 | static SQLType toJdbcType(Type r2dbcType) { 243 | if (r2dbcType instanceof Type.InferredType) 244 | return toJdbcType(r2dbcType.getJavaType()); 245 | else if (r2dbcType instanceof OracleR2dbcTypes.ArrayType) 246 | return JDBCType.ARRAY; 247 | else if (r2dbcType instanceof OracleR2dbcTypes.ObjectType) 248 | return JDBCType.STRUCT; 249 | else 250 | return R2DBC_TO_JDBC_TYPE_MAP.get(r2dbcType); 251 | } 252 | 253 | /** 254 | *

255 | * Returns the JDBC {@code SQLType} identifying the default SQL type 256 | * mapping for a {@code javaType}, or {@code null} if 257 | * {@code javaType} has no SQL type mapping. 258 | *

259 | * The type returned by this method is derived from the the R2DBC 260 | * Specification's SQL to Java type mappings. Where the specification 261 | * defines a Java type that maps to a single SQL type, this method returns 262 | * that SQL type. Where the specification defines a Java type that maps to 263 | * multiple SQL types, the return value of this method is as follows: 264 | *

    265 | *
  • {@link String} : VARCHAR
  • 266 | *
  • {@link ByteBuffer} : VARBINARY
  • 267 | *
268 | * This method returns non-standard SQL types supported by Oracle 269 | * Database for the following Java types: 270 | *
    271 | *
  • {@link Double} : BINARY_DOUBLE
  • 272 | *
  • {@link Float} : BINARY_FLOAT
  • 273 | *
  • {@link Duration} : INTERVAL DAY TO SECOND
  • 274 | *
  • {@link Period} : INTERVAL YEAR TO MONTH
  • 275 | *
  • {@link RowId} : ROWID
  • 276 | *
  • {@link OracleJsonObject} : JSON
  • 277 | *
  • {@link oracle.sql.VECTOR} : VECTOR
  • 278 | *
279 | * @param javaType Java type to map 280 | * @return SQL type mapping for the {@code javaType} 281 | */ 282 | static SQLType toJdbcType(Class javaType) { 283 | SQLType sqlType = JAVA_TO_SQL_TYPE_MAP.get(javaType); 284 | 285 | if (sqlType != null) { 286 | return sqlType; 287 | } 288 | else { 289 | // Search for a mapping of the object's super-type 290 | return JAVA_TO_SQL_TYPE_MAP.entrySet() 291 | .stream() 292 | .filter(entry -> entry.getKey().isAssignableFrom(javaType)) 293 | .map(Map.Entry::getValue) 294 | .findFirst() 295 | .orElse(null); 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/SuppliedOptionConnectionFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.Connection; 25 | import io.r2dbc.spi.ConnectionFactory; 26 | import io.r2dbc.spi.ConnectionFactoryMetadata; 27 | import io.r2dbc.spi.ConnectionFactoryOptions; 28 | import io.r2dbc.spi.Option; 29 | import oracle.r2dbc.OracleR2dbcOptions; 30 | import org.reactivestreams.Publisher; 31 | import reactor.core.publisher.Flux; 32 | import reactor.core.publisher.Mono; 33 | 34 | import java.util.Objects; 35 | import java.util.Set; 36 | import java.util.function.Supplier; 37 | import java.util.stream.Collectors; 38 | import java.util.stream.Stream; 39 | 40 | /** 41 | * A connection factory having {@link io.r2dbc.spi.ConnectionFactoryOptions} 42 | * with values provided by a {@link Supplier} or {@link Publisher}. Supplied 43 | * values are requested when {@link #create()} is called. After all requested 44 | * values are supplied, this factory delegates to 45 | * {@link OracleConnectionFactoryProviderImpl#create(ConnectionFactoryOptions)}, 46 | * with {@link ConnectionFactoryOptions} composed of the supplied values. 47 | */ 48 | final class SuppliedOptionConnectionFactory implements ConnectionFactory { 49 | 50 | /** 51 | * The set of all options recognized by Oracle R2DBC. This set includes 52 | * the standard options declared by {@link ConnectionFactoryOptions} and 53 | * the extended options declared by {@link OracleR2dbcOptions}. 54 | * 55 | * TODO: This set only includes standard options defined for version 1.0.0 of 56 | * the SPI. If a future SPI version introduces new options, those must be 57 | * added to this set. 58 | */ 59 | private static final Set> ALL_OPTIONS = 60 | Stream.concat( 61 | // Standard options: 62 | Stream.of( 63 | ConnectionFactoryOptions.CONNECT_TIMEOUT, 64 | ConnectionFactoryOptions.DATABASE, 65 | ConnectionFactoryOptions.DRIVER, 66 | ConnectionFactoryOptions.HOST, 67 | ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT, 68 | ConnectionFactoryOptions.PASSWORD, 69 | ConnectionFactoryOptions.PORT, 70 | ConnectionFactoryOptions.PROTOCOL, 71 | ConnectionFactoryOptions.SSL, 72 | ConnectionFactoryOptions.STATEMENT_TIMEOUT, 73 | ConnectionFactoryOptions.USER), 74 | // Extended options: 75 | OracleR2dbcOptions.options().stream()) 76 | .collect(Collectors.toUnmodifiableSet()); 77 | 78 | /** 79 | * Publishers which emit the value of an option. The value may come from a 80 | * user provided Publisher, from a user provided Supplier, or just a user 81 | * provided value. So, all the option values below would get turned into a 82 | * Publisher which is added to this set: 83 | *
{@code
 84 |    * ConnectionFactoryOptions.builder()
 85 |    *   .option(DRIVER, "oracle")
 86 |    *   .option(supplied(HOST, () -> "database-host")
 87 |    *   .option(published(PORT, Mono.just(1521))
 88 |    * }
89 | */ 90 | private final Set> optionValuePublishers; 91 | 92 | SuppliedOptionConnectionFactory(ConnectionFactoryOptions options) { 93 | optionValuePublishers = ALL_OPTIONS.stream() 94 | .map(option -> toOptionValuePublisher(option, options.getValue(option))) 95 | .filter(Objects::nonNull) 96 | .collect(Collectors.toUnmodifiableSet()); 97 | } 98 | 99 | @Override 100 | public Publisher create() { 101 | return Flux.merge(optionValuePublishers) 102 | .collectList() 103 | .map(SuppliedOptionConnectionFactory::toConnectionFactoryOptions) 104 | .flatMap(options -> 105 | Mono.from(new OracleConnectionFactoryProviderImpl() 106 | .create(options) 107 | .create())); 108 | } 109 | 110 | @Override 111 | public ConnectionFactoryMetadata getMetadata() { 112 | return OracleConnectionFactoryMetadataImpl.INSTANCE; 113 | } 114 | 115 | /** 116 | * Converts an {@link Option} and its value to a {@link Publisher} that 117 | * emits an {@link OptionValue}. If the value passed to this 118 | * method is a Supplier or Publisher, then the Publisher returned by this 119 | * method emits an OptionValue with the value supplied by the 120 | * Supplier or Publisher. If the value passed to this method is 121 | * not a Supplier or Publisher, then the Publisher returned by this method 122 | * just emits an OptionValue with the given value. 123 | * 124 | * @param option An option. Not null. 125 | * @param value The value of the option, or null if there is no value, or a 126 | * Supplier or Publisher which supplies the value of 127 | * the option. 128 | * @return A publisher that emits the option and its possibly supplied value, 129 | * or this method returns null if the given value is null. 130 | */ 131 | private static Publisher toOptionValuePublisher( 132 | Option option, Object value) { 133 | final Publisher valuePublisher; 134 | 135 | if (value == null) 136 | return null; 137 | 138 | if (value instanceof Supplier) 139 | valuePublisher = Mono.fromSupplier((Supplier)value); 140 | else if (value instanceof Publisher) 141 | valuePublisher = Mono.from((Publisher)value); 142 | else 143 | return Mono.just(new OptionValue(option, value)); 144 | 145 | return Mono.from(valuePublisher) 146 | .map(publishedValue -> new OptionValue(option, publishedValue)) 147 | .onErrorMap(error -> OracleR2dbcExceptions.newNonTransientException( 148 | "Error when requesting a value of " + option 149 | + " from a Supplier or Publisher", 150 | null, // sql 151 | error)); 152 | } 153 | 154 | /** 155 | * Converts a collection of options and values into an instance of 156 | * {@link ConnectionFactoryOptions}. 157 | * 158 | * @param optionValues Iterable options and values. Not null. 159 | * @return ConnectionFactoryOptions configured with the given 160 | * options and values. Not null. 161 | */ 162 | private static ConnectionFactoryOptions toConnectionFactoryOptions( 163 | Iterable optionValues) { 164 | 165 | ConnectionFactoryOptions.Builder optionsBuilder = 166 | ConnectionFactoryOptions.builder(); 167 | 168 | optionValues.forEach(optionValue -> 169 | optionValue.configure(optionsBuilder)); 170 | 171 | return optionsBuilder.build(); 172 | } 173 | 174 | /** 175 | * Checks if the value of an option is supplied by a {@link Supplier} or 176 | * {@link Publisher}. This method is used to check if 177 | * {@link SuppliedOptionConnectionFactory} should be used to handle any 178 | * supplied option values. If this method returns false, then there is no 179 | * reason to use this connection factory. 180 | * 181 | * @param options Options that may a value which is a {@link Supplier} or 182 | * {@link Publisher}. 183 | * @return true if the value of at least one option is a {@link Supplier} or 184 | * {@link Publisher}. Returns false otherwise. 185 | */ 186 | static boolean containsSuppliedValue( 187 | ConnectionFactoryOptions options) { 188 | return ALL_OPTIONS.stream() 189 | .map(options::getValue) 190 | .anyMatch(value -> 191 | value instanceof Supplier || value instanceof Publisher); 192 | } 193 | 194 | /** A record of an {@link Option} and its value */ 195 | private static final class OptionValue { 196 | 197 | final Option option; 198 | 199 | final Object value; 200 | 201 | OptionValue(Option option, Object value) { 202 | this.option = option; 203 | this.value = value; 204 | } 205 | 206 | /** 207 | * @param builder Builder to configure with the {@link #value} of an 208 | * {@link #option}. Not null. 209 | */ 210 | void configure(ConnectionFactoryOptions.Builder builder) { 211 | @SuppressWarnings("unchecked") 212 | Option option = (Option)this.option; 213 | builder.option(option, value); 214 | } 215 | 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/impl/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | /** 23 | *

24 | * Implements the R2DBC SPI for the Oracle Database. Classes in this package are 25 | * concrete implementations of interfaces defined in the io.r2dbc.spi package. 26 | * For maximum portability between different R2DBC drivers, application 27 | * developers should program against the io.r2dbc.spi interfaces only, without 28 | * referencing any classes defined in this package. 29 | *

30 | * 31 | * The API defined by this package is not stable. It may change between 32 | * releases of Oracle R2DBC. 33 | * 34 | *

35 | * {@link oracle.r2dbc.impl.ReactiveJdbcAdapter} defines a reactive interface 36 | * to be implemented using any non-standard APIs that a JDBC driver may provide 37 | * for asynchronous database access. The Oracle R2DBC Driver relies on an 38 | * instance of this adapter in order to implement most R2DBC SPI methods. 39 | * {@link oracle.r2dbc.impl.OracleReactiveJdbcAdapter} is the only concrete 40 | * implementation of this adapter currently. 41 | *

42 | */ 43 | package oracle.r2dbc.impl; 44 | -------------------------------------------------------------------------------- /src/main/java/oracle/r2dbc/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | /** 23 | *

24 | * Extensions to the standard R2DBC SPI for the Oracle Database. The API of this 25 | * package is intended for consumption by external code bases. This API may be 26 | * used to access functions of Oracle Database which are not offered by the 27 | * standard R2DBC SPI. 28 | *

29 | */ 30 | package oracle.r2dbc; 31 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider: -------------------------------------------------------------------------------- 1 | oracle.r2dbc.impl.OracleConnectionFactoryProviderImpl 2 | -------------------------------------------------------------------------------- /src/test/java/oracle/r2dbc/impl/OracleBatchImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.Batch; 25 | import io.r2dbc.spi.Connection; 26 | import org.junit.jupiter.api.Test; 27 | import reactor.core.publisher.Flux; 28 | import reactor.core.publisher.Mono; 29 | 30 | import static java.util.Arrays.asList; 31 | import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; 32 | import static oracle.r2dbc.test.DatabaseConfig.sharedConnection; 33 | import static oracle.r2dbc.util.Awaits.awaitMany; 34 | import static oracle.r2dbc.util.Awaits.awaitNone; 35 | import static oracle.r2dbc.util.Awaits.awaitOne; 36 | import static org.junit.jupiter.api.Assertions.assertEquals; 37 | import static org.junit.jupiter.api.Assertions.assertThrows; 38 | import static org.junit.jupiter.api.Assertions.assertTrue; 39 | 40 | /** 41 | * Verifies that 42 | * {@link OracleBatchImpl} implements behavior that is specified in 43 | * it's class and method level javadocs. 44 | */ 45 | public class OracleBatchImplTest { 46 | 47 | /** 48 | * Verifies the implementation of 49 | * {@link OracleBatchImpl#add(String)} 50 | */ 51 | @Test 52 | public void testAdd() { 53 | Connection connection = 54 | Mono.from(sharedConnection()).block(connectTimeout()); 55 | try { 56 | Batch batch = connection.createBatch(); 57 | 58 | // Expect IllegalArgumentException from null SQL 59 | assertThrows(IllegalArgumentException.class, () -> batch.add(null)); 60 | 61 | // Expect add to return the same object 62 | assertTrue(batch == batch.add("SELECT x FROM sys.dual")); 63 | } 64 | finally { 65 | awaitNone(connection.close()); 66 | } 67 | } 68 | 69 | /** 70 | * Verifies the implementation of 71 | * {@link OracleBatchImpl#execute()} 72 | */ 73 | @Test 74 | public void testExecute() { 75 | Connection connection = 76 | Mono.from(sharedConnection()).block(connectTimeout()); 77 | try { 78 | Batch batch = connection.createBatch(); 79 | 80 | // Expect empty batch publisher to emit onComplete 81 | awaitNone(batch.execute()); 82 | 83 | // Expect batch of 1 to emit 1 value 84 | awaitOne(1, Flux.from(batch.add( 85 | "SELECT 1 FROM sys.dual") 86 | .execute()) 87 | .flatMap(result -> 88 | result.map((row, metadata) -> row.get(0, Integer.class)))); 89 | 90 | // Expect the Batch to be cleared after execute() is called 91 | awaitNone(batch.execute()); 92 | 93 | // Expect statements to execute in order 94 | awaitMany(asList(1, 2, 3, 4, 5), 95 | Flux.from(batch.add("SELECT 1 FROM sys.dual") 96 | .add("SELECT 2 FROM sys.dual") 97 | .add("SELECT 3 FROM sys.dual") 98 | .add("SELECT 4 FROM sys.dual") 99 | .add("SELECT 5 FROM sys.dual") 100 | .execute()) 101 | .flatMapSequential(result -> 102 | result.map((row, metadata) -> row.get(0, Integer.class)))); 103 | 104 | } 105 | finally { 106 | awaitNone(connection.close()); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/oracle/r2dbc/impl/OracleConnectionFactoryImplTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.Connection; 25 | import io.r2dbc.spi.ConnectionFactories; 26 | import io.r2dbc.spi.ConnectionFactoryOptions; 27 | import io.r2dbc.spi.R2dbcException; 28 | import oracle.r2dbc.test.DatabaseConfig; 29 | import org.junit.jupiter.api.Test; 30 | import org.reactivestreams.Publisher; 31 | import reactor.core.publisher.Flux; 32 | import reactor.core.publisher.Mono; 33 | 34 | import java.sql.SQLException; 35 | import java.time.Duration; 36 | import java.util.HashSet; 37 | import java.util.Set; 38 | import java.util.function.IntSupplier; 39 | import java.util.function.Supplier; 40 | 41 | import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; 42 | import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; 43 | import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; 44 | import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; 45 | import static io.r2dbc.spi.ConnectionFactoryOptions.USER; 46 | import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions; 47 | import static org.junit.jupiter.api.Assertions.assertEquals; 48 | import static org.junit.jupiter.api.Assertions.assertTrue; 49 | import static org.junit.jupiter.api.Assertions.fail; 50 | 51 | /** 52 | * Verifies that 53 | * {@link OracleConnectionFactoryImpl} implements behavior that is specified 54 | * in it's class and method level javadocs. 55 | */ 56 | public class OracleConnectionFactoryImplTest { 57 | 58 | /** 59 | * Verifies that {@link OracleConnectionFactoryImpl} can be discovered by 60 | * {@link ConnectionFactories}. 61 | */ 62 | @Test 63 | public void testDiscovery() { 64 | assertEquals( 65 | OracleConnectionFactoryImpl.class, 66 | ConnectionFactories 67 | .get(ConnectionFactoryOptions.builder() 68 | .option(ConnectionFactoryOptions.DRIVER, "oracle") 69 | .option(HOST, "dbhost") 70 | .option(PORT, 1521) 71 | .option(ConnectionFactoryOptions.DATABASE, "service_name") 72 | .build()) 73 | .getClass()); 74 | 75 | assertEquals( 76 | OracleConnectionFactoryImpl.class, 77 | ConnectionFactories 78 | .find(ConnectionFactoryOptions.builder() 79 | .option(ConnectionFactoryOptions.DRIVER, "oracle") 80 | .option(HOST, "dbhost") 81 | .option(PORT, 1521) 82 | .option(ConnectionFactoryOptions.DATABASE, "service_name") 83 | .build()) 84 | .getClass()); 85 | 86 | assertEquals( 87 | OracleConnectionFactoryImpl.class, 88 | ConnectionFactories 89 | .get("r2dbc:oracle://dbhost:1521/service_name") 90 | .getClass()); 91 | 92 | assertEquals( 93 | OracleConnectionFactoryImpl.class, 94 | ConnectionFactories 95 | .get("r2dbc:oracle://user:password@dbhost:1521/service_name") 96 | .getClass()); 97 | 98 | assertEquals( 99 | OracleConnectionFactoryImpl.class, 100 | ConnectionFactories 101 | .get( 102 | "r2dbc:oracle://user:password@dbhost:1521/service_name?option=value") 103 | .getClass()); 104 | } 105 | 106 | /** 107 | * Verifies the implementation of 108 | * {@link OracleConnectionFactoryImpl#create()} 109 | * TODO: Verify resource allocation upon Subscription.request(long), and 110 | * resource release upon Subscription.cancel(). Consider querying V$SESSION 111 | * to check if JDBC connections are being opened and closed correctly. 112 | */ 113 | @Test 114 | public void testCreate() { 115 | Publisher connectionPublisher = 116 | new OracleConnectionFactoryImpl(connectionFactoryOptions()).create(); 117 | verifyConnectionPublisher(connectionPublisher); 118 | } 119 | 120 | /** 121 | * Verifies the implementation of 122 | * {@link OracleConnectionFactoryImpl#create()} when connection creation 123 | * fails. 124 | */ 125 | @Test 126 | public void testCreateFailure() { 127 | // Connect with the wrong username 128 | Publisher connectionPublisher = 129 | new OracleConnectionFactoryImpl(connectionFactoryOptions() 130 | .mutate() 131 | .option(ConnectionFactoryOptions.USER, 132 | "Wrong" + DatabaseConfig.user()) 133 | .build()) 134 | .create(); 135 | 136 | // Expect publisher to signal onError with an R2DBCException 137 | try { 138 | Flux.from(connectionPublisher) 139 | .doOnNext(connection -> Mono.from(connection.close()).subscribe()) 140 | .blockLast(DatabaseConfig.connectTimeout()); 141 | fail("Connection publisher did not signal onError"); 142 | } 143 | catch (R2dbcException expected) { 144 | Throwable cause = expected.getCause(); 145 | assertTrue( 146 | cause instanceof SQLException, 147 | "Unexpected type returned by R2dbcException.getCause(): " + 148 | cause.getClass()); 149 | 150 | assertEquals( 151 | 1017, // ORA-01017 is the error code for a wrong user name 152 | ((SQLException)cause).getErrorCode()); 153 | } 154 | 155 | // Expect publisher to reject multiple subscribers 156 | try { 157 | Mono.from(connectionPublisher) 158 | .block(Duration.ofSeconds(1)); 159 | fail("Connection publisher did not reject multiple subscribers"); 160 | } 161 | catch (IllegalStateException expected) { } 162 | } 163 | 164 | /** 165 | * Verifies the implementation of 166 | * {@link OracleConnectionFactoryImpl#getMetadata()} 167 | */ 168 | @Test 169 | public void testGetMetadata() { 170 | assertEquals("Oracle Database", 171 | new OracleConnectionFactoryImpl( 172 | ConnectionFactoryOptions.builder() 173 | .option(ConnectionFactoryOptions.DRIVER, "oracle") 174 | .option(HOST, "dbhost") 175 | .option(PORT, 1521) 176 | .option(ConnectionFactoryOptions.DATABASE, "service_name") 177 | .build()) 178 | .getMetadata() 179 | .getName()); 180 | } 181 | 182 | /** Verifies that a publisher emits connections to multiple subscribers */ 183 | private static void verifyConnectionPublisher( 184 | Publisher connectionPublisher) { 185 | 186 | // Expect publisher to emit one connection to each subscriber 187 | Set connections = new HashSet<>(); 188 | Flux.from(connectionPublisher) 189 | .doOnNext(connections::add) 190 | .doOnNext(connection -> Mono.from(connection.close()).subscribe()) 191 | .blockLast(DatabaseConfig.connectTimeout()); 192 | assertEquals(1, connections.size()); 193 | Flux.from(connectionPublisher) 194 | .doOnNext(connections::add) 195 | .doOnNext(connection -> Mono.from(connection.close()).subscribe()) 196 | .blockLast(DatabaseConfig.connectTimeout()); 197 | assertEquals(2, connections.size()); 198 | Flux.from(connectionPublisher) 199 | .doOnNext(connections::add) 200 | .doOnNext(connection -> Mono.from(connection.close()).subscribe()) 201 | .blockLast(DatabaseConfig.connectTimeout()); 202 | assertEquals(3, connections.size()); 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /src/test/java/oracle/r2dbc/impl/OracleR2dbcExceptionsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.impl; 23 | 24 | import io.r2dbc.spi.R2dbcBadGrammarException; 25 | import io.r2dbc.spi.R2dbcDataIntegrityViolationException; 26 | import io.r2dbc.spi.R2dbcException; 27 | import io.r2dbc.spi.R2dbcNonTransientException; 28 | import io.r2dbc.spi.R2dbcNonTransientResourceException; 29 | import io.r2dbc.spi.R2dbcRollbackException; 30 | import io.r2dbc.spi.R2dbcTimeoutException; 31 | import io.r2dbc.spi.R2dbcTransientException; 32 | import io.r2dbc.spi.R2dbcTransientResourceException; 33 | import org.junit.jupiter.api.Test; 34 | 35 | import java.io.IOException; 36 | import java.sql.SQLException; 37 | import java.sql.SQLIntegrityConstraintViolationException; 38 | import java.sql.SQLNonTransientConnectionException; 39 | import java.sql.SQLNonTransientException; 40 | import java.sql.SQLRecoverableException; 41 | import java.sql.SQLSyntaxErrorException; 42 | import java.sql.SQLTimeoutException; 43 | import java.sql.SQLTransactionRollbackException; 44 | import java.sql.SQLTransientConnectionException; 45 | import java.sql.SQLTransientException; 46 | import java.util.ArrayList; 47 | import java.util.List; 48 | 49 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.fromJdbc; 50 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; 51 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.runJdbc; 52 | import static oracle.r2dbc.impl.OracleR2dbcExceptions.toR2dbcException; 53 | import static org.junit.jupiter.api.Assertions.assertEquals; 54 | import static org.junit.jupiter.api.Assertions.assertSame; 55 | import static org.junit.jupiter.api.Assertions.assertThrows; 56 | import static org.junit.jupiter.api.Assertions.assertTrue; 57 | 58 | /** 59 | * Verifies that 60 | * {@link OracleR2dbcExceptions} implements behavior that is specified in it's 61 | * class and method level javadocs. 62 | */ 63 | public class OracleR2dbcExceptionsTest { 64 | 65 | /** 66 | * Verifies the implementation of 67 | * {@link OracleR2dbcExceptions#fromJdbc(OracleR2dbcExceptions.JdbcSupplier)} ()} 68 | */ 69 | @Test 70 | public void testGetOrHandleSqlException() { 71 | 72 | // Expect a supplied value when the supplier doesn't throw 73 | Object expected = new Object(); 74 | assertSame(expected, fromJdbc(() -> expected)); 75 | 76 | // Expect a thrown SQLException to be handled by throwing an 77 | // R2dbcException. 78 | IOException ioException = new IOException("IO-MESSAGE"); 79 | SQLException sqlException = new SQLException( 80 | "SQL-MESSAGE", "SQL-STATE", 9, ioException); 81 | R2dbcException r2dbcException = assertThrows(R2dbcException.class, () -> 82 | fromJdbc(() -> { 83 | throw sqlException; 84 | })); 85 | 86 | // Expect the R2dbcException to have the same message, sql state, and error 87 | // code as the SQLException 88 | assertEquals(sqlException.getMessage(), r2dbcException.getMessage()); 89 | assertEquals(sqlException.getSQLState(), r2dbcException.getSqlState()); 90 | assertEquals(sqlException.getErrorCode(), r2dbcException.getErrorCode()); 91 | 92 | // Expect the R2dbcException to have the SQLException as it's initial cause 93 | assertSame(sqlException, r2dbcException.getCause()); 94 | 95 | // Expect the R2dbcException to leave the initial cause of the SQLException 96 | // intact 97 | assertSame(ioException, r2dbcException.getCause().getCause()); 98 | } 99 | 100 | /** 101 | * Verifies the implementation of 102 | * {@link OracleR2dbcExceptions#runJdbc(OracleR2dbcExceptions.JdbcRunnable)} 103 | */ 104 | @Test 105 | public void testRunOrHandleSqlException() { 106 | 107 | // Expect a runnable to be ran 108 | Object expected = new Object(); 109 | List list = new ArrayList<>(); 110 | runJdbc(() -> list.add(expected)); 111 | assertSame(expected, list.get(0)); 112 | 113 | // Expect a thrown SQLException to be handled by throwing an 114 | // R2dbcException. 115 | IOException ioException = new IOException("IO-MESSAGE"); 116 | SQLException sqlException = new SQLException( 117 | "SQL-MESSAGE", "SQL-STATE", 9, ioException); 118 | R2dbcException r2dbcException = assertThrows(R2dbcException.class, () -> 119 | runJdbc(() -> { 120 | throw sqlException; 121 | })); 122 | 123 | // Expect the R2dbcException to have the same message, sql state, and error 124 | // code as the SQLException 125 | assertEquals(sqlException.getMessage(), r2dbcException.getMessage()); 126 | assertEquals(sqlException.getSQLState(), r2dbcException.getSqlState()); 127 | assertEquals(sqlException.getErrorCode(), r2dbcException.getErrorCode()); 128 | 129 | // Expect the R2dbcException to have the SQLException as it's initial cause 130 | assertSame(sqlException, r2dbcException.getCause()); 131 | 132 | // Expect the R2dbcException to leave the initial cause of the SQLException 133 | // intact 134 | assertSame(ioException, r2dbcException.getCause().getCause()); 135 | } 136 | 137 | /** 138 | * Verifies the implementation of 139 | * {@link OracleR2dbcExceptions#newNonTransientException(String, String, Throwable)} 140 | */ 141 | @Test 142 | public void testNewNonTransientException() { 143 | IOException ioException = new IOException("IO-MESSAGE"); 144 | SQLException sqlException = new SQLException( 145 | "SQL-MESSAGE", "SQL-STATE", 9, ioException); 146 | String message = "MESSAGE"; 147 | R2dbcNonTransientException r2dbcException = 148 | OracleR2dbcExceptions.newNonTransientException( 149 | message, null, sqlException); 150 | 151 | // Expect the R2dbcException to have the same message 152 | assertSame(message, r2dbcException.getMessage()); 153 | 154 | // Expect the R2dbcException to have the SQLException as it's initial cause 155 | assertSame(sqlException, r2dbcException.getCause()); 156 | 157 | // Expect the R2dbcException to leave the initial cause of the SQLException 158 | // intact 159 | assertSame(ioException, r2dbcException.getCause().getCause()); 160 | } 161 | 162 | /** 163 | * Verifies the implementation of 164 | * {@link OracleR2dbcExceptions#requireNonNull(Object, String)} 165 | */ 166 | @Test 167 | public void testRequireNonNull() { 168 | // Expect the input object to be returned if it is not null 169 | Object expected = new Object(); 170 | assertSame(expected, requireNonNull(expected, "object can not null")); 171 | 172 | // Expect an IllegalArgumentException having a specified message to be 173 | // thrown if the input object is null 174 | String message = "The argument is null!"; 175 | assertSame(message, assertThrows( 176 | IllegalArgumentException.class, () -> requireNonNull(null, message)) 177 | .getMessage()); 178 | 179 | } 180 | 181 | /** 182 | * Verifies the implementation of 183 | * {@link OracleR2dbcExceptions#toR2dbcException(SQLException)} 184 | */ 185 | @Test 186 | public void testToR2dbcException() { 187 | // Expect a SQLSyntaxErrorException to be mapped as an R2dbcBadGrammarException 188 | verifyR2dbcExceptionMapping( 189 | new SQLSyntaxErrorException("message", "state", 99, 190 | new IOException("io failure")), 191 | R2dbcBadGrammarException.class); 192 | 193 | // Expect a SQLIntegrityConstraintViolationException to be mapped as an 194 | // R2dbcDataIntegrityViolationException 195 | verifyR2dbcExceptionMapping( 196 | new SQLIntegrityConstraintViolationException("message", "state", 99, 197 | new IOException("io failure")), 198 | R2dbcDataIntegrityViolationException.class); 199 | 200 | // Expect a SQLNonTransientConnectionException to be mapped as an 201 | // R2dbcNonTransientResourceException 202 | verifyR2dbcExceptionMapping( 203 | new SQLNonTransientConnectionException("message", "state", 99, 204 | new IOException("io failure")), 205 | R2dbcNonTransientResourceException.class); 206 | 207 | // Expect a SQLNonTransientException to be mapped as an R2dbcNonTransientException 208 | verifyR2dbcExceptionMapping( 209 | new SQLNonTransientException("message", "state", 99, 210 | new IOException("io failure")), 211 | R2dbcNonTransientException.class); 212 | 213 | // Expect a SQLTimeoutException to be mapped as an R2dbcTimeoutException 214 | verifyR2dbcExceptionMapping( 215 | new SQLTimeoutException("message", "state", 99, 216 | new IOException("io failure")), 217 | R2dbcTimeoutException.class); 218 | 219 | // Expect a SQLTransactionRollbackException to be mapped as an 220 | // R2dbcRollbackException 221 | verifyR2dbcExceptionMapping( 222 | new SQLTransactionRollbackException("message", "state", 99, 223 | new IOException("io failure")), 224 | R2dbcRollbackException.class); 225 | 226 | // Expect a SQLTransientConnectionException to be mapped as an 227 | // R2dbcTransientResourceException 228 | verifyR2dbcExceptionMapping( 229 | new SQLTransientConnectionException("message", "state", 99, 230 | new IOException("io failure")), 231 | R2dbcTransientResourceException.class); 232 | 233 | // Expect a SQLTransientException to be mapped as an R2dbcTransientException 234 | verifyR2dbcExceptionMapping( 235 | new SQLTransientException("message", "state", 99, 236 | new IOException("io failure")), 237 | R2dbcTransientException.class); 238 | 239 | // Expect a SQLRecoverableException to be mapped as an 240 | // R2dbcTransientResourceException 241 | verifyR2dbcExceptionMapping( 242 | new SQLRecoverableException("message", "state", 99, 243 | new IOException("io failure")), 244 | R2dbcTransientResourceException.class); 245 | 246 | // Expect a SQLException to be mapped as an R2dbcException 247 | verifyR2dbcExceptionMapping( 248 | new SQLException("message", "state", 99, 249 | new IOException("io failure")), 250 | R2dbcException.class); 251 | 252 | // Expect a non-standard SQLException subtype to be mapped as an 253 | // R2dbcException 254 | class TestSqlException extends SQLException { 255 | public TestSqlException( 256 | String message, String sqlState, int errorCode, 257 | Throwable cause) { 258 | super(message, sqlState, errorCode, cause); 259 | } 260 | } 261 | verifyR2dbcExceptionMapping( 262 | new TestSqlException("message", "state", 99, 263 | new IOException("io failure")), 264 | R2dbcException.class); 265 | } 266 | 267 | /** 268 | * Verifies that a {@code sqlException} that is input to 269 | * {@link OracleR2dbcExceptions#toR2dbcException(SQLException)} is mapped to 270 | * an instance of an {@code expectedMapping} R2dbcException type. The 271 | * mapped R2dbcException is verified to have the same message, sql state, 272 | * and error code as the {@code sqlException}, is verified to the {@code 273 | * sqlException} as it's initial cause. 274 | * @param sqlException Input to toR2dbcException 275 | * @param expectedMapping Expected type output by toR2dbcException 276 | */ 277 | private void verifyR2dbcExceptionMapping(SQLException sqlException, 278 | Class expectedMapping) { 279 | 280 | final Throwable cause = sqlException.getCause(); 281 | 282 | // Expect a SQLException to be mapped to the expected R2dbcException type 283 | R2dbcException r2dbcException = toR2dbcException(sqlException); 284 | assertTrue(expectedMapping.isInstance(r2dbcException), 285 | String.format("%s is not an instance of %s", 286 | r2dbcException.getClass(), expectedMapping)); 287 | 288 | // Expect the R2dbcException to have the same message, sql state, and error 289 | // code as the SQLException 290 | assertEquals(sqlException.getMessage(), r2dbcException.getMessage()); 291 | assertEquals(sqlException.getSQLState(), r2dbcException.getSqlState()); 292 | assertEquals(sqlException.getErrorCode(), r2dbcException.getErrorCode()); 293 | 294 | // Expect the R2dbcException to have the SQLException as it's initial cause 295 | assertSame(sqlException, r2dbcException.getCause()); 296 | 297 | // Expect the R2dbcException to leave the initial cause of the SQLException 298 | // intact 299 | assertSame(cause, r2dbcException.getCause().getCause()); 300 | } 301 | 302 | } 303 | -------------------------------------------------------------------------------- /src/test/java/oracle/r2dbc/test/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.test; 23 | 24 | import io.r2dbc.spi.Connection; 25 | import io.r2dbc.spi.ConnectionFactories; 26 | import io.r2dbc.spi.ConnectionFactory; 27 | import io.r2dbc.spi.ConnectionFactoryOptions; 28 | import io.r2dbc.spi.Option; 29 | import oracle.jdbc.OracleConnection; 30 | import oracle.r2dbc.util.SharedConnectionFactory; 31 | import org.reactivestreams.Publisher; 32 | 33 | import java.io.FileNotFoundException; 34 | import java.io.InputStream; 35 | import java.sql.DriverManager; 36 | import java.sql.SQLException; 37 | import java.time.Duration; 38 | import java.util.Optional; 39 | import java.util.Properties; 40 | 41 | /** 42 | * Stores configuration used by integration tests that connect to a database. 43 | * The configuration is read from a resource file named "config.properties" 44 | * which is located by {@link ClassLoader#getResourceAsStream(String)}. 45 | */ 46 | public final class DatabaseConfig { 47 | 48 | private DatabaseConfig() {} 49 | 50 | /** 51 | * Returns the protocol used to connect with a database, specified as 52 | * {@code PROTOCOL} in the "config.properties" file. 53 | * @return Connection protocol for the test database. May be {@code null} if 54 | * no protocol is configured. 55 | */ 56 | public static String protocol() { 57 | return PROTOCOL; 58 | } 59 | 60 | /** 61 | * Returns the hostname of the server where a test database listens for 62 | * connections, specified as {@code HOST} in the "config.properties" file. 63 | * @return Hostname of a test database. 64 | */ 65 | public static String host() { 66 | return HOST; 67 | } 68 | 69 | /** 70 | * Returns the port number where a test database listens for connections, 71 | * specified as {@code PORT} in the "config.properties" file. 72 | * @return Port number of a test database. 73 | */ 74 | public static int port() { 75 | return PORT; 76 | } 77 | 78 | /** 79 | * Returns the service name that a test database identifies itself as, 80 | * specified as {@code DATABASE} in the "config.properties" file. 81 | * @return Service name of a test database. 82 | */ 83 | public static String serviceName() { 84 | return SERVICE_NAME; 85 | } 86 | 87 | /** 88 | * Returns the user name that a test database authenticates, specified as 89 | * {@code USER} in the "config.properties" file. 90 | * @return User name of a test database. 91 | */ 92 | public static String user() { 93 | return USER; 94 | } 95 | 96 | /** 97 | * Returns the password that a test database authenticates, specified as 98 | * {@code PASSWORD} in the "config.properties" file. 99 | * @return Password of a test database. 100 | */ 101 | public static String password() { 102 | return PASSWORD; 103 | } 104 | 105 | /** 106 | * Returns the maximum duration that a test should wait for a database 107 | * connection to be created, specified as a number of seconds with 108 | * {@code connectTimeout} in the "config.properties" file 109 | * @return Connection timeout of a test database. 110 | */ 111 | public static Duration connectTimeout() { 112 | return CONNECT_TIMEOUT; 113 | } 114 | 115 | /** 116 | * Returns the maximum duration that a test should wait for a SQL command 117 | * to execute, specified as a number of seconds with {@code sqlTimeout} in 118 | * "config.properties" file. 119 | * @return Connection timeout of a test database. 120 | */ 121 | public static Duration sqlTimeout() { 122 | return SQL_TIMEOUT; 123 | } 124 | 125 | /** 126 | *

127 | * Returns a publisher that emits a newly created {@code Connection}. The 128 | * connection is created according to the the values specified in the 129 | * "config.properties" file. 130 | *

131 | * To reduce test latency, {@link #sharedConnection()} should be used in 132 | * favor of this method whenever it is possible to do so. This method 133 | * should only be used when a test requires more than one connection, or if 134 | * a test must verify the {@link Connection#close()} method. 135 | *

136 | * @return A publisher of a newly created connection to a test database. 137 | */ 138 | public static Publisher newConnection() { 139 | return CONNECTION_FACTORY.create(); 140 | } 141 | 142 | /** 143 | *

144 | * Returns a publisher that emits a shared {@code Connection}. The 145 | * shared connection is created according to the the values specified in the 146 | * "config.properties" file. 147 | *

148 | * Tests which use the shared connection method can eliminate the latency of 149 | * creating a new connection. 150 | *

151 | * Each call to this method returns a publisher that emits the same 152 | * connection. This method is not suitable for tests that require 153 | * multiple connections, or tests that verify the 154 | * {@link Connection#close()} method. 155 | *

156 | * 157 | * @implNote The shared connection is closed abruptly when the JVM is 158 | * shutdown and closes the shared connection's network socket. An Oracle 159 | * Database will detect the closed socket and deallocate resources 160 | * that were allocated for the connection's session. 161 | * TODO: Close the shared connection by calling {@link Connection#close()}. 162 | * The call to close() should happen after JUnit has run the last test in 163 | * a suite of tests. 164 | * 165 | * @return A publisher of a shared connection to a test database. 166 | */ 167 | public static Publisher sharedConnection() { 168 | // Check this property in case tests should run without sharing a connection 169 | return Boolean.getBoolean("oracle.r2dbc.test.disableSharedConnection") 170 | ? newConnection() 171 | : SHARED_CONNECTION_FACTORY.create(); 172 | } 173 | 174 | /** 175 | * Returns the major version number of the database specified in the 176 | * "config.properties" file. Tests can call this method to determine if the 177 | * database is expected to support a particular feature, such as the JSON 178 | * column type that was introduced in version 21.1 of Oracle Database. 179 | * @return The major version number of the test database. 180 | */ 181 | public static int databaseVersion() { 182 | try ( 183 | var jdbcConnection = 184 | DriverManager.getConnection(jdbcUrl(), user(), password())) { 185 | return jdbcConnection.getMetaData().getDatabaseMajorVersion(); 186 | } 187 | catch (SQLException sqlException) { 188 | throw new AssertionError(sqlException); 189 | } 190 | } 191 | 192 | /** 193 | * Returns an Oracle JDBC URL for opening connections to the test database. 194 | * @return URL for the Oracle JDBC Driver. Not null. 195 | */ 196 | public static String jdbcUrl() { 197 | return String.format( 198 | "jdbc:oracle:thin:@%s%s:%d/%s", 199 | protocol() == null ? "" : protocol() + ":", 200 | host(), port(), serviceName()); 201 | } 202 | 203 | /** 204 | * Returns the major version number of the Oracle JDBC Driver installed as 205 | * a service provider for java.sql.Driver. 206 | * @return The major version number, such as 21 or 23. 207 | */ 208 | public static int jdbcVersion() { 209 | try { 210 | return DriverManager.getDriver("jdbc:oracle:thin:").getMajorVersion(); 211 | } 212 | catch (SQLException sqlException) { 213 | throw new AssertionError(sqlException); 214 | } 215 | } 216 | 217 | /** 218 | * Returns the minor version number of the Oracle JDBC Driver installed as 219 | * a service provider for java.sql.Driver. 220 | * @return The major version number, such as 11 for 21.11, or 4 for 23.4. 221 | */ 222 | public static int jdbcMinorVersion() { 223 | try { 224 | return DriverManager.getDriver("jdbc:oracle:thin:").getMinorVersion(); 225 | } 226 | catch (SQLException sqlException) { 227 | throw new AssertionError(sqlException); 228 | } 229 | } 230 | 231 | /** 232 | * Returns the options parsed from the "config.properties" resource. 233 | */ 234 | public static ConnectionFactoryOptions connectionFactoryOptions() { 235 | 236 | ConnectionFactoryOptions.Builder optionsBuilder = 237 | ConnectionFactoryOptions.builder() 238 | .option(ConnectionFactoryOptions.DRIVER, "oracle") 239 | .option(ConnectionFactoryOptions.HOST, HOST) 240 | .option(ConnectionFactoryOptions.PORT, PORT) 241 | .option(ConnectionFactoryOptions.DATABASE, SERVICE_NAME) 242 | .option(ConnectionFactoryOptions.USER, USER) 243 | .option(ConnectionFactoryOptions.PASSWORD, PASSWORD) 244 | // Disable statement caching in order to verify cursor closing; 245 | // Cached statements don't close their cursors 246 | .option(Option.valueOf( 247 | OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE), 248 | 0) 249 | // Disable out-of-band breaks to support testing with the 18.x 250 | // database. The 19.x database will automatically detect when it's 251 | // running on a system where OOB is not supported, but the 18.x 252 | // database does not do this and so statement timeout tests will 253 | // hang if the database system does not support OOB 254 | .option(Option.valueOf( 255 | OracleConnection.CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK), 256 | "true"); 257 | 258 | if (PROTOCOL != null) 259 | optionsBuilder.option(ConnectionFactoryOptions.PROTOCOL, PROTOCOL); 260 | 261 | return optionsBuilder.build(); 262 | } 263 | 264 | private static final String PROTOCOL; 265 | private static final String HOST; 266 | private static final int PORT; 267 | private static final String SERVICE_NAME; 268 | private static final String USER; 269 | private static final String PASSWORD; 270 | private static final Duration CONNECT_TIMEOUT; 271 | private static final Duration SQL_TIMEOUT; 272 | private static final ConnectionFactory CONNECTION_FACTORY; 273 | private static final ConnectionFactory SHARED_CONNECTION_FACTORY; 274 | 275 | private static final String CONFIG_FILE_NAME = "config.properties"; 276 | static { 277 | try (InputStream inputStream = 278 | DatabaseConfig.class.getClassLoader() 279 | .getResourceAsStream(CONFIG_FILE_NAME)) { 280 | 281 | if (inputStream == null) { 282 | throw new FileNotFoundException( 283 | CONFIG_FILE_NAME + " resource not found. " + 284 | "Check if it exists under src/test/resources/"); 285 | } 286 | 287 | Properties prop = new Properties(); 288 | prop.load(inputStream); 289 | 290 | HOST = prop.getProperty("HOST"); 291 | PORT = Integer.parseInt(prop.getProperty("PORT")); 292 | SERVICE_NAME = prop.getProperty("DATABASE"); 293 | USER = prop.getProperty("USER"); 294 | PASSWORD = prop.getProperty("PASSWORD"); 295 | CONNECT_TIMEOUT = Duration.ofSeconds( 296 | Long.parseLong(prop.getProperty("CONNECT_TIMEOUT"))); 297 | SQL_TIMEOUT = Duration.ofSeconds( 298 | Long.parseLong(prop.getProperty("SQL_TIMEOUT"))); 299 | PROTOCOL = prop.getProperty("PROTOCOL"); 300 | 301 | CONNECTION_FACTORY = ConnectionFactories.get(connectionFactoryOptions()); 302 | SHARED_CONNECTION_FACTORY = new SharedConnectionFactory( 303 | CONNECTION_FACTORY.create(), 304 | CONNECTION_FACTORY.getMetadata()); 305 | } 306 | catch (Throwable initializationFailure) { 307 | // Most test cases require a database connection; If it can't be 308 | // configured, then the test run can not proceed. Print the failure and 309 | // terminate the JVM: 310 | initializationFailure.printStackTrace(); 311 | System.exit(-1); 312 | 313 | // This throw is dead code; exit(-1) doesn't return. This throw helps 314 | // javac to understand that the final fields are always initialized when 315 | // this static initialization block returns successfully. 316 | throw new RuntimeException(initializationFailure); 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/test/java/oracle/r2dbc/test/OracleTestKit.java: -------------------------------------------------------------------------------- 1 | /* 2 | Reactive Relational Database Connectivity 3 | Copyright 2017-2018 the original author or authors. 4 | 5 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 6 | 7 | This software is dual-licensed to you under the Universal Permissive License 8 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 9 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 10 | either license. 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); 13 | you may not use this file except in compliance with the License. 14 | You may obtain a copy of the License at 15 | 16 | https://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, 20 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | See the License for the specific language governing permissions and 22 | limitations under the License. 23 | */ 24 | 25 | package oracle.r2dbc.test; 26 | 27 | import io.r2dbc.spi.Connection; 28 | import io.r2dbc.spi.ConnectionFactories; 29 | import io.r2dbc.spi.ConnectionFactory; 30 | import io.r2dbc.spi.ConnectionFactoryOptions; 31 | import io.r2dbc.spi.R2dbcNonTransientException; 32 | import io.r2dbc.spi.Result; 33 | import io.r2dbc.spi.Row; 34 | import io.r2dbc.spi.Statement; 35 | import io.r2dbc.spi.test.TestKit; 36 | import oracle.jdbc.datasource.OracleDataSource; 37 | import org.junit.jupiter.api.Disabled; 38 | import org.junit.jupiter.api.Test; 39 | import org.springframework.jdbc.core.JdbcOperations; 40 | import org.springframework.jdbc.core.JdbcTemplate; 41 | import reactor.core.publisher.Flux; 42 | import reactor.core.publisher.Mono; 43 | import reactor.test.StepVerifier; 44 | 45 | import java.math.BigDecimal; 46 | import java.sql.SQLException; 47 | import java.util.Arrays; 48 | import java.util.Optional; 49 | import java.util.function.Function; 50 | import java.util.stream.IntStream; 51 | 52 | import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; 53 | import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; 54 | import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; 55 | import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; 56 | import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; 57 | import static io.r2dbc.spi.ConnectionFactoryOptions.USER; 58 | import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions; 59 | import static oracle.r2dbc.test.DatabaseConfig.host; 60 | import static oracle.r2dbc.test.DatabaseConfig.jdbcUrl; 61 | import static oracle.r2dbc.test.DatabaseConfig.password; 62 | import static oracle.r2dbc.test.DatabaseConfig.port; 63 | import static oracle.r2dbc.test.DatabaseConfig.protocol; 64 | import static oracle.r2dbc.test.DatabaseConfig.serviceName; 65 | import static oracle.r2dbc.test.DatabaseConfig.user; 66 | 67 | /** 68 | *

69 | * Subclass implementation of the R2DBC {@link TestKit} for Oracle Database. 70 | * This test kit implementation overrides super class test methods that are 71 | * fundamentally incompatible with Oracle Database. The javadoc of each 72 | * overridden test method describes why it must be overridden when interacting 73 | * with an Oracle Database. 74 | *

75 | * The developers of the Oracle R2DBC Driver are mindful of the fact that 76 | * distributing a non-compliant implementation of the SPI would create 77 | * confusion about what behavior is to be expected from an R2DBC driver. To 78 | * avoid this confusion, we exercised our best judgement when determining if 79 | * it would be acceptable to override any test case of the R2DBC SPI TCK. It 80 | * should only be acceptable to do so when the behavior verified by the 81 | * overriding method would still be correct according the written specification 82 | * of the R2DBC SPI. 83 | *

84 | * If you think that our judgement was incorrect, then we strongly encourage 85 | * you to bring this to our attention. The easiest way to contact us is through 86 | * GitHub. 87 | *

88 | * 89 | * @author harayuanwang, Michael-A-McMahon 90 | * @since 0.1.0 91 | */ 92 | public class OracleTestKit implements TestKit { 93 | 94 | private final JdbcOperations jdbcOperations; 95 | { 96 | try { 97 | OracleDataSource dataSource = 98 | new oracle.jdbc.datasource.impl.OracleDataSource(); 99 | dataSource.setURL(jdbcUrl()); 100 | dataSource.setUser(user()); 101 | dataSource.setPassword(password()); 102 | this.jdbcOperations = new JdbcTemplate(dataSource); 103 | } 104 | catch (SQLException sqlException) { 105 | throw new RuntimeException(sqlException); 106 | } 107 | } 108 | 109 | private final ConnectionFactory connectionFactory = 110 | ConnectionFactories.get(connectionFactoryOptions()); 111 | 112 | public JdbcOperations getJdbcOperations() { 113 | return jdbcOperations; 114 | } 115 | 116 | @Override 117 | public ConnectionFactory getConnectionFactory() { 118 | return connectionFactory; 119 | } 120 | 121 | /** 122 | * {@inheritDoc} 123 | *

124 | * Overrides the standard {@link TestKit} implementation for compatibility 125 | * with Oracle Database. 126 | *

127 | * Oracle Database converts unquoted alias values to uppercase. This 128 | * conflicts with the {@link #rowMetadata()} test where unquoted aliases are 129 | * expected to retain their original lower case values. To resolve this 130 | * conflict, the SQL statement returned for 131 | * {@link TestStatement#SELECT_VALUE_ALIASED_COLUMNS} enquotes the alias 132 | * names that appear in in the {@code SELECT} statement. 133 | *

134 | * @param statement 135 | * @return 136 | */ 137 | @Override 138 | public String doGetSql(TestStatement statement) { 139 | switch (statement) { 140 | case SELECT_VALUE_ALIASED_COLUMNS: 141 | // Enquote alias names to retain their lower case values 142 | return "SELECT col1 AS \"b\", col1 AS \"c\", col1 AS \"a\"" + 143 | " FROM test_two_column"; 144 | case SELECT_VALUE: 145 | // Use ORDER BY to return rows in a consistent order 146 | return "SELECT test_value FROM test ORDER BY test_value"; 147 | case CREATE_TABLE_AUTOGENERATED_KEY: 148 | // For Oracle SQL, need to declare "GENERATED ALWAYS AS" 149 | return "CREATE TABLE test (" + 150 | "id NUMBER GENERATED ALWAYS AS IDENTITY, test_value NUMBER)"; 151 | case INSERT_VALUE_AUTOGENERATED_KEY: 152 | // For Oracle SQL, the column name must be specified: test(test_value) 153 | return "INSERT INTO test(test_value) VALUES(100)"; 154 | default: 155 | return statement.getSql(); 156 | } 157 | } 158 | 159 | /** 160 | * {@inheritDoc} 161 | *

162 | * Overrides the standard {@link TestKit} implementation for compatibility 163 | * with Oracle Database. 164 | *

165 | * Oracle Database does not support the {@code INTEGER} datatype declared 166 | * with {@link TestStatement#CREATE_TABLE}, 167 | * {@link TestStatement#CREATE_TABLE_AUTOGENERATED_KEY}, and 168 | * {@link TestStatement#CREATE_TABLE_TWO_COLUMNS}. When the {@code INTEGER} 169 | * type appears in these {@code CREATE TABLE} statements, Oracle Database 170 | * creates columns of type {@code NUMBER(38)}, as specified in Oracle's 171 | * 172 | * SQL Language Reference 173 | * . Oracle R2DBC converts NUMBER values into {@link BigDecimal} 174 | * objects, and this conflicts with the {@code TestKit} verifications that 175 | * expect the standard mapping of {@code INTEGER} to {@link Integer}. The 176 | * {@code extractColumn} method is overridden to resolve this conflict by 177 | * converting {@code BigDecimal} objects to {@code int}. 178 | *

179 | */ 180 | @Override 181 | public Object extractColumn(Row row) { 182 | return extractColumn("test_value", row); 183 | } 184 | 185 | /** 186 | * Extracts the value of a column identified by {@code name} from a 187 | * {@code row}. The extracted value is converted as specified by 188 | * {@link OracleTestKit#extractColumn(Row)}. 189 | * @param name Column name 190 | * @return Column value 191 | */ 192 | private Object extractColumn(String name, Row row) { 193 | Object value = row.get(name); 194 | 195 | if (value instanceof BigDecimal) 196 | return ((BigDecimal)value).intValue(); 197 | else 198 | return value; 199 | } 200 | 201 | /** 202 | * {@inheritDoc} 203 | *

204 | * Override the default implementation to extract multiple update counts 205 | * from a single {@code result} and return a {@code Mono} that emits the 206 | * sum of all update counts. 207 | *

208 | */ 209 | @Override 210 | public Mono extractRowsUpdated(Result result) { 211 | return Flux.from(result.getRowsUpdated()) 212 | .reduce(0L, (total, updateCount) -> total + updateCount); 213 | } 214 | 215 | @Override 216 | public String getPlaceholder(int index) { 217 | return String.format(":%d", index + 1); 218 | } 219 | 220 | @Override 221 | public Integer getIdentifier(int index) { 222 | return index; 223 | } 224 | 225 | 226 | /** 227 | * {@inheritDoc} 228 | *

229 | * Overrides the default implementation to use 230 | * {@link #extractColumn(String, Row)} rather than {@link Row#get(String)}. 231 | * This override is necessary because {@link Row#get(String)} returns 232 | * an instance of {@link BigDecimal} for columns declared as {@code INTEGER} 233 | * by {@link TestStatement#INSERT_TWO_COLUMNS}. See 234 | * {@link OracleTestKit#extractColumn(Row)} for details. 235 | *

236 | * This override does not prevent this test from verifying a case-insensitive 237 | * name match is implemented by Row.get(String) when duplicate column names 238 | * are present. 239 | */ 240 | @Test 241 | public void duplicateColumnNames() { 242 | getJdbcOperations().execute(expand(TestStatement.INSERT_TWO_COLUMNS)); 243 | 244 | Mono.from(getConnectionFactory().create()) 245 | .flatMapMany(connection -> Flux.from(connection 246 | 247 | .createStatement(expand(TestStatement.SELECT_VALUE_TWO_COLUMNS)) 248 | .execute()) 249 | 250 | .flatMap(result -> result 251 | .map((row, rowMetadata) -> Arrays.asList( 252 | extractColumn("test_value", row), 253 | extractColumn("TEST_VALUE", row)))) 254 | .flatMapIterable(Function.identity()) 255 | 256 | .concatWith(close(connection))) 257 | .as(StepVerifier::create) 258 | .expectNext(100).as("value from col1") 259 | .expectNext(100).as("value from col1 (upper case)") 260 | .verifyComplete(); 261 | } 262 | 263 | /** 264 | * {@inheritDoc} 265 | *

266 | * Overrides the default implementation to expect 10 {@link io.r2dbc.spi.Result.UpdateCount} 267 | * segments from a single {@code Result}. The default implementation expects 268 | * 10 {@code Result}s each with a single {@code UpdateCount}. Batch DML 269 | * execution is a single call to Oracle Database, and so Oracle R2DBC 270 | * returns a single {@code Result} 271 | *

272 | */ 273 | @Override 274 | @Test 275 | public void prepareStatement() { 276 | Flux.usingWhen(getConnectionFactory().create(), 277 | connection -> { 278 | Statement statement = connection.createStatement(expand(TestStatement.INSERT_VALUE_PLACEHOLDER, getPlaceholder(0))); 279 | 280 | IntStream.range(0, 10) 281 | .forEach(i -> { 282 | TestKit.bind(statement, getIdentifier(0), i); 283 | 284 | if (i != 9) { 285 | statement.add(); 286 | } 287 | }); 288 | 289 | // The original TestKit implementation is modified below to call 290 | // Result.getRowsUpdated(), which returns a Publisher of 10 291 | // UpdateCount segments. 292 | return Flux.from(statement 293 | .execute()) 294 | .flatMap(Result::getRowsUpdated); 295 | }, 296 | Connection::close) 297 | .as(StepVerifier::create) 298 | .expectNextCount(10).as("values from insertions") 299 | .verifyComplete(); 300 | } 301 | 302 | @Disabled("Compound statements are not supported by Oracle Database") 303 | @Test 304 | @Override 305 | public void compoundStatement() {} 306 | 307 | static Mono close(Connection connection) { 308 | return Mono.from(connection.close()) 309 | .then(Mono.empty()); 310 | } 311 | 312 | } 313 | 314 | /* 315 | MODIFIED (MM/DD/YY) 316 | Michael-A-McMahon 09/22/20 - Blob and Clob tests 317 | harayuanwang 05/12/20 - Creation 318 | */ 319 | 320 | -------------------------------------------------------------------------------- /src/test/java/oracle/r2dbc/test/TestUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2022, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | package oracle.r2dbc.test; 22 | 23 | import io.r2dbc.spi.ColumnMetadata; 24 | import io.r2dbc.spi.Connection; 25 | import io.r2dbc.spi.Parameters; 26 | import io.r2dbc.spi.Statement; 27 | import oracle.r2dbc.OracleR2dbcObject; 28 | import oracle.r2dbc.OracleR2dbcTypes; 29 | import reactor.core.publisher.Flux; 30 | 31 | import java.util.Arrays; 32 | import java.util.stream.Collectors; 33 | 34 | import static java.lang.String.format; 35 | import static oracle.r2dbc.util.Awaits.awaitOne; 36 | 37 | public class TestUtils { 38 | 39 | /** 40 | * Queries the {@code user_errors} data dictionary view and prints all rows. 41 | * When writing new tests that declare a PL/SQL procedure or function, 42 | * "ORA-17110: executed completed with a warning" results if the PL/SQL has 43 | * a syntax error. The error details will be printed by calling this method. 44 | */ 45 | public static void showErrors(Connection connection) { 46 | Flux.from(connection.createStatement( 47 | "SELECT * FROM user_errors ORDER BY sequence") 48 | .execute()) 49 | .flatMap(result -> 50 | result.map((row, metadata) -> 51 | metadata.getColumnMetadatas() 52 | .stream() 53 | .map(ColumnMetadata::getName) 54 | .map(name -> name + ": " + row.get(name)) 55 | .collect(Collectors.joining("\n")))) 56 | .toStream() 57 | .map(errorText -> "\n" + errorText) 58 | .forEach(System.err::println); 59 | } 60 | 61 | /** 62 | * Constructs an OBJECT of a given {@code objectType} with the given attribute 63 | * {@code attributeValues}. 64 | */ 65 | public static OracleR2dbcObject constructObject( 66 | Connection connection, OracleR2dbcTypes.ObjectType objectType, 67 | Object... attributeValues) { 68 | 69 | Statement constructor = connection.createStatement(format( 70 | "{? = call %s(%s)}", 71 | objectType.getName(), 72 | Arrays.stream(attributeValues) 73 | .map(value -> 74 | // Bind the NULL literal, as SQL type of the bind value can not be 75 | // inferred from a null value 76 | value == null ? "NULL" : "?") 77 | .collect(Collectors.joining(",")))); 78 | 79 | constructor.bind(0, Parameters.out(objectType)); 80 | 81 | for (int i = 0; i < attributeValues.length; i++) { 82 | if (attributeValues[i] != null) 83 | constructor.bind(i + 1, attributeValues[i]); 84 | } 85 | 86 | return awaitOne(Flux.from(constructor.execute()) 87 | .flatMap(result -> 88 | result.map(row -> row.get(0, OracleR2dbcObject.class)))); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/test/java/oracle/r2dbc/util/Awaits.java: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2020, 2021, Oracle and/or its affiliates. 3 | 4 | This software is dual-licensed to you under the Universal Permissive License 5 | (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 6 | 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose 7 | either license. 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | 13 | https://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | */ 21 | 22 | package oracle.r2dbc.util; 23 | 24 | import io.r2dbc.spi.Result; 25 | import io.r2dbc.spi.Statement; 26 | import oracle.r2dbc.test.DatabaseConfig; 27 | import org.reactivestreams.Publisher; 28 | import reactor.core.publisher.Flux; 29 | import reactor.core.publisher.Mono; 30 | 31 | import java.util.List; 32 | import java.util.concurrent.atomic.AtomicReference; 33 | import java.util.function.Consumer; 34 | import java.util.function.Function; 35 | 36 | import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout; 37 | import static org.junit.jupiter.api.Assertions.assertEquals; 38 | import static org.junit.jupiter.api.Assertions.assertNull; 39 | import static org.junit.jupiter.api.Assertions.assertThrows; 40 | 41 | /** 42 | * This class implements methods that await, or block, until an asynchronous 43 | * operation has completed. Test cases use these methods to execute asynchronous 44 | * operations in a consistent order. The maximum time that a method will spend 45 | * blocked is configured by {@link DatabaseConfig#sqlTimeout()}. 46 | */ 47 | public final class Awaits { 48 | 49 | private Awaits() {/* This class only defines static methods*/} 50 | 51 | /** 52 | *

53 | * Subscribes to an {@code emptyPublisher} and tries to block until the 54 | * publisher emits {@code onComplete}. This method verifies that the 55 | * publisher does not emit {@code onNext}. If {@code emptyPublisher} emits 56 | * {@code onError}, this method invokes {@link Throwable#printStackTrace()} 57 | * on the error and then returns normally 58 | *

59 | * This method is useful in the scope of a {@code finally} block, where a 60 | * throwing an exception will obtrude the processing of any {@code Throwable} 61 | * thrown from the {@code try} block, like this: 62 | *

 63 |    *   try {
 64 |    *     throw new RuntimeException("Try Block Throws");
 65 |    *   }
 66 |    *   finally {
 67 |    *     throw new RuntimeException("Finally Block Throws");
 68 |    *   }
 69 |    * 

70 | * If the code above is executed, only the finally block's RuntimeException 71 | * will be thrown. When a test cases fails within a {@code try} block, it 72 | * throws an {@link AssertionError} with useful information; That 73 | * information is lost if the {@code finally} block throws as well. 74 | *

75 | * @param emptyPublisher A publisher that emits no values. 76 | */ 77 | public static void tryAwaitNone(Publisher emptyPublisher) { 78 | try { 79 | awaitNone(emptyPublisher); 80 | } 81 | catch (Throwable throwable) { 82 | throwable.printStackTrace(); 83 | } 84 | } 85 | 86 | /** 87 | * Subscribes to an {@code emptyPublisher} and blocks until the publisher 88 | * emits {@code onComplete}. This method verifies that the publisher does 89 | * not emit {@code onNext}. 90 | * @param emptyPublisher A publisher that emits no values. 91 | * @throws Throwable If the publisher emits {@code onError}. 92 | */ 93 | public static void awaitNone(Publisher emptyPublisher) { 94 | assertNull( 95 | Mono.from(emptyPublisher).block(sqlTimeout()), 96 | "Unexpected onNext signal from Publisher of no values"); 97 | } 98 | 99 | /** 100 | * Subscribes to an {@code errorPublisher} and blocks until the publisher 101 | * emits {@code onError} with an {@code errorType}. This method verifies that 102 | * the publisher does not emit {@code onComplete}. 103 | * @param errorPublisher A publisher that emits an error. 104 | * @throws Throwable If the publisher emits {@code onError} with a 105 | * {@code Throwable} that is not an instance of {@code errorType}. 106 | */ 107 | public static T awaitError( 108 | Class errorType, Publisher errorPublisher) { 109 | return assertThrows( 110 | errorType, 111 | () -> Flux.from(errorPublisher).blockLast(sqlTimeout()), 112 | "Unexpected signal from Publisher of an error"); 113 | } 114 | 115 | /** 116 | * Subscribes to an {@code singlePublisher} and blocks until the publisher 117 | * emits {@code onNext} and then {@code onComplete}. This method verifies 118 | * that the publisher does not more than one {@code onNext} signal with the 119 | * {@code expectedValue}. 120 | * @param expectedValue Value that the publisher emits 121 | * @param singlePublisher A publisher that emits one value. 122 | * @throws Throwable If the publisher emits {@code onError} or does not 123 | * emit one {@code onNext} signal. 124 | */ 125 | public static void awaitOne(T expectedValue, Publisher singlePublisher) { 126 | assertEquals(expectedValue, awaitOne(singlePublisher), 127 | "Unexpected onNext signal from Publisher of a single value"); 128 | } 129 | 130 | /** 131 | * Subscribes to an {@code singlePublisher} and blocks until the publisher 132 | * emits {@code onNext} and then {@code onComplete}. 133 | * @param singlePublisher A publisher that emits one value. 134 | * @return An item emitted with {@code onNext}, or null if the publisher 135 | * emits no item. 136 | * @throws Throwable If the publisher emits {@code onError} or does not 137 | * emit one {@code onNext} signal. 138 | */ 139 | public static T awaitOne(Publisher singlePublisher) { 140 | AtomicReference result = new AtomicReference<>(null); 141 | consumeOne(singlePublisher, result::set); 142 | return result.get(); 143 | } 144 | 145 | /** 146 | * Subscribes to an {@code singlePublisher} and blocks until the publisher 147 | * emits {@code onNext} and then {@code onComplete}. Values emitted to 148 | * {@code onNext} are input to a {@code consumer}. This is for cases where 149 | * a {@code Publisher} does not emit {@code onNext} or {@code onComplete} 150 | * until a previous value has been consumed, which is the case for 151 | * {@code OracleStatementImpl.execute()}. 152 | * @param singlePublisher A publisher that emits one value. 153 | * @return An item emitted with {@code onNext}, or null if the publisher 154 | * emits no item. 155 | * @throws Throwable If the publisher emits {@code onError} or does not 156 | * emit one {@code onNext} signal. 157 | */ 158 | public static void consumeOne( 159 | Publisher singlePublisher, Consumer consumer) { 160 | assertEquals(1, Flux.from(singlePublisher) 161 | .doOnNext(consumer) 162 | .collectList() 163 | .block(sqlTimeout()) 164 | .size()); 165 | } 166 | 167 | /** 168 | * Subscribes to an {@code multiPublisher} and blocks until the publisher 169 | * emits 0 or more {@code onNext} signals and then {@code onComplete}. This 170 | * method verifies that the publisher emits {@code onNext} signals with the 171 | * same values in the same order as the list of {@code expectedValues}. 172 | * @param expectedValues Values that the publisher emits 173 | * @param multiPublisher A publisher that emits 0 or more values. 174 | * @throws Throwable If the publisher emits {@code onError}. 175 | */ 176 | public static void awaitMany( 177 | List expectedValues, Publisher multiPublisher) { 178 | assertEquals(expectedValues, awaitMany(multiPublisher), 179 | "Unexpected onNext signals from Publisher of multiple values"); 180 | } 181 | 182 | /** 183 | * Subscribes to an {@code multiPublisher} and blocks until the publisher 184 | * emits 0 or more {@code onNext} signals and then {@code onComplete}. 185 | * @param multiPublisher A publisher that emits 0 or more values. 186 | * @return A list of items emitted with {@code onNext}. 187 | * @throws Throwable If the publisher emits {@code onError}. 188 | */ 189 | public static List awaitMany(Publisher multiPublisher) { 190 | return Flux.from(multiPublisher).collectList().block(sqlTimeout()); 191 | } 192 | 193 | /** 194 | *

195 | * Executes a {@code statement} and tries to blocks until the execution 196 | * completes. This method verifies that the execution produces a 197 | * {@link Result} with a count of zero updated rows. If 198 | * {@link Statement#execute()} emits {@code onError}, this method invokes 199 | * {@link Throwable#printStackTrace()} on the error and then returns normally 200 | *

201 | * This method is useful in the scope of a {@code finally} block, where a 202 | * throwing an exception will obtrude the processing of any {@code Throwable} 203 | * thrown from the {@code try} block, like this: 204 | *

205 |    *   try {
206 |    *     throw new RuntimeException("Try Block Throws");
207 |    *   }
208 |    *   finally {
209 |    *     throw new RuntimeException("Finally Block Throws");
210 |    *   }
211 |    * 

212 | * If the code above is executed, only the finally block's RuntimeException 213 | * will be thrown. When a test cases fails within a {@code try} block, it 214 | * throws an {@link AssertionError} with useful information; That 215 | * information is lost if the {@code finally} block throws as well. 216 | *

217 | * @param statement A statement that updates zero rows. 218 | */ 219 | public static void tryAwaitExecution(Statement statement) { 220 | try { 221 | awaitExecution(statement); 222 | } 223 | catch (Throwable throwable) { 224 | throwable.printStackTrace(); 225 | } 226 | } 227 | 228 | /** 229 | * Executes a {@code statement} and blocks until the execution 230 | * completes. This method verifies that the execution produces a 231 | * {@link Result} with a count of zero updated rows. 232 | * @param statement A statement that updates zero rows. 233 | * @throws Throwable If the statement execution results in an error. 234 | */ 235 | public static void awaitExecution(Statement statement) { 236 | awaitNone(Flux.from(statement.execute()) 237 | .flatMap(Result::getRowsUpdated) 238 | // Don't emit an update count of 0, which is the expected return value of 239 | // Oracle JDBC's implementation of Statement.getUpdateCount() after 240 | // executing a DDL statement. Oracle R2DBC relies on getUpdateCount() 241 | // to determine the value emitted by getRowsUpdated. 242 | .filter(updateCount -> updateCount != 0)); 243 | } 244 | 245 | /** 246 | * Executes a {@code statement} and blocks until the execution 247 | * completes. This method verifies that the execution produces a 248 | * {@link Result} with an update count that matches an {@code expectedCount}. 249 | * @param expectedCount Expected count of updated rows 250 | * @param statement A statement that updates rows. 251 | * @throws Throwable If the statement execution results in an error. 252 | */ 253 | public static void awaitUpdate(int expectedCount, Statement statement) { 254 | awaitUpdate(List.of(expectedCount), statement); 255 | } 256 | 257 | /** 258 | * Executes a {@code statement} and blocks until the execution 259 | * completes. This method verifies that the execution produces a 260 | * {@link Result} with update counts that match a list of 261 | * {@code expectedCounts}. 262 | * @param expectedCounts Expected counts of updated rows 263 | * @param statement A statement that updates rows. 264 | * @throws Throwable If the statement execution results in an error. 265 | */ 266 | public static void awaitUpdate( 267 | List expectedCounts, Statement statement) { 268 | assertEquals( 269 | expectedCounts, 270 | Flux.from(statement.execute()) 271 | .flatMap(result -> Flux.from(result.getRowsUpdated())) 272 | .map(Math::toIntExact) 273 | .collectList() 274 | .block(sqlTimeout()), 275 | "Unexpected update counts"); 276 | } 277 | 278 | /** 279 | * Executes a {@code statement} and blocks until the execution 280 | * completes. This method verifies that the execution produces a 281 | * {@link Result} with row data that matches a sequence of 282 | * {@code expectedRows}. 283 | * @param expectedRows List of expected row data. 284 | * @param rowMapper Maps {@code Rows} to objects that are expected to match 285 | * the {@code expectedRows}. 286 | * @param statement A statement that queries expectedRows. 287 | * @throws Throwable If the statement execution results in an error. 288 | */ 289 | public static void awaitQuery( 290 | List expectedRows, Function rowMapper, 291 | Statement statement) { 292 | assertEquals( 293 | expectedRows, 294 | Flux.from(statement.execute()) 295 | .concatMap(result -> Flux.from(result.map(rowMapper))) 296 | .collectList() 297 | .block(sqlTimeout()), 298 | "Unexpected row data"); 299 | } 300 | 301 | } 302 | -------------------------------------------------------------------------------- /src/test/resources/example-config.properties: -------------------------------------------------------------------------------- 1 | # Values in this properties file configure how test cases connect to a database. 2 | 3 | # This file contains example values. Create a copy named config.properties in 4 | # /src/test/resources/ and change the example values to actual values for your 5 | # test database. 6 | 7 | # Host name of a test database 8 | HOST=db.host.example.com 9 | 10 | # Port number of a test database 11 | PORT=1521 12 | 13 | # Service name of a test database 14 | DATABASE=db.service.name 15 | 16 | # User name authenticated by a test database 17 | USER=db_user 18 | 19 | # Password authenticated by a test database 20 | PASSWORD=db_password 21 | 22 | # Maximum number of seconds to connect to a test database 23 | CONNECT_TIMEOUT=15 24 | 25 | # Maximum number of seconds to execute SQL on a test database 26 | SQL_TIMEOUT=15 27 | --------------------------------------------------------------------------------