From 8193d45b39a93f6356248c1aeadf8bb41d25249d Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 12 Nov 2021 19:20:12 -0800 Subject: [PATCH 01/26] Add a new job 21.3 --- .github/workflows/build-and-test.yml | 16 ++++++++++++---- .github/workflows/test.sh | 17 ++++++++--------- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index e2ad056..c4d4b9f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -51,11 +51,19 @@ jobs: java-version: 11 - name: Build with Maven run: mvn -B package --file pom.xml -DskipTests=true - # Tests the Oracle R2DBC Driver with an Oracle Database - test: + # Tests the Oracle R2DBC Driver with an 18 XE Oracle Database + test-18-xe: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Test with Oracle Database - run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh + - name: Test with Oracle Database 18 XE + run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 18.4.0 + # Tests the Oracle R2DBC Driver with a 21.3 XE Oracle Database + test-21.3-xe: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Test with Oracle Database 21.3 XE + run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 21.3.0 diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 044cb5b..a7264a7 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -45,13 +45,12 @@ echo "touch $startUpMount/done" > $startUpScripts/99_done.sh # The oracle/docker-images repo is cloned. This repo provides Dockerfiles along # with a handy script to build images of Oracle Database. For now, this script -# is just going to build an 18.4.0 XE image, because this can be done in an -# automated fashion, without having to accept license agreements required by -# newer versions like 19 and 21. -# TODO: Also test with newer database versions +# is just going to build an Express Edition (XE) image, because this can be +# done in an automated fashion. Other editions would require a script to accept +# a license agreement. git clone https://github.com/oracle/docker-images.git cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ -./buildContainerImage.sh -v 18.4.0 -x +./buildContainerImage.sh -v $1 -x # Run the image in a detached container # The startup directory is mounted. It contains a createUser.sql script that @@ -59,7 +58,7 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # database has started. # The database port number, 1521, is mapped to the host system. The Oracle # R2DBC test suite is configured to connect with this port. -docker run --name test_db --detach --rm -p 1521:1521 -v $startUpScripts:$startUpMount oracle/database:18.4.0-xe +docker run --name test_db --detach --rm -p 1521:1521 -v $startUpScripts:$startUpMount oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "done" in the startup directory. When that file exists, it means @@ -72,9 +71,9 @@ do done # Create a configuration file and run the tests. The service name, "xepdb1", -# is always created for the 18.4.0 XE database, but it would probably change -# for other database versions (TODO). The test user is created by the -# startup/01_createUser.sql script +# is always created for the XE database. It would probably change for other +# database editions. The test user is created by the startup/01_createUser.sql +# script cd $GITHUB_WORKSPACE echo "DATABASE=xepdb1" > src/test/resources/config.properties echo "HOST=localhost" >> src/test/resources/config.properties From a9941d5c8f8b106be50c274b3193dd84e8839b68 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 10 Dec 2021 16:59:23 -0800 Subject: [PATCH 02/26] Include minor version in name --- .github/workflows/build-and-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c4d4b9f..b1a9030 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -51,8 +51,8 @@ jobs: java-version: 11 - name: Build with Maven run: mvn -B package --file pom.xml -DskipTests=true - # Tests the Oracle R2DBC Driver with an 18 XE Oracle Database - test-18-xe: + # Tests the Oracle R2DBC Driver with an 18.4 XE Oracle Database + test-18.4-xe: needs: build runs-on: ubuntu-latest steps: From b9bd5cf5f5252ec5ef94117eb4f49a54b774b53b Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 10 Dec 2021 17:10:03 -0800 Subject: [PATCH 03/26] No dots in workflow names --- .github/workflows/build-and-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b1a9030..5a55f46 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -52,18 +52,18 @@ jobs: - name: Build with Maven run: mvn -B package --file pom.xml -DskipTests=true # Tests the Oracle R2DBC Driver with an 18.4 XE Oracle Database - test-18.4-xe: + test-18-4-xe: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Test with Oracle Database 18 XE + - name: Test with Oracle Database 18.4.0 XE run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 18.4.0 # Tests the Oracle R2DBC Driver with a 21.3 XE Oracle Database - test-21.3-xe: + test-21-3-xe: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Test with Oracle Database 21.3 XE + - name: Test with Oracle Database 21.3.0 XE run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 21.3.0 From 544e3eb92c4984d5776e49c16ddb761c9d46ca1a Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 10 Dec 2021 17:59:49 -0800 Subject: [PATCH 04/26] Include version number in file name --- .github/workflows/test.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index a7264a7..d54f12e 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -35,13 +35,13 @@ # it finds under /opt/oracle/scripts/startup. The startup scripts are run # after the database instance is active.A numeric prefix on the script name # determines the order in which scripts are run. The final script, prefixed -# with "99_" will create a file named "done" in the mounted volumn. When the -# "done" file exists, this signals that the database is active and that all -# startup scripts have completed. +# with "99_" will create a file named "$1-ready" in the mounted volume, where +# $1 is the database version number.. When that file exists, this signals that +# the database is active and that all startup scripts have completed. startUpScripts=$PWD/startup startUpMount=/opt/oracle/scripts/startup -echo "touch $startUpMount/done" > $startUpScripts/99_done.sh - +dbReady=$1-ready +echo "touch $startUpMount/$dbReady" > $startUpScripts/99_done.sh # The oracle/docker-images repo is cloned. This repo provides Dockerfiles along # with a handy script to build images of Oracle Database. For now, this script @@ -61,10 +61,11 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ docker run --name test_db --detach --rm -p 1521:1521 -v $startUpScripts:$startUpMount oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create -# a file named "done" in the startup directory. When that file exists, it means -# the database is ready for testing. +# a file named "$1-ready" in the startup directory, where $1 is the database +# version number. When that file exists, it means the database is ready for +# testing. echo "Waiting for database to start..." -until [ -f $startUpScripts/done ] +until [ -f $startUpScripts/$dbReady] do docker logs --since 3s test_db sleep 3 From e3f6fc0d3cae973d48d73005c7ac62036f83b02d Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 10 Dec 2021 18:32:49 -0800 Subject: [PATCH 05/26] Space before ] --- .github/workflows/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index d54f12e..140fa09 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -65,7 +65,7 @@ docker run --name test_db --detach --rm -p 1521:1521 -v $startUpScripts:$startUp # version number. When that file exists, it means the database is ready for # testing. echo "Waiting for database to start..." -until [ -f $startUpScripts/$dbReady] +until [ -f $startUpScripts/$dbReady ] do docker logs --since 3s test_db sleep 3 From e9cccb38e31106c9dd85f5ec2b6cf665e44eeb6f Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 11 Dec 2021 11:38:48 -0800 Subject: [PATCH 06/26] Mount an isolated directory --- .github/workflows/test.sh | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 140fa09..3b0e796 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -35,13 +35,23 @@ # it finds under /opt/oracle/scripts/startup. The startup scripts are run # after the database instance is active.A numeric prefix on the script name # determines the order in which scripts are run. The final script, prefixed -# with "99_" will create a file named "$1-ready" in the mounted volume, where -# $1 is the database version number.. When that file exists, this signals that -# the database is active and that all startup scripts have completed. -startUpScripts=$PWD/startup +# with "99_" will create a file named "ready" in the mounted volume, indicating +# that all scripts have completed and the database is ready for testing. + +# Create directory with the startup scripts. Naming the directory with the +# version number should isolate it from database container that is running +# concurrently, assuming multiple containers are not running the same database +# version. +startUp=$PWD/$1/startup +mkdir -p $startUp +cp $PWD/startup/* $startUp + +# Create the 99_ready.sh script. It will touch a file in the mounted startup +# directory. startUpMount=/opt/oracle/scripts/startup -dbReady=$1-ready -echo "touch $startUpMount/$dbReady" > $startUpScripts/99_done.sh +readyFile=ready +echo "touch $startUpMount/$readyFile" > $startUp/99_ready.sh + # The oracle/docker-images repo is cloned. This repo provides Dockerfiles along # with a handy script to build images of Oracle Database. For now, this script @@ -58,14 +68,14 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # database has started. # The database port number, 1521, is mapped to the host system. The Oracle # R2DBC test suite is configured to connect with this port. -docker run --name test_db --detach --rm -p 1521:1521 -v $startUpScripts:$startUpMount oracle/database:$1-xe +docker run --name test_db --detach --rm -p 1521:1521 -v $startUp:$startUpMount oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "$1-ready" in the startup directory, where $1 is the database # version number. When that file exists, it means the database is ready for # testing. echo "Waiting for database to start..." -until [ -f $startUpScripts/$dbReady ] +until [ -f $startUp/$readyFile ] do docker logs --since 3s test_db sleep 3 From c6df751a820a2b8af89afb45391681f84f1c6ad9 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 13 May 2022 14:57:15 -0700 Subject: [PATCH 07/26] Trying different port numbers --- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/test.sh | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5a55f46..c4be872 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -58,7 +58,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Test with Oracle Database 18.4.0 XE - run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 18.4.0 + run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 18.4.0 50184 # Tests the Oracle R2DBC Driver with a 21.3 XE Oracle Database test-21-3-xe: needs: build @@ -66,4 +66,4 @@ jobs: steps: - uses: actions/checkout@v2 - name: Test with Oracle Database 21.3.0 XE - run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 21.3.0 + run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 21.3.0 50213 diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 3b0e796..513ec7a 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -23,6 +23,13 @@ # execute the Oracle R2DBC test suite with a configuration that has it connect # to that database. # +# The database version is configured by the first parameter. The version is +# expressed is as .. version number, for example: "18.4.0" +# +# The database port number is configured by the second parameter. If multiple +# databases are created by running this script in parallel, then a unique port +# number should be provided for each database. +# # This script makes no attempt to clean up. The docker container is left # running, and the database retains the test user and any other modifications # that the test suite may have performed. @@ -68,7 +75,7 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # database has started. # The database port number, 1521, is mapped to the host system. The Oracle # R2DBC test suite is configured to connect with this port. -docker run --name test_db --detach --rm -p 1521:1521 -v $startUp:$startUpMount oracle/database:$1-xe +docker run --name test_db --detach --rm -p 1521:$2 -v $startUp:$startUpMount oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "$1-ready" in the startup directory, where $1 is the database @@ -88,7 +95,7 @@ done cd $GITHUB_WORKSPACE echo "DATABASE=xepdb1" > src/test/resources/config.properties echo "HOST=localhost" >> src/test/resources/config.properties -echo "PORT=1521" >> src/test/resources/config.properties +echo "PORT=$2" >> src/test/resources/config.properties echo "USER=test" >> src/test/resources/config.properties echo "PASSWORD=test" >> src/test/resources/config.properties echo "CONNECT_TIMEOUT=30" >> src/test/resources/config.properties From e3b71fef86c2d5061b7027dc0d5455f8a19bb1be Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 13 May 2022 16:40:13 -0700 Subject: [PATCH 08/26] Fix -p option, and correct comments --- .github/workflows/.test.sh.swp | Bin 0 -> 16384 bytes .github/workflows/test.sh | 11 +++++------ 2 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/.test.sh.swp diff --git a/.github/workflows/.test.sh.swp b/.github/workflows/.test.sh.swp new file mode 100644 index 0000000000000000000000000000000000000000..1af9c2a4c43c148c9b69ec289c2ccf2ff4293d88 GIT binary patch literal 16384 zcmeHO&5s;M6>m)V2oOMUhy;rCJi8J$%yjQ;fY?sfV!S&}2HU&K&TKXjidHq`EvI2uOg$AHanRNL&IDUqakKa6ll$hlCIkHxQt}0g)32;PUDd(^7P;aU2wY&UT=4tU%m3e`0HO=J-hUvF+SGz0x!&2)kz4INrw~lG98Uz{y8Uz{y z8Uz{y8Uz{y8Uz{y{;v@zvNO)dq2U>;<(7Sa&8hd-+Utky`}V2d?Pc@UAkZMtAkZMt zAkZMtAkZMtAkZMtAkZMtAkZN2KZt+`9Or$I>r;H-$Nm56{Qv6P9Ov(#?}EMqnt&b# z{rIi827L>36Vw5n0Xd+bJ>)n)1$`RyA<(ODah#Vxp9g&o^f>4*Z+4vDfPM}7Ea)QW zG0>miVSUtfa81t)C2tuO^@G#z6_dyu7diY zM?gPE!{jrd1hfSDBk+3>#5jJEvAmC0kopHQr@Tz4(@8`uiJx)U55~9*WFh<+KIAcV zGFo1})cwF}mpX^EtO}7AyV<4c!;Q2|3NjB-c!4OySSWdEx!XCC&JyjcZd_T{j7gPY zAt_Ykr^zIombnb5Nck#}enE3Ql0n4`zh`M)xR^VdNvas?FDWVGvCP}V!nB!HEV9h7 zDKL1Js)E!pp5TaGJ$p+(+5uKIo0(9gHIM->;pb97YZ6ApQpw!RwBSIvLWBumnuk%O zvC<+cKMyl#o%ld)#)`7AnsJ#?5a!S*&5yN+j1N|$LBYB*J6_MMyvqx!n-N)t#Y_}n zwga26LSZbX_?uw7u?TdnMS}Nu<1&nb^-9PA5643)+D@?6k>dwsH0fwo$}@;;STlLU zFquYjkU(;%;+e6NFp|o1@CXd~QJMhPqR7-G&ptKd`e|$*nz0isbAD(B!V8K*UnWYD znC4Q(GSSuym=mjle`S%zz&)UeP_r;iT-t)VImsX_aH(jXmQg^tyjcQDaID}-N%+3Z zco|s#`4aY?rqF>MNn^q6`$t(Wm7>1kM(3XCpEpppX)JvKD;r1iF?cKq%(U|fnu#Ph zKCzf#X`rKYLIypm;B)C}np2X_+lHT!671L+s)*20W)Xr$S*k;TafMHe*x&V+S)Lw- z0bFrIPtZ0XfnigGwDe~wwTiSvOx_ZA-ZC6GIKB#twdivFn|JZzzW#xqSgJ>?=}(Rg zU>&z>UYZq^F^%}jvu+GMYDeZ+S;uq)F#F{R_7*Me-a1B%l!90Twl|NDO$<~4;y6BX z%J$wywUqbXT68Bqz^XdwSV1d+*Yu3#ZZ!!a9m-s>``{zO!Y}h2&K4cFNhnpu>=$tv z6=4QR>aaoZ!KIS4;Al>???6Ju`LD_n3(hv1e^q+FPI}FLxPeyq=yT$_-P&> zpke>{4A1k`;xhJt#w6KEn21V~HX&#y;gJl$OH9D)lpZ!mu|mQx!b9oy*g7%~eLEHD zSkE!g5UT)QY%2(=s7zR*yEwcN%6k84iNR?EwQ4tdk0SUQQhbK|gI&jL2s#4fk@QP= ziAau#voYJ`M3PsQRkhH<&`_<46l_z149);iAu|{_%${9K;UYOKQ6%963aM-cML8I- z)i6vP1hArZNw&$8`l(0WUFsbTPd*|EV$Wh>A(0Mn%r*LeRXv$~$L`^=I5vKl$sBHl zHF#0$@d&=tKA5AL;9ZYp&}?|0)x&tz6kq31JP0S+n~?v(O=m`nYAg<*UV`LbU~9*| z%VmQV+!*b)?HVdXa)NyVtW4EV#V``S&9&Hm5EGIMohn>dkLt5Y6ecVJ8Xs2*LX8?A zcJNH(2uK<6CLnpME|c(PiB(X>Xhjx@xl8d!z3Pkj_g5Zcl#rqTNlyF+3y}{_It=$pr^>%i8o1;O0NLxF!zO}hA z7!9^Iadnk?o6pj`a*rF@F;b7Qifp!y=dR&3%LQ8u&Dr(ekVvIdA5_4WvmvOpQ=X9Q4*Q)|cg4(kHZ4)WBLR##Js@!WG{ea&lW6s{n3bj5^t7ob2qMxs zhO=cSwa!0oWVSYmPWT27=2OKiokI!uE2?D_%l80-OBDpD?^4CP6`kANzJ8v%ci_nt zc&ZdY<>%FP6>h~)UYSK~say47b|#HlQ)#a#MbxqLv`rb>h12gQ*jaOhZF$?eFUkuG zfEuHnoVdCL? zU*i1zR}i27tNZ&eVh5w{R!v$KY)G+`X1=pAnpN7LGJ?n1n2!9fL;Qnpf2bi zIPZTS^djgA=m8L~^Lx;bc)jM;AkZMtAkZMtAkZMtAkZMtAkZLiPYA?^IA`h33!k>o z;l1q(-Qdv;>-wkqgFd2hTbP?bwy8b*$o2iv;6@+E;D);q&xAoA)Ps`lL z%?Q&LaM8HIsk#WIva@k?-t4cB{tFJ;tK5!|mRBA7{(D zMOd|Z>f1M_C%5SIai>e)JkSjWK3Ch`r?=AGH@{Rj!oZi?TG3n_FDB{MHD*22ubXq` zeuHkx!zspSR~!@m+jxQw439QFMKD`8H!ZlgU|Maav7}oE+^0m-QnwpeSY;c)GC#!e zUiXjN)Usf;9f$8=IYcUwGNUd&*{)-hP8DvIs*qa`U9>sD3%5}*-odTEFrtrAXHwtb zIC0B{p0jSeCv;Oqi(aFNPMU_vbzSb$p#>JBS8KYc^&o(yd-SFwg@w7jkI{E)<6H3S z4tL#vc4?10n7V&wn=5ECsW#s(U=i!K*JAmqD_;w%yX3a9&v%N^T0YquH`aFk4R~+v AssI20 literal 0 HcmV?d00001 diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 513ec7a..6551ec7 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -75,17 +75,16 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # database has started. # The database port number, 1521, is mapped to the host system. The Oracle # R2DBC test suite is configured to connect with this port. -docker run --name test_db --detach --rm -p 1521:$2 -v $startUp:$startUpMount oracle/database:$1-xe +docker run --name test_db --detach --rm -p $2:1521 -v $startUp:$startUpMount oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create -# a file named "$1-ready" in the startup directory, where $1 is the database -# version number. When that file exists, it means the database is ready for -# testing. +# a file named "ready" in the startup scripts directory. When that file exists, +# it means the database is ready for testing. echo "Waiting for database to start..." until [ -f $startUp/$readyFile ] do - docker logs --since 3s test_db - sleep 3 + docker logs --since 1s test_db + sleep 1 done # Create a configuration file and run the tests. The service name, "xepdb1", From 4c0bb570c86c33f92f6075f4c8972532b88fff4a Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 13 May 2022 16:44:44 -0700 Subject: [PATCH 09/26] Trying -f option for touch command --- .github/workflows/.test.sh.swp | Bin 16384 -> 16384 bytes .github/workflows/test.sh | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/.test.sh.swp b/.github/workflows/.test.sh.swp index 1af9c2a4c43c148c9b69ec289c2ccf2ff4293d88..304a48324a87e12010e0f70e2bee88ca14311bdc 100644 GIT binary patch delta 137 zcmZo@U~Fh$+#n#p=(kx=;4?q3E&~HYBO{OyU|{f@EU56=Q-_s-K^ur~voJ990I@v~ zzh`D(xB $startUp/99_ready.sh - +echo "touch -f $startUpMount/$readyFile" > $startUp/99_ready.sh # The oracle/docker-images repo is cloned. This repo provides Dockerfiles along # with a handy script to build images of Oracle Database. For now, this script From a7090328c2706eba594a734009ba222634613925 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Fri, 13 May 2022 16:45:54 -0700 Subject: [PATCH 10/26] Ignore vim's swp files --- .github/workflows/.test.sh.swp | Bin 16384 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .github/workflows/.test.sh.swp diff --git a/.github/workflows/.test.sh.swp b/.github/workflows/.test.sh.swp deleted file mode 100644 index 304a48324a87e12010e0f70e2bee88ca14311bdc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHO&5s;M6>m)V2oOMUh(v_+JYESKX1aGaKx`*#81Igg!S?R5Gn-9>qE$_I%}mGr z<*x3XT?qvN;lRgVzy%2jiAx~jONbi?4hR$pkWeJ9L;?lGft)x1zgJb=GhSPk4HtxZ zv(!s-;YYHQuLWZtyi_4`&ou+Qd79st zMCn|4g;a&BW_Met`K>{qLEu&d%3{(v|B&BRe+?dON>`_75q?Z@V=L7+jPL7+jP zL7+jPL7+jPL7+jPL7+jPL7+k4e-Hr?I8Fz0eVz~exc^_B|KE6z4_W@-m@LClRqEe!}4(7~^9g3*pajk;l}@ zXl3nU_hV~a>KxLFDnwrFWf!Z9>uH%3WFDdL0#S&uQ1aqRw{s+&Gqk(5d3i%KCRK)o zq)?HcCX;Yl<}#on$JxlY##oWHkk!6NW zfx)v>6{L>w{6_5R+1vWT4zQ}(%!DGXfed&FKbHbplQ1HdO6F#!1qZ?vB1{0&Jd7fZ zl@?L?d6+@##0P3KR+NR+jLVFIFo#BIeyl}ge6Sh~3f7g`@p@+EJzh}VjL0%9W}*PI zUD$*b3S%+F-vr~0MWAaflH<7ExD2CUqY`q!!|{lUwiB#%B#<1ccxLP*jHL1$JOD#}lqSHnC^B`?v(L=9ej3|HX6yvZoFAEi@PeYymx+=j zrn!`{Otduv=ESPtUsSf%qqEQS&lxD&G?u=Am5rnM7(A8)X4?4#%|sF$ zFD+(R8t5pUkU@_s_*}Y@=9HxKw&7=_1Uq(yDk5~0S%jcbmg*2-T;UTV_ILedmZyhd z09V}96SNITVA#}gq(4ikRiq_{ypgily(4=-{j+PBfM+mcLF0r?GX$RL)Vgwb$I;(*57(kQ@eQo1(~cFi5_nzDSm{=iAkv}C6*~|v5g2}%=Ww^^xJ^Q-GG@<+ z%cux5NKyw5!VgZBqy@)wqP+(aD$amao>*|UIfv9tyJV=+h%rEHO!73YCoH%_T~V-& zE9MK}YA~y0w@4Wld6%|?mYOlUeK;PUG3*7Bk(~mkJ!Iu#=*S@j7uJYZs%TNzMQ8$N zGs$2&$+|SfaZ)yb;QJ|4E6#X#}-uH~Nqw_!^RYh75##$7~5Y0_2hO zOL&P$j*0Uz+hi%tE6b`{XklomRz(UnDnSNkfT)lg3>;?HuBC9X+n=FG!U+_T*$j$u zFkrJ`m^cVvMeUMolPC34kG#Lsdm5g6L>k1d#lk`&9pIR2^Z~1SGW(9*#ba@7{4SF@ z+zM;(qSoUPd?$S{M?Jy&9?PKF@J_3T@v14l&Y^e^PP8{+2LLyn8!f7_xB>MNr2hh& zJ9b_!9IW8RXt!=a;S>V_(Yk@9V}#Ri0!kX-6i;lg@U*CtVzun1^;Tqy`O zYJk|mGm#@8WyITnTG2)jcxi?f52n#BQ=!O+Rsq2Z+D-Jf%Fyn?6(YzgI+~G8xoG2J#8iq} z;syRX;B+LxpDN~?w!o<(N7!w35xtdAGe zQp}Ogwbco)s!p8G=dQ>QRnkrKsb|YUDUhCZM!bu^hPC*xFX~@)SF<)mF!}JwOq6f?e#KRJuPNs={7xW>Is5~ zbe7?4*-5Q)j~kh-O`;{=;K6*Zn5AnmY`{o)IGq)A);~vUt81%m{)-ct4Q=>3DoTVb+Xa$I3Ug( zKL3|EfBzN4=l|;d{>wP)AAq``2SIv z{R8LypMqWlT?Rb>;&uKA`US7oycz@=1R4Yy1R4Yy1R4Yy1R4Yy1nvoe_z>qT-Fe~D z7COAQeW4pXx@TSgRA1;L8n=bH31s`)!%ts37!9uXaV%bcbQMkPOQdpN|94vtef+e{ zecX;PZ2=dJ8=R_(P%1kcN9V2n#^}G`u+tk3_qTU9*IB0fU4C!a-~Ep!?riUl)>qc< zcfF^!hoki<^+m)tyY6@PW^dHP(Z9cbB(q@L#q@iF8n=;Fofxvdq=#qna2Ze3&6GyS?b zXYM!XraYWtjCRE_;lGV1=)mx3(^CYq4Rh0idkd!3W*SSnb-;Z}G%ava%qjaipvs8uLdg!9f30}C7it!F^{)G{JX8FF& z(k&Z$&bslw&`lLBdW|MJX&NTib-7Q67Fdj4t?8oHg8-K9(VLDG7UuRoM>CZ^5rS z+;s!mrG4&T>i(T=uAs@J+I+i!MXcLii{-1Xd@Zc*lH0~U-z!FIdAT=ktnK_8l(+AN diff --git a/.gitignore b/.gitignore index f63f671..383a866 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ /sample/config.properties .github/workflows/startup/99_done.sh .github/workflows/startup/done +*.swp From d7b0585beae79d936303840946dbbedf1d4ccada Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 14 May 2022 17:40:24 -0700 Subject: [PATCH 11/26] Allow non-default config file for tests --- .github/workflows/test.sh | 17 +++++++++-------- .gitignore | 18 ++++++++++++++---- sample/example-config.properties | 9 +++++++-- .../java/oracle/r2dbc/test/DatabaseConfig.java | 8 +++++++- src/test/resources/example-config.properties | 10 +++++++--- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 9bfdb82..d4500f1 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -91,11 +91,12 @@ done # database editions. The test user is created by the startup/01_createUser.sql # script cd $GITHUB_WORKSPACE -echo "DATABASE=xepdb1" > src/test/resources/config.properties -echo "HOST=localhost" >> src/test/resources/config.properties -echo "PORT=$2" >> src/test/resources/config.properties -echo "USER=test" >> src/test/resources/config.properties -echo "PASSWORD=test" >> src/test/resources/config.properties -echo "CONNECT_TIMEOUT=30" >> src/test/resources/config.properties -echo "SQL_TIMEOUT=30" >> src/test/resources/config.properties -mvn clean compile test +echo "Configuration for testing with Oracle Database $1" > src/test/resources/$1.properties +echo "DATABASE=xepdb1" >> src/test/resources/$1.properties +echo "HOST=localhost" >> src/test/resources/$1.properties +echo "PORT=$2" >> src/test/resources/$1.properties +echo "USER=test" >> src/test/resources/$1.properties +echo "PASSWORD=test" >> src/test/resources/$1.properties +echo "CONNECT_TIMEOUT=30" >> src/test/resources/$1.properties +echo "SQL_TIMEOUT=30" >> src/test/resources/$1.properties +mvn -Doracle.r2dbc.config=$1.properties clean compile test diff --git a/.gitignore b/.gitignore index 383a866..604ed5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,19 @@ +# Exclude properties files, as they may contain user/password credentials and +# names of internal systems. Include only example-config.properties files +/src/test/resources/*.properties +/sample/*.properties +!/src/test/resources/example-config.properties +!/sample/example-config.properties + +# Excluded complier output /target/ -.DS_Store -*.iml -/src/test/resources/config.properties /sample/target/ -/sample/config.properties + +# Exclude the scripts and files generated by the test workflow .github/workflows/startup/99_done.sh .github/workflows/startup/done + +# Exclude files generated by the OS and other programs +.DS_Store +*.iml *.swp diff --git a/sample/example-config.properties b/sample/example-config.properties index e3c5c0c..817297a 100644 --- a/sample/example-config.properties +++ b/sample/example-config.properties @@ -1,5 +1,10 @@ -# 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 -# /sample and change the example values to actual values for your test database. +# 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 +# /sample, and then change the example values to actual values +# for your test database. +# The example-config.properties file is versioned controlled. DO NOT TYPE +# SENSITIVE VALUES INTO THE EXAMPLE FILE. # Host name of a test database HOST=db.host.example.com diff --git a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java index e7e4729..ebbd5cb 100644 --- a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java +++ b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java @@ -213,7 +213,13 @@ public static void showErrors(Connection connection) { private static final ConnectionFactory CONNECTION_FACTORY; private static final ConnectionFactory SHARED_CONNECTION_FACTORY; - private static final String CONFIG_FILE_NAME = "config.properties"; + /** + * Name of configuration file. It is "config.properties" by default, but can + * be set to a none default with: -Doracle.r2dbc.config=something-else + */ + private static final String CONFIG_FILE_NAME = + System.getProperty("oracle.r2dbc.config", "config.properties"); + static { try (InputStream inputStream = DatabaseConfig.class.getClassLoader() diff --git a/src/test/resources/example-config.properties b/src/test/resources/example-config.properties index 670bd64..286a199 100755 --- a/src/test/resources/example-config.properties +++ b/src/test/resources/example-config.properties @@ -1,8 +1,12 @@ # Values in this properties file configure how test cases connect to a database. - # This file contains example values. Create a copy named config.properties in -# /src/test/resources/ and change the example values to actual values for your -# test database. +# /src/test/resources/, and then change the example values to actual values +# for your test database. +# The example-config.properties file is versioned controlled. DO NOT TYPE +# SENSITIVE VALUES INTO THE EXAMPLE FILE. +# Running the Oracle R2DBC test suite with -Doracle.r2dbc.config= +# has it read configuration from the named file under src/test/resources, +# rather than from the default "config.properties" file. # Host name of a test database HOST=db.host.example.com From 58108a3515db235b4cea711e643395093481cd25 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 14 May 2022 17:41:40 -0700 Subject: [PATCH 12/26] Nabbed concurrent test fixes --- .../r2dbc/impl/OracleStatementImpl.java | 3621 ++++++++++------- 1 file changed, 2205 insertions(+), 1416 deletions(-) diff --git a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java index d847fd1..946b5ca 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleStatementImpl.java @@ -21,1590 +21,2379 @@ package oracle.r2dbc.impl; -import io.r2dbc.spi.OutParameterMetadata; +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Parameter; +import io.r2dbc.spi.Parameters; import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientException; +import io.r2dbc.spi.R2dbcType; import io.r2dbc.spi.Result; +import io.r2dbc.spi.Result.Message; +import io.r2dbc.spi.Result.UpdateCount; import io.r2dbc.spi.Statement; import io.r2dbc.spi.Type; -import oracle.r2dbc.impl.ReactiveJdbcAdapter.JdbcReadable; -import oracle.r2dbc.impl.ReadablesMetadata.OutParametersMetadataImpl; +import oracle.r2dbc.OracleR2dbcOptions; +import oracle.r2dbc.test.DatabaseConfig; +import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.Signal; -import java.nio.ByteBuffer; -import java.sql.BatchUpdateException; -import java.sql.CallableStatement; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLType; +import java.sql.RowId; import java.sql.SQLWarning; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedList; +import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; -import java.util.function.Function; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; import java.util.stream.IntStream; - -import static java.sql.Statement.KEEP_CURRENT_RESULT; -import static java.sql.Statement.RETURN_GENERATED_KEYS; -import static java.util.Objects.requireNonNullElse; -import static oracle.r2dbc.impl.OracleR2dbcExceptions.fromJdbc; -import static oracle.r2dbc.impl.OracleR2dbcExceptions.newNonTransientException; -import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull; -import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireOpenConnection; -import static oracle.r2dbc.impl.OracleR2dbcExceptions.runJdbc; -import static oracle.r2dbc.impl.OracleReadableImpl.createOutParameters; -import static oracle.r2dbc.impl.OracleReadableMetadataImpl.createParameterMetadata; -import static oracle.r2dbc.impl.OracleResultImpl.createBatchUpdateErrorResult; -import static oracle.r2dbc.impl.OracleResultImpl.createCallResult; -import static oracle.r2dbc.impl.OracleResultImpl.createErrorResult; -import static oracle.r2dbc.impl.OracleResultImpl.createGeneratedValuesResult; -import static oracle.r2dbc.impl.OracleResultImpl.createQueryResult; -import static oracle.r2dbc.impl.OracleResultImpl.createUpdateCountResult; -import static oracle.r2dbc.impl.ReadablesMetadata.createOutParametersMetadata; -import static oracle.r2dbc.impl.SqlTypeMap.toJdbcType; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; +import static oracle.r2dbc.test.DatabaseConfig.host; +import static oracle.r2dbc.test.DatabaseConfig.newConnection; +import static oracle.r2dbc.test.DatabaseConfig.password; +import static oracle.r2dbc.test.DatabaseConfig.port; +import static oracle.r2dbc.test.DatabaseConfig.serviceName; +import static oracle.r2dbc.test.DatabaseConfig.sharedConnection; +import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout; +import static oracle.r2dbc.test.DatabaseConfig.user; +import static oracle.r2dbc.util.Awaits.awaitError; +import static oracle.r2dbc.util.Awaits.awaitExecution; +import static oracle.r2dbc.util.Awaits.awaitMany; +import static oracle.r2dbc.util.Awaits.awaitNone; +import static oracle.r2dbc.util.Awaits.awaitOne; +import static oracle.r2dbc.util.Awaits.awaitQuery; +import static oracle.r2dbc.util.Awaits.awaitUpdate; +import static oracle.r2dbc.util.Awaits.consumeOne; +import static oracle.r2dbc.util.Awaits.tryAwaitExecution; +import static oracle.r2dbc.util.Awaits.tryAwaitNone; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; /** - *

- * Implementation of the {@link Statement} SPI for the Oracle Database. - *

- * This implementation executes SQL using a {@link PreparedStatement} - * from the Oracle JDBC Driver. JDBC API calls are adapted into Reactive - * Streams APIs using a {@link ReactiveJdbcAdapter}. - *

- * - *

Database Cursor Management

- *

- * A cursor is opened each time a new SQL statement is executed on an Oracle - * Database session. If a session never closes it's cursors, it will - * eventually exceed the maximum number of open cursors allowed by the Oracle - * Database and an ORA-01000 error will be raised. The Oracle R2DBC Driver - * closes cursors after all {@link Result}s emitted by the {@link #execute()} - * publisher has been fully consumed. - *

- * To ensure that cursors are eventually closed, application code MUST - * fully consume every {@link Result} object emitted by the {@link #execute()} - * {@code Publisher}. A {@code Result} is fully consumed by first subscribing - * to {@link Result#getRowsUpdated()}, {@link Result#map(BiFunction)}, - * {@link Result#map(Function)}, or {@link Result#flatMap(Function)}, and then - * requesting items until the {@code Publisher} emits {@code onComplete/onError} - * or its {@code Subscription} is cancelled. - *

- * To improve performance when the same SQL statement is executed multiple - * times, implementations of {@link ReactiveJdbcAdapter} are expected to - * configure statement caching using any non-standard APIs that the adapted - * JDBC driver may implement. - *

- * - *

Named Parameter Markers

- *

- * The Oracle R2DBC Driver implements the {@code Statement} SPI to support - * named parameter markers. A expression of the form {@code :name} designates - * a parameterized value within the SQL statement. The following example shows a - * SQL statement with two named parameter markers in the WHERE clause: - *

- *   SELECT name FROM pets WHERE species=:species AND age=:age
- * 

- * Parameter values can be bound to alpha-numeric names that appear - * after the colon character. Given a {@link Statement} created with the SQL - * above, the following code would set parameter values to select the names - * of all 10 year old dogs: - *

- *   statement
- *     .bind("species", "Dog")
- *     .bind("age", 10);
- * 
- * - *

JDBC Style Parameter Markers

- *

- * The Oracle R2DBC Driver implements the {@code Statement} SPI to support - * JDBC style parameter markers. A {@code ?} character designates a - * parameterized value within the SQL statement. When this style of parameter - * is used, the Oracle R2DBC Driver does not support SPI methods for setting - * {@linkplain #bind(String, Object) named binds}. The following example - * shows a SQL statement with two {@code ?} parameter markers in the WHERE - * clause: - *

- *   SELECT name FROM pets WHERE species=? AND age=?
- * 

- * Parameter values can be bound to the numeric zero-based index of a - * {@code ?} marker, where the index corresponds to the position of the - * marker within the sequence of all markers that appear when the - * statement is read from left to right (ie: the ordinal index). In the example - * above, the {@code species=?} marker appears first, so the bind index for - * this parameter is {@code 0}. The {@code age=?} marker appears next, so the - * bind index for that parameter is {@code 1}. Given a {@link Statement} - * created with the SQL above, the following code would set parameter values - * to select the names of all 9 year old cats: - *

- *   statement
- *     .bind(0, "Cat")
- *     .bind(1, 9);
- * 
- * - * @author harayuanwang, michael-a-mcmahon - * @since 0.1.0 + * Verifies that + * {@link OracleStatementImpl} implements behavior that is specified in it's + * class and method level javadocs. */ -final class OracleStatementImpl implements Statement { - - /** - * Instance of {@code Object} representing a null bind value. This object - * is stored at indexes of {@link #bindValues} that have been set with a - * null value. - */ - private static final Object NULL_BIND = new Object(); - - /** A JDBC connection that executes this statement's {@link #sql}. */ - private final Connection jdbcConnection; - - /** Adapts Oracle JDBC Driver APIs into Reactive Streams APIs */ - private final ReactiveJdbcAdapter adapter; - - /** - * SQL Language command that this statement executes. The command is - * provided by user code and may include parameter markers. - */ - private final String sql; +public class OracleStatementImplTest { /** - * Timeout, in seconds, applied to the execution of this {@code Statement} + * Verifies the implementation of + * {@link OracleStatementImpl#bind(int, Object)} */ - private final int timeout; - - /** - * Parameter names recognized in this statement's SQL. This list contains - * {@code null} entries at the indexes of unnamed parameters. - */ - private final List parameterNames; - - /** - * The current set of bind values. This array stores {@code null} at - * positions that have not been set with a value. All {@code Objects} input - * to a {@code bind} method of this {@code Statement} are stored in this - * array. - */ - private final Object[] bindValues; - - /** - * The current batch of bind values. A copy of {@link #bindValues} is added - * to this queue when {@link #add()} is invoked. - */ - private Queue batch = new LinkedList<>(); + @Test + public void testBindByIndex() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + // INSERT and SELECT from this table with a parameterized statements + awaitExecution(connection.createStatement( + "CREATE TABLE testBindByIndex (x NUMBER, y NUMBER)")); + + // Expect bind values to be applied in VALUES clause + awaitUpdate( + asList(1, 1, 1, 1), + connection + .createStatement("INSERT INTO testBindByIndex VALUES (?, ?)") + .bind(0, 0).bind(1, 0).add() + .bind(0, 1).bind(1, 0).add() + .bind(0, 1).bind(1, 1).add() + .bind(0, 1).bind(1, 2)); + + // Expect bind values to be applied in WHERE clause as: + // SELECT x, y FROM testBindByIndex WHERE x = 1 AND y > 0 + awaitQuery( + asList(asList(1, 1), asList(1, 2)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testBindByIndex" + + " WHERE x = ? and y > ?" + + " ORDER BY x, y") + .bind(0, 1).bind(1, 0)); + + Statement statement = connection.createStatement( + "SELECT x FROM testBindByIndex WHERE x > ?"); + + // Expect IllegalArgumentException for a null value + assertThrows( + IllegalArgumentException.class, + () -> statement.bind(0, null)); + assertThrows( + IllegalArgumentException.class, + () -> statement.bind(1, null)); + + // Expect IllegalArgumentException for an unsupported conversion + class UnsupportedType { } + assertThrows( + IllegalArgumentException.class, + () -> statement.bind(0, new UnsupportedType())); + + // Expect IndexOutOfBoundsException for an out of range index + assertThrows( + IndexOutOfBoundsException.class, + () -> statement.bind(-1, 1)); + assertThrows( + IndexOutOfBoundsException.class, + () -> statement.bind(-2, 1)); + assertThrows( + IndexOutOfBoundsException.class, + () -> statement.bind(1, 1)); + assertThrows( + IndexOutOfBoundsException.class, + () -> statement.bind(2, 1)); + assertThrows( + IndexOutOfBoundsException.class, () -> + connection.createStatement("SELECT x FROM testBindByIndex") + .bind(0, 0)); + + // Expect bind values to be replaced when set more than once + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindByIndex VALUES (?, ?)") + .bind(0, 99).bind(1, 99) + .bind(0, 2).bind(1, 0)); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindByIndex VALUES (:x, :y)") + .bind("x", 99).bind("y", 99) + .bind(0, 2).bind(1, 1)); + awaitQuery( + asList(asList(2, 0), asList(2, 1)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindByIndex WHERE x = 2 ORDER BY y")); + + // Expect bind values to be replaced when set more than once, after + // calling add() + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testBindByIndex VALUES (?, ?)") + .bind(0, 3).bind(1, 0).add() + .bind(0, 99).bind(1, 99) + .bind(0, 3).bind(1, 1)); + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testBindByIndex VALUES (:x, :y)") + .bind(0, 3).bind(1, 2).add() + .bind("x", 99).bind("y", 99) + .bind(0, 3).bind(1, 3)); + awaitQuery( + asList(asList(3, 0), asList(3, 1), asList(3, 2), asList(3, 3)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindByIndex WHERE x = 3 ORDER BY y")); - /** - * Fetch size that has been provided to {@link #fetchSize(int)}. - */ - private int fetchSize = 0; + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testBindByIndex")); + tryAwaitNone(connection.close()); + } + } /** - * A hint from user code providing the names of columns that the database - * might generate a value for when this statement is executed. This array - * is a copy of one provided to {@link #returnGeneratedValues(String...)}, - * or is {@code null} if user code has not specified generated values. If - * this array has been specified, then it does not contain {@code null} - * values. This array may be specified as a zero-length array to indicate - * that the R2DBC driver should determine which column values are returned. + * Verifies the implementation of + * {@link OracleStatementImpl#bind(String, Object)} */ - private String[] generatedColumns = null; + @Test + public void testBindByName() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + // INSERT and SELECT from this table with a parameterized statement + awaitExecution(connection.createStatement( + "CREATE TABLE testBindByName (x NUMBER, y NUMBER)")); + + // Expect bind values to be applied in VALUES clause + awaitUpdate( + asList(1, 1, 1, 1), + connection + .createStatement("INSERT INTO testBindByName VALUES (:X, :Y)") + .bind("X", 0).bind("Y", 0).add() + .bind("X", 1).bind("Y", 0).add() + .bind("X", 1).bind("Y", 1).add() + .bind("X", 1).bind("Y", 2)); + + // Expect bind values to be applied in WHERE clause as: + // SELECT x, y FROM testBindByName WHERE x = 1 AND y > 0 + awaitQuery( + asList(asList(1, 1), asList(1, 2)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testBindByName" + + " WHERE x = :x and y > :y" + + " ORDER BY x, y") + .bind("x", 1).bind("y", 0)); + + // Using a duplicate parameter name, expect bind values to be applied + // in WHERE clause as: + // SELECT x, y FROM testBindByName WHERE x = 1 AND y > 0 AND y < 2 + awaitQuery( + asList(asList(1, 1)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testBindByName" + + " WHERE x = :x AND y > :y AND y < :y") + .bind("x", 1).bind("y", 0).bind(2, 2)); + + // Expect IllegalArgumentException for a null value + Statement statement = connection.createStatement( + "SELECT x FROM testBindByIndex WHERE x > :x"); + assertThrows( + IllegalArgumentException.class, + () -> statement.bind("x", null)); + + // Expect IllegalArgumentException for a null identifier + assertThrows( + IllegalArgumentException.class, + () -> statement.bind(null, 1)); + + // Expect IllegalArgumentException for an unsupported conversion + class UnsupportedType { + } + assertThrows( + IllegalArgumentException.class, + () -> statement.bind("x", new UnsupportedType())); + + // Expect NoSuchElementException for an unmatched identifier + assertThrows( + NoSuchElementException.class, + () -> statement.bind("z", 1)); + assertThrows( + NoSuchElementException.class, + () -> statement.bind("xx", 1)); + assertThrows( + NoSuchElementException.class, + () -> statement.bind("", 1)); + assertThrows( + NoSuchElementException.class, + () -> statement.bind("X", 1)); + assertThrows( + NoSuchElementException.class, + () -> + connection.createStatement("SELECT x FROM testBindByIndex") + .bind("x", 0)); + + // Expect bind values to be replaced when set more than once + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindByName VALUES (:x, :y)") + .bind(0, 99).bind(1, 99) + .bind("x", 2).bind(1, 0)); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindByName VALUES (:x, :y)") + .bind("x", 99).bind("y", 99) + .bind("x", 2).bind(1, 1)); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindByName VALUES (:x, :y)") + .bind("x", 99).bind("y", 99) + .bind("x", 2).bind("y", 2)); + awaitQuery( + asList(asList(2, 0), asList(2, 1), asList(2, 2)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindByName WHERE x = 2 ORDER BY y")); + + // Expect bind values to be replaced when set more than once, after + // calling add() + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testBindByName VALUES (:x, :y)") + .bind("x", 3).bind(1, 0).add() + .bind(0, 99).bind(1, 99) + .bind("x", 3).bind(1, 1)); + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testBindByName VALUES (:x, :y)") + .bind("x", 3).bind("y", 2).add() + .bind("x", 99).bind("y", 99) + .bind("x", 3).bind("y", 3)); + awaitQuery( + asList(asList(3, 0), asList(3, 1), asList(3, 2), asList(3, 3)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindByName WHERE x = 3 ORDER BY y")); + + // When the same name is used for multiple parameters, expect a value + // bound to that name to be set as the value for all of those parameters. + // Expect a value bound to the index of one of those parameters to be + // set only for the parameter at that index. + awaitUpdate(asList(1, 1, 1), + connection + .createStatement("INSERT INTO testBindByName VALUES (:same, :same)") + .bind("same", 4).add() + .bind("same", 4).bind(1, 5).add() + .bind(0, 4).bind(1, 6)); + awaitQuery(asList(asList(4,4)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindByName WHERE x = :x_and_y AND y = :x_and_y") + .bind("x_and_y", 4)); + awaitQuery( + asList(asList(4, 5), asList(4, 6)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindByName" + + " WHERE x = :both AND y <> :both" + + " ORDER BY y") + .bind("both", 4)); + awaitQuery(asList(asList(4,4)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindByName" + + " WHERE x = :x_and_y" + + " AND (x * y) = :x_times_y" + + " AND y = :x_and_y") + .bind("x_times_y", 16) + .bind("x_and_y", 4)); - /** - *

- * Constructs a new statement that executes {@code sql} using the specified - * {@code adapter} and {@code jdbcConnection}. - *

- * The SQL string may be parameterized as described in the javadoc of - * {@link SqlParameterParser}. - *

- * @param sql SQL Language statement that may include parameter markers. - * @param timeout Timeout applied to the execution of the constructed - * {@code Statement}. Not null. Not negative. - * @param jdbcConnection JDBC connection to an Oracle Database. - * @param adapter Adapts JDBC calls into reactive streams. - */ - OracleStatementImpl( - String sql, Duration timeout, Connection jdbcConnection, - ReactiveJdbcAdapter adapter) { - this.sql = sql; - this.jdbcConnection = jdbcConnection; - this.adapter = adapter; - - // The SQL string is parsed to identify parameter markers and allocate the - // bindValues array accordingly - this.parameterNames = SqlParameterParser.parse(sql); - this.bindValues = new Object[parameterNames.size()]; - - // Round the timeout up to the nearest whole second, so that it may be - // set with PreparedStatement.setQueryTimeout(int) - this.timeout = (int)Math.min( - Integer.MAX_VALUE, - timeout.toSeconds() + (timeout.getNano() == 0 ? 0 : 1)); + } + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE testBindByName")); + tryAwaitNone(connection.close()); + } } /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by storing the bind {@code value} at the - * specified {@code index} in {@link #bindValues}. A reference to the - * {@code value} is retained until this statement is executed. - *

+ * Verifies the implementation of + * {@link OracleStatementImpl#bindNull(int, Class)} */ - @Override - public Statement bind(int index, Object value) { - requireOpenConnection(jdbcConnection); - requireNonNull(value, "value is null"); - requireValidIndex(index); - bindObject(index, value); - return this; + @Test + public void testBindNullByIndex() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + // INSERT into this table with a parameterized VALUES clause + awaitExecution(connection.createStatement( + "CREATE TABLE testBindNullByIndex (x NUMBER, y NUMBER)")); + Statement selectStatement = connection.createStatement( + "SELECT x, y" + + " FROM testBindNullByIndex" + + " WHERE x = :x and y > :y" + + " ORDER BY x, y"); + + // Expect IllegalArgumentException for a null Class + assertThrows( + IllegalArgumentException.class, + () -> selectStatement.bindNull(0, null)); + assertThrows( + IllegalArgumentException.class, + () -> selectStatement.bindNull(1, null)); + + // Expect IndexOutOfBoundsException for an out of range index + assertThrows( + IndexOutOfBoundsException.class, + () -> selectStatement.bindNull(-1, Integer.class)); + assertThrows( + IndexOutOfBoundsException.class, + () -> selectStatement.bindNull(-2, Integer.class)); + assertThrows( + IndexOutOfBoundsException.class, + () -> selectStatement.bindNull(2, Integer.class)); + assertThrows( + IndexOutOfBoundsException.class, + () -> selectStatement.bindNull(3, Integer.class)); + assertThrows( + IndexOutOfBoundsException.class, + () -> + connection.createStatement("SELECT x FROM testBindByIndex") + .bind(0, 0)); + + // Expect NULL bind values to be applied in VALUES clause + awaitUpdate( + asList(1, 1, 1, 1, 1, 1), + connection + .createStatement("INSERT INTO testBindNullByIndex VALUES (?, ?)") + .bindNull(0, Integer.class).bindNull(1, Integer.class).add() + .bindNull(0, Integer.class).bind(1, 0).add() + .bindNull(0, Integer.class).bind(1, 1).add() + .bindNull(0, Integer.class).bind(1, 2).add() + .bind(0, 0).bind(1, 3).add() + .bind(0, 0).bindNull(1, Integer.class)); + awaitQuery( + asList( + asList(null, 0), + asList(null, 1), + asList(null, 2)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testBindNullByIndex" + + " WHERE x IS NULL and y IS NOT NULL" + + " ORDER BY y")); + + // Expect bind values to be replaced when set more than once + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindNullByIndex VALUES (?, ?)") + .bind(0, 99).bind(1, 99) + .bind(0, 1).bindNull(1, Integer.class)); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindNullByIndex VALUES (?, ?)") + .bindNull(0, Integer.class).bindNull(1, Integer.class) + .bind(0, 1).bind(1, 0)); + awaitQuery( + asList(asList(1, 0), asList(1, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindNullByIndex WHERE x = 1 ORDER BY y")); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindNullByIndex VALUES (:x, :y)") + .bind("x", 99).bind("y", 99) + .bind(0, 2).bindNull(1, Integer.class)); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testBindNullByIndex VALUES (:x, :y)") + .bindNull("x", Integer.class).bindNull("y", Integer.class) + .bind(0, 2).bind(1, 0)); + awaitQuery( + asList(asList(2, 0), asList(2, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindNullByIndex WHERE x = 2 ORDER BY y")); + + // Expect bind values to be replaced when set more than once, after + // calling add() + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testBindNullByIndex VALUES (?, ?)") + .bind(0, 3).bind(1, 0).add() + .bind(0, 99).bind(1, 99) + .bind(0, 3).bindNull(1, Integer.class)); + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testBindNullByIndex VALUES (:x, :y)") + .bind(0, 3).bind(1, 1).add() + .bind("x", 99).bindNull("y", Integer.class) + .bind(0, 3).bind(1, 3)); + awaitQuery( + asList( + asList(3, 0), asList(3, 1), asList(3, 3), asList(3, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testBindNullByIndex WHERE x = 3 ORDER BY y")); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testBindNullByIndex")); + tryAwaitNone(connection.close()); + } } /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by setting the specified {@code value} as a - * parameter on the JDBC PreparedStatement that this statement executes. - * The JDBC PreparedStatement retains a reference to the {@code value} - * until this statement is executed. - *

- * The Oracle R2DBC Driver only supports this method if the SQL used to - * create this statement contains - * colon prefixed parameter names. - *

- * Note that parameter names are case sensitive. See - * {@link SqlParameterParser} for a full specification of the parameter name - * syntax. - *

- * If the specified {@code identifier} matches more than one parameter name, - * then this method binds the {@code value} to all parameters having a - * matching name. For instance, when {@code 9} is bound to the parameter - * named "x", the following SQL would return all names having a birthday on - * the 9th day of the 9th month: - *

-   * SELECT name FROM birthday WHERE month=:x AND day=:x
-   * 
- *

- * @throws IllegalArgumentException {@inheritDoc} - * @throws IllegalArgumentException If the {@code identifier} does match a - * case sensitive parameter name that appears in this {@code Statement's} - * SQL command. - * @throws IllegalArgumentException If the JDBC PreparedStatement does not - * support conversions of the bind value's Java type into a SQL type. + * Verifies the implementation of + * {@link OracleStatementImpl#bindNull(String, Class)} */ - @Override - public Statement bind(String identifier, Object value) { - requireOpenConnection(jdbcConnection); - requireNonNull(identifier, "identifier is null"); - requireNonNull(value, "value is null"); - bindNamedParameter(identifier, value); - return this; + @Test + public void testBindNullByName() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + // INSERT into this table with a parameterized VALUES clause + awaitExecution(connection.createStatement( + "CREATE TABLE testNullBindByName (x NUMBER, y NUMBER)")); + Statement selectStatement = connection.createStatement( + "SELECT x, y" + + " FROM testNullBindByName" + + " WHERE x = :x and y > :y" + + " ORDER BY x, y"); + + // Expect IllegalArgumentException for a null class + assertThrows( + IllegalArgumentException.class, + () -> selectStatement.bindNull("x", null)); + assertThrows( + IllegalArgumentException.class, + () -> selectStatement.bindNull("y", null)); + + // Expect IllegalArgumentException for a null identifier + assertThrows( + IllegalArgumentException.class, + () -> selectStatement.bindNull(null, Integer.class)); + + // Expect NoSuchElementException for an unmatched identifier + assertThrows( + NoSuchElementException.class, + () -> selectStatement.bindNull("z", Integer.class)); + assertThrows( + NoSuchElementException.class, + () -> selectStatement.bindNull("xx", Integer.class)); + assertThrows( + NoSuchElementException.class, + () -> selectStatement.bindNull("", Integer.class)); + assertThrows( + NoSuchElementException.class, + () -> selectStatement.bindNull("X", Integer.class)); + assertThrows( + NoSuchElementException.class, + () -> + connection.createStatement("SELECT x FROM testBindByIndex") + .bind("x", 0)); + + + // Expect NULL bind values to be applied in VALUES clause + awaitUpdate( + asList(1, 1, 1, 1, 1, 1), + connection + .createStatement("INSERT INTO testNullBindByName VALUES (:x, :y)") + .bindNull("x", Integer.class).bindNull("y", Integer.class).add() + .bindNull("x", Integer.class).bind("y", 0).add() + .bindNull("x", Integer.class).bind("y", 1).add() + .bindNull("x", Integer.class).bind("y", 2).add() + .bind("x", 0).bind("y", 3).add() + .bind("x", 0).bindNull("y", Integer.class)); + awaitQuery( + asList( + asList(null, 0), + asList(null, 1), + asList(null, 2)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testNullBindByName" + + " WHERE x IS NULL and y IS NOT NULL" + + " ORDER BY y")); + + // Using a duplicate parameter name, expect bind values to be applied + // in WHERE clause as: + // UPDATE testNullBindByName SET x = NULL WHERE x = 0 + awaitUpdate( + asList(2), + connection.createStatement( + "UPDATE testNullBindByName" + + " SET x = :x WHERE x = :x") + .bindNull("x", Integer.class).bind(1, 0)); + awaitQuery( + asList( + asList(null, 0), + asList(null, 1), + asList(null, 2), + asList(null, 3), + asList(null, null), + asList(null, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testNullBindByName" + + " WHERE x IS NULL" + + " ORDER BY y")); + + // Expect bind values to be replaced when set more than once + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testNullBindByName VALUES (:x, :y)") + .bind(0, 99).bind(1, 99) + .bind("x", 1).bindNull("y", Integer.class)); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testNullBindByName VALUES (:x, :y)") + .bindNull(0, Integer.class).bindNull(1, Integer.class) + .bind("x", 1).bind("y", 0)); + awaitQuery( + asList(asList(1, 0), asList(1, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testNullBindByName WHERE x = 1 ORDER BY y")); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testNullBindByName VALUES (:x, :y)") + .bind("x", 99).bind("y", 99) + .bind("x", 2).bindNull("y", Integer.class)); + awaitUpdate( + asList(1), + connection + .createStatement("INSERT INTO testNullBindByName VALUES (:x, :y)") + .bindNull("x", Integer.class).bindNull("y", Integer.class) + .bind("x", 2).bind("y", 0)); + awaitQuery( + asList(asList(2, 0), asList(2, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testNullBindByName WHERE x = 2 ORDER BY y")); + + // Expect bind values to be replaced when set more than once, after + // calling add() + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testNullBindByName VALUES (:x, :y)") + .bind("x", 3).bind("y", 0).add() + .bind(0, 99).bind(1, 99) + .bind("x", 3).bindNull("y", Integer.class)); + awaitUpdate( + asList(1, 1), + connection + .createStatement("INSERT INTO testNullBindByName VALUES (:x, :y)") + .bind("x", 3).bind("y", 1).add() + .bind("x", 99).bindNull("y", Integer.class) + .bind("x", 3).bind("y", 3)); + awaitQuery( + asList( + asList(3, 0), asList(3, 1), asList(3, 3), asList(3, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testNullBindByName WHERE x = 3 ORDER BY y")); + + // When the same name is used for multiple parameters, expect a value + // bound to that name to be set as the value for all of those parameters. + // Expect a value bound to the index of one of those parameters to be + // set only for the parameter at that index. + awaitUpdate(2, connection.createStatement( + "DELETE FROM testNullBindByName WHERE x IS NULL AND y IS NULL")); + awaitUpdate(asList(1, 1, 1), + connection + .createStatement( + "INSERT INTO testNullBindByName VALUES (:same, :same)") + .bindNull("same", Integer.class).add() + .bindNull("same", Integer.class).bind(0, 4).add() + .bind(0, 5).bindNull(1, Integer.class)); + awaitQuery(asList(asList(null, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testNullBindByName" + + " WHERE x IS NULL AND y IS NULL")); + awaitQuery(asList(asList(4, null), asList(5, null)), + row -> + asList(row.get(0, Integer.class), row.get(1,Integer.class)), + connection.createStatement( + "SELECT x, y FROM testNullBindByName" + + " WHERE x >= 4 AND x IS NOT NULL AND y IS NULL" + + " ORDER BY x, y")); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testNullBindByName")); + tryAwaitNone(connection.close()); + } } /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by setting the {@code null} value as a - * parameter on the JDBC PreparedStatement that this statement executes. The - * {@code null} value is specified to JDBC as the SQL - * {@link java.sql.Types#NULL} type. - *

+ * Verifies the implementation of + * {@link OracleStatementImpl#add()} */ - @Override - public Statement bindNull(int index, Class type) { - requireOpenConnection(jdbcConnection); - requireNonNull(type, "type is null"); - requireValidIndex(index); - bindObject(index, null); - return this; - } + @Test + public void testAdd() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + // INSERT into this table with a parameterized VALUES clause + awaitExecution(connection.createStatement( + "CREATE TABLE testAdd (x NUMBER, y NUMBER)")); + // Expect add() with zero parameters to execute a batch of INSERTs + awaitUpdate( + asList(1, 1, 1), + connection.createStatement("INSERT INTO testAdd VALUES(0, 0)") + .add().add()); + awaitQuery( + asList(asList(0, 0), asList(0, 0), asList(0, 0)), + row -> asList(row.get(0, Integer.class), row.get(1, Integer.class)), + connection.createStatement("SELECT x, y FROM testAdd")); + + // Expect add() with parameters to execute a batch of INSERTs + awaitUpdate( + asList(1, 1, 1), + connection.createStatement("INSERT INTO testAdd VALUES(:x, :y)") + .bind("x", 1).bind("y", 1).add() + .bind("x", 1).bind("y", 2).add() + .bind("x", 1).bind("y", 3)); + awaitQuery( + asList(asList(1, 1), asList(1, 2), asList(1, 3)), + row -> asList(row.get(0, Integer.class), row.get(1, Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testAdd" + + " WHERE x = 1" + + " ORDER BY y")); + + // Expect an implicit add() after add() has been called once + awaitUpdate( + asList(1, 1), + connection.createStatement("INSERT INTO testAdd VALUES(:x, :y)") + .bind("x", 2).bind("y", 1).add() + .bind("x", 2).bind("y", 2)); // implicit .add() + awaitQuery( + asList(asList(2, 1), asList(2, 2)), + row -> asList(row.get(0, Integer.class), row.get(1, Integer.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testAdd" + + " WHERE x = 2" + + " ORDER BY y")); + + // Expect R2dbcException when executing a non-DML batch + awaitError( + R2dbcException.class, + Mono.from(connection.createStatement("SELECT ? FROM dual") + .bind(0, 1).add() + .bind(0, 2).add() + .bind(0, 3) + .execute()) + .flatMapMany(Result::getRowsUpdated)); + + // Expect IllegalStateException if not all parameters are set + assertThrows( + IllegalStateException.class, + () -> + connection.createStatement("INSERT INTO table VALUES(?)") + .add()); + assertThrows( + IllegalStateException.class, + () -> + connection.createStatement("INSERT INTO table VALUES(?, ?)") + .bind(0, 0).add()); + assertThrows( + IllegalStateException.class, + () -> + connection.createStatement("INSERT INTO table VALUES(:x, :y)") + .bind("y", 1).add()); + assertThrows( + IllegalStateException.class, + () -> + connection.createStatement("INSERT INTO table VALUES(?)") + .bind(0, 0).add() + .add()); + assertThrows( + IllegalStateException.class, + () -> + connection.createStatement("INSERT INTO table VALUES(?, ?)") + .bind(0, 0).bind(1, 1).add() + .bind(1, 1).add()); + assertThrows( + IllegalStateException.class, + () -> + connection.createStatement("INSERT INTO table VALUES(:x, :y)") + .bind("x", 0).bind("y", 1).add() + .bind("x", 0).add()); + + // Expect the statement to execute with previously added binds, and + // then emit an error if binds are missing in the final set of binds. + List> signals = + awaitOne(Flux.from(connection.createStatement( + "INSERT INTO testAdd VALUES (:x, :y)") + .bind("x", 0).bind("y", 1).add() + .bind("y", 1).execute()) + .flatMap(Result::getRowsUpdated) + .materialize() + .collectList()); + assertEquals(2, signals.size()); + assertEquals(1, signals.get(0).get()); + assertTrue( + signals.get(1).getThrowable() instanceof R2dbcNonTransientException); - /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by setting the {@code null} value as a - * parameter on the JDBC PreparedStatement that this statement executes. The - * {@code null} value is specified to JDBC as the SQL - * {@link java.sql.Types#NULL} type. - *

- * The Oracle R2DBC Driver only supports this method if the SQL used to - * create this statement contains - * colon prefixed parameter names. - *

- * Note that parameter names are case sensitive. See - * {@link SqlParameterParser} for a full specification of the parameter name - * syntax. - *

- * If the specified {@code identifier} matches more than one parameter name - * in this {@code Statement's} SQL command, this method binds the SQL - * {@code NULL} value to the first matching parameter that appears when the - * SQL command is read from left to right. (Note: It is not recommended to use - * duplicate parameter names. Use {@link #bindNull(int, Class)} to set the - * SQL {@code NULL} value for a duplicate parameter name at a given index). - *

- *

- * If the specified {@code identifier} matches more than one parameter name, - * then this method binds the SQL {@code NULL} value to all parameters - * having a matching name. For instance, when {@code NULL} is bound to the - * parameter named "x", the following SQL would create a birthday with - * {@code NULL} values for month and day: - *

-   * INSERT INTO birthday (name, month, day) VALUES ('Plato', :x, :x)
-   * 
- *

- * @throws IllegalArgumentException {@inheritDoc} - * @throws IllegalArgumentException If the {@code identifier} does match a - * case sensitive parameter name that appears in this {@code Statement's} - * SQL command. - */ - @Override - public Statement bindNull(String identifier, Class type) { - requireOpenConnection(jdbcConnection); - requireNonNull(identifier, "identifier is null"); - requireNonNull(type, "type is null"); - bindNamedParameter(identifier, null); - return this; + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testAdd")); + tryAwaitNone(connection.close()); + } } /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by adding the current set of bind values - * to the batch of the JDBC PreparedStatement that this statement executes. - *

- * The Oracle R2DBC Driver only supports this method if this - * {@code Statement} was created with a DML type SQL command. If this - * method is invoked on a non-DML {@code Statement}, the publisher returned - * by {@link #execute()} emits {@code onError} with an - * {@code R2dbcException} indicating that the SQL is not a DML command. - *

- * @throws IllegalStateException If one or more binds are out parameters + * Verifies the implementation of + * {@link OracleStatementImpl#execute()} */ - @Override - public Statement add() { - requireOpenConnection(jdbcConnection); - addBatchValues(); - return this; + @Test + public void testExecute() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + // Expect DDL to result in an update count of zero + awaitUpdate(0, connection.createStatement( + "CREATE TABLE testExecute (x NUMBER)")); + // Expect DDL to result in no row data + awaitQuery( + Collections.emptyList(), + row -> row.get(0), + connection.createStatement( + "ALTER TABLE testExecute ADD (y NUMBER)")); + + // Expect DML to result in an update count + Statement insertStatement = connection.createStatement( + "INSERT INTO testExecute (x, y) VALUES (:x, :y)"); + awaitUpdate( + asList(1), + insertStatement.bind("x", 0).bind("y", 0)); + + // Expect DML to result in no row data + awaitQuery( + Collections.emptyList(), + row -> row.get(0), + insertStatement.bind("x", 0).bind("y", 1)); + + // Expect batch DML to result in an update count + Statement updateStatement = connection.createStatement( + "UPDATE testExecute SET y = :newValue WHERE y = :oldValue"); + awaitUpdate( + asList(1, 1), + updateStatement + .bind("oldValue", 1).bind("newValue", 2).add() + .bind("oldValue", 0).bind("newValue", 1)); + + // Expect bind values to be cleared after execute with explicit add() + assertThrows(IllegalStateException.class, updateStatement::execute); + + // Expect batch DML to result in no row data + awaitQuery( + Collections.emptyList(), + row -> row.get(0), + updateStatement + .bind("oldValue", 2).bind("newValue", 3).add() + .bind("oldValue", 1).bind("newValue", 2)); + + // Expect bind values to be cleared after execute with implicit add() + assertThrows(IllegalStateException.class, updateStatement::execute); + + // Expect publisher to defer execution until a subscriber subscribes + Publisher updatePublisher = + updateStatement.bind("oldValue", 3).bind("newValue", 1).execute(); + + // Expect DQL to result in no update count + Statement selectStatement = connection.createStatement( + "SELECT x, y FROM testExecute WHERE x = :x ORDER BY y"); + awaitUpdate( + Collections.emptyList(), + selectStatement.bind("x", 0)); + + // Expect DQL to result in row data + awaitQuery( + asList(asList(0, 2), asList(0, 3)), + row -> + asList(row.get("x", Integer.class), row.get("y", Integer.class)), + selectStatement.bind("x", 0)); + + // Expect bind values to be cleared after execute without add() + assertThrows( + IllegalStateException.class, + selectStatement::execute); + + // Expect update to execute when a subscriber subscribes + awaitOne(1L, + Flux.from(updatePublisher) + .flatMap(result -> result.getRowsUpdated())); + awaitQuery( + asList(asList(0, 1), asList(0, 2)), + row -> + asList(row.get("x", Integer.class), row.get("y", Integer.class)), + selectStatement.bind("x", 0)); + + // Expect publisher to reject multiple subscribers + awaitError(IllegalStateException.class, updatePublisher); + + // TODO: Verify that cursors opened by execute() are closed after the + // result has been consumed. Consider querying V$ tables to verify the + // open cursor count. Consider that the JDBC driver may be caching + // statements and leaving cursors open until a cache eviction happens. + } + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE testExecute")); + tryAwaitNone(connection.close()); + } } + /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by setting the {@link #generatedColumns} - * that a JDBC {@link PreparedStatement} will be configured to return when - * this statement is executed. - *

- * No reference to the {@code columns} array is retained after this method - * returns. - *

- * @throws IllegalStateException If one or more binds are out-parameters. - * Returning generated values is not supported when executing a stored - * procedure. - * @throws IllegalStateException If one or more binds have been added with - * {@link #add()}. Returning generated values is not supported when - * executing a batch DML command. + * Verifies the implementation of + * {@link OracleStatementImpl#returnGeneratedValues(String...)} */ - @Override - public Statement returnGeneratedValues(String... columns) { - requireOpenConnection(jdbcConnection); - requireNonNull(columns, "Column names are null"); - - for (int i = 0; i < columns.length; i++) { - if (columns[i] == null) - throw new IllegalArgumentException("Null column name at index: " + i); + @Test + public void testReturnGeneratedValues() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testReturnGeneratedValues (" + + "x NUMBER GENERATED ALWAYS AS IDENTITY, " + + "y VARCHAR2(100))")); + + Statement statement = connection.createStatement( + "INSERT INTO testReturnGeneratedValues(y) VALUES (?)"); + + // Expect IllegalArgumentException for a null argument + assertThrows(IllegalArgumentException.class, + () -> statement.returnGeneratedValues((String[])null)); + // Expect IllegalArgumentException for a null String[] element + assertThrows(IllegalArgumentException.class, + () -> statement.returnGeneratedValues("x", null)); + + // Expect a failure with invalid column name "eye-d" + assertEquals(statement, statement.returnGeneratedValues("x", "eye-d")); + awaitError(R2dbcException.class, + Flux.from(statement.bind(0, "test").execute()) + .flatMap(result -> + result.map(generatedValues -> fail("Unexpected row")))); + + // Expect a ROWID value when no column names are specified + Statement rowIdQuery = connection.createStatement( + "SELECT x, y FROM testReturnGeneratedValues WHERE rowid=?"); + RowId rowId = awaitOne(Mono.from(statement.returnGeneratedValues() + .bind(0, "test1") + .execute()) + .flatMapMany(result -> + result.map(row -> row.get(0, RowId.class)))); + // Expect a generated value of 1 when the ROWID is queried + awaitQuery(asList(asList(1, "test1")), + row -> asList(row.get(0, Integer.class), row.get(1, String.class)), + rowIdQuery.bind(0, rowId)); + + // Expect the second insert to generate a value of 2 + awaitQuery(asList(asList(2, "test2")), + row -> asList(row.get(0, Integer.class), row.get(1, String.class)), + statement.returnGeneratedValues("x", "y").bind(0, "test2")); + + // Expect an update count of 1 ... + awaitUpdate(1, statement.returnGeneratedValues("x").bind(0, "test3")); + // ... and generated value of 3 + awaitQuery(asList(asList(3, "test3")), + row -> asList(row.get(0, Integer.class), row.get(1, String.class)), + connection.createStatement( + "SELECT x, y FROM testReturnGeneratedValues WHERE x = 3")); + + // Expect non-generated values to be returned as well + assertEquals(statement, statement.returnGeneratedValues("x", "y")); + awaitQuery(asList(asList(4, "test4")), + row -> asList(row.get("x", Integer.class), row.get("Y", String.class)), + statement.bind(0, "test4")); + + // Expect an error when attempting to batch execute with generated + // values + assertThrows(IllegalStateException.class, () -> + statement.bind(0, "a").add() + .bind(0, "b").add() + .bind(0, "c").add() + .execute()); + + // Expect multiple results of generated values when executing an UPDATE + // on multiple rows + awaitQuery(asList( + asList(1, "TEST1"), + asList(2, "TEST2"), + asList(3, "TEST3"), + asList(4, "TEST4")), + row -> asList(row.get("x", Integer.class), row.get("y", String.class)), + connection.createStatement( + "UPDATE testReturnGeneratedValues SET y =:prefix||x") + .bind("prefix", "TEST") + .returnGeneratedValues("x", "y")); + + // Expect a normal row data result when executing a SELECT statement, + // even if the Statement is configured to return columns generated by DML. + awaitQuery(asList( + asList(1, "TEST1"), + asList(2, "TEST2"), + asList(3, "TEST3"), + asList(4, "TEST4")), + row -> asList(row.get("x", Integer.class), row.get("y", String.class)), + connection.createStatement( + "SELECT x, y" + + " FROM testReturnGeneratedValues" + + " WHERE x < :old_x" + + " ORDER BY x") + .bind("old_x", 10) + .returnGeneratedValues("x")); + + // Expect the column names to be ignored if the SQL is not an INSERT or + // UPDATE + awaitUpdate(4, connection.createStatement( + "DELETE FROM testReturnGeneratedValues WHERE x < :old_x") + .bind("old_x", 10) + .returnGeneratedValues("x", "y")); + + // Expect no generated values if an UPDATE doesn't effect any rows. + awaitUpdate(0, connection.createStatement( + "UPDATE testReturnGeneratedValues SET y = 'effected' WHERE x IS NULL") + .returnGeneratedValues("y")); + } + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testReturnGeneratedValues")); + tryAwaitNone(connection.close()); } - - if (isOutParameterPresent()) - throw outParameterWithGeneratedValues(); - - if (! batch.isEmpty()) - throw generatedValuesWithBatch(); - - generatedColumns = columns.clone(); - return this; } /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by storing a number of rows to be set as a - * JDBC statement's fetch size when this R2DBC statement is executed. - *

+ * Verifies the implementation of + * {@link OracleStatementImpl#fetchSize(int) */ - @Override - public Statement fetchSize(int rows) { - requireOpenConnection(jdbcConnection); - if (rows < 0) { - throw new IllegalArgumentException( - "Fetch size is less than zero: " + rows); + @Test + public void testFetchSize() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + // Expect normal return when argument is at least 0 + Statement statement = connection.createStatement( + "SELECT x, y FROM testFetchSize"); + assertEquals(statement, statement.fetchSize(0)); + assertEquals(statement, statement.fetchSize(100)); + + // Expect IllegalArgumentException when argument is less than 0 + assertThrows(IllegalArgumentException.class, + () -> statement.fetchSize(-1)); + assertThrows(IllegalArgumentException.class, + () -> statement.fetchSize(-100)); + + // TODO: Figure out a way to verify that the implementation actually + // set the fetch size. Might expect a large query to complete quicker + // when a large fetch size is set, but execution time is not consistent + // due to external factors like network latency and database response + // time. + } + finally { + tryAwaitNone(connection.close()); } - fetchSize = rows; - return this; } /** - * {@inheritDoc} - *

- * Implements the R2DBC SPI method by returning a publisher that publishes the - * result of executing a JDBC PreparedStatement. For typical - * {@code SELECT, INSERT, UPDATE, and DELETE} commands, a single - * {@code Result} is published. The {@code Result} will either have - * zero or more {@link Result.RowSegment}s, or a single - * {@link Result.UpdateCount} segment, or a single {@link Result.Message} - * segment if an error occurs. The sections that follow will describe the - * {@code Result}s published for additional types of SQL that might be - * executed. - *

- * When this method returns, any bind values previously set on this - * statement are cleared, and any sets of bind values saved with - * {@link #add()} are also cleared. Any - * {@linkplain #fetchSize(int) fetch size} or - * {@linkplain #returnGeneratedValues(String...) generated values} will be - * retained between executions. - *

- *

Executing Batch DML

- *

- * If a batch of bind values have been {@linkplain #add() added} to this - * statement, then a single {@code Result} is published. The {@code Result} - * has an {@link Result.UpdateCount} for each set of added parameters, with - * the count providing the number of rows effected by those parameters. The - * order in which {@code UpdateCount}s are published corresponds to the - * order in which parameters that produced the count were added: The first - * count is the number of rows effected by the first set of added - * parameters, the second count is number of rows effected by - * the second set of added parameters, and so on. - *

- *

Executing Value Generating DML

- *

- * If this statement was created with an {@code INSERT} or {@code UPDATE} - * command, and {@link #returnGeneratedValues(String...)} has configured this - * statement to return generated values, then a single {@code Result} is - * published. The {@code Result} has an {@link Result.UpdateCount} segment - * and one or more {@link Result.RowSegment}s. The update count provides - * the number of rows effected by the statement, and the row segments provide - * the values generated for each row that was created or updated. - *

- * If this statement was created with a SQL command that does not return - * generated values, such as a {@code SELECT} or {@code DELETE}, then the - * columns specified with {@link #returnGeneratedValues(String...)} are - * ignored, and {@code Result}s are published as normal, as if - * {@code returnGeneratedValues} had never been called. - *

- *

Executing a Stored Procedure

- *

- * If this statement was created with a stored procedure call (ie: PL/SQL), - * then a {@code Result} is published for any cursors returned by - * {@code DBMS_SQL.RETURN_RESULT}, followed by a {@code Result} having an - * {@link Result.OutSegment} for any out-parameters of the call. - * When this method returns, any bind values previously set on this - * statement are cleared, and any sets of bind values saved with - * {@link #add()} are also cleared. - *

- * The returned publisher initiates SQL execution the first time a - * subscriber subscribes, before the subscriber emits a {@code request} - * signal. The returned publisher does not support multiple subscribers. After - * one subscriber has subscribed, the publisher signals {@code onError} - * with {@code IllegalStateException} to all subsequent subscribers. - *

- * - * @implNote - *

- * - * The 21.1 Oracle JDBC Driver does not determine a fetch size based on demand - * signalled with {@link org.reactivestreams.Subscription#request(long)}. - * - * Oracle JDBC will use a fixed fetch size specified with - * {@link #fetchSize(int)}. If no fetch size is specified, Oracle JDBC will - * use a default fixed fetch size. - *

- * When executing queries that return a large number of rows, programmers - * are advised to configure the amount of rows that Oracle JDBC should - * fetch and buffer by calling {@link #fetchSize(int)}. - *

- * A later release of Oracle JDBC may implement dynamic fetch sizes that are - * adjusted to based on {@code request} signals from the subscriber. - *

+ * Verifies {@link OracleStatementImpl#execute()} when calling a procedure + * having no out parameters. */ - @Override - public Publisher execute() { - requireOpenConnection(jdbcConnection); - - final Publisher statementPublisher; - if (! batch.isEmpty()) - statementPublisher = createJdbcBatch(); - else if (isOutParameterPresent()) - statementPublisher = createJdbcCall(); - else if (generatedColumns != null) - statementPublisher = createJdbcReturningGenerated(); - else - statementPublisher = createJdbcStatement(); - - // Allow just one subscriber to the result publisher. - AtomicBoolean isSubscribed = new AtomicBoolean(false); - return Flux.defer(() -> { - if (isSubscribed.compareAndSet(false, true)) { - return Mono.from(statementPublisher) - .flatMapMany(JdbcStatement::execute); - } - else { - return Mono.error(new IllegalStateException( - "Multiple subscribers are not supported by the Oracle R2DBC" + - " Statement.execute() publisher")); - } - }); + @Test + public void testNoOutCall() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testNoOutCall (value VARCHAR2(100))")); + + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testNoOutCallAdd(" + + "value VARCHAR2 DEFAULT 'Default Value') IS" + + " BEGIN " + + " INSERT INTO testNoOutCall VALUES (value);" + + " END;")); + + // Execute the procedure with no out parameters. Expect a single Result + // with no update count. Expect the IN parameter's default value to + // have been inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd; END;") + .execute()) + .flatMapMany(Result::getRowsUpdated)); + awaitQuery(asList("Default Value"), + row -> row.get(0), + connection.createStatement("SELECT * FROM testNoOutCall")); + + // Execute the procedure again with no out parameters. Expect a single + // Result with no rows. Expect the IN parameter's default value to have + // been inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd; END;") + .execute()) + .flatMap(result -> + Mono.from(result.map(row -> "Unexpected")))); + awaitQuery(asList("Default Value", "Default Value"), + row -> row.get(0), + connection.createStatement("SELECT * FROM testNoOutCall")); + + // Delete the previously inserted rows + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testNoOutCall")); + + // Execute the procedure with no out parameters. Expect a single Result + // with no update count. Expect the an indexed based String bind to + // have been inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd(?); END;") + .bind(0, "Indexed Bind") + .execute()) + .flatMap(result -> Mono.from(result.getRowsUpdated()))); + awaitQuery(asList("Indexed Bind"), + row -> row.get(0), + connection.createStatement("SELECT * FROM testNoOutCall")); + + // Execute the procedure with no out parameters. Expect a single Result + // with no update count. Expect the a named String bind to have been + // inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd(:parameter); END;") + .bind("parameter", "Named Bind") + .execute()) + .flatMap(result -> + Mono.from(result.map(row -> "Unexpected")))); + awaitQuery(asList("Indexed Bind", "Named Bind"), + row -> row.get(0), + connection.createStatement( + "SELECT * FROM testNoOutCall ORDER BY value")); + + // Delete the previously inserted rows + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testNoOutCall")); + + // Execute the procedure with no out parameters. Expect a single Result + // with no update count. Expect the an indexed based Parameter bind to + // have been inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd(?); END;") + .bind(0, Parameters.in(R2dbcType.VARCHAR, "Indexed Parameter")) + .execute()) + .flatMap(result -> + Mono.from(result.getRowsUpdated()))); + awaitQuery(asList("Indexed Parameter"), + row -> row.get(0), + connection.createStatement("SELECT * FROM testNoOutCall")); + + // Execute the procedure with no out parameters. Expect a single Result + // with no update count. Expect the a named Parameter bind to have been + // inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd(:parameter); END;") + .bind("parameter", + Parameters.in(R2dbcType.VARCHAR, "Named Parameter")) + .execute()) + .flatMap(result -> + Mono.from(result.map(row -> "Unexpected")))); + awaitQuery(asList("Indexed Parameter", "Named Parameter"), + row -> row.get(0), + connection.createStatement( + "SELECT * FROM testNoOutCall ORDER BY value")); + + // Delete the previously inserted rows + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testNoOutCall")); + + // Execute the procedure with no out parameters. Expect a single Result + // with no update count. Expect the an indexed based Parameter.In bind to + // have been inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd(?); END;") + .bind(0, Parameters.in(R2dbcType.VARCHAR, "Indexed Parameter.In")) + .execute()) + .flatMap(result -> + Mono.from(result.getRowsUpdated()))); + awaitQuery(asList("Indexed Parameter.In"), + row -> row.get(0), + connection.createStatement("SELECT * FROM testNoOutCall")); + + // Execute the procedure with no out parameters. Expect a single Result + // with no update count. Expect the a named Parameter.In bind to have been + // inserted by the call. + awaitNone(Mono.from(connection.createStatement( + "BEGIN testNoOutCallAdd(:parameter); END;") + .bind("parameter", + Parameters.in(R2dbcType.VARCHAR, "Named Parameter.In")) + .execute()) + .flatMap(result -> + Mono.from(result.map(row -> "Unexpected")))); + awaitQuery(asList("Indexed Parameter.In", "Named Parameter.In"), + row -> row.get(0), + connection.createStatement( + "SELECT * FROM testNoOutCall ORDER BY value")); + } + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE testNoOutCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testNoOutCallAdd")); + tryAwaitNone(connection.close()); + } } /** - * Creates a {@code JdbcStatement} that executes this statement as a DML - * statement returning generated values. - * @return A JDBC call statement publisher + * Verifies {@link OracleStatementImpl#execute()} when calling a procedure + * having a single in-out parameter. */ - private Publisher createJdbcStatement() { - int currentFetchSize = fetchSize; - Object[] currentBinds = transferBinds(); - - return adapter.getLock().get(() -> { - PreparedStatement preparedStatement = - jdbcConnection.prepareStatement(sql); - preparedStatement.setFetchSize(currentFetchSize); - preparedStatement.setQueryTimeout(timeout); - return new JdbcStatement(preparedStatement, currentBinds); - }); - } + @Test + public void testOneInOutCall() { - /** - * Creates a {@code JdbcStatement} that executes this statement with a batch - * of bind values added by {@link #add()}. If one or more values are - * missing in the current set of binds, the statement executes with all - * previously added binds, and then emits an error. - * @return A JDBC batch statement publisher - */ - private Publisher createJdbcBatch() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); - IllegalStateException invalidBinds; try { - add(); - invalidBinds = null; + // Create a table with one value. Create a procedure that updates the + // value and returns the previous value + awaitExecution(connection.createStatement( + "CREATE TABLE testOneInOutCall (value NUMBER)")); + awaitUpdate(1, connection.createStatement( + "INSERT INTO testOneInOutCall VALUES (0)")); + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testOneInOutCallAdd(" + + " inout_value IN OUT NUMBER) IS" + + " previous NUMBER;" + + " BEGIN " + + " SELECT value INTO previous FROM testOneInOutCall;" + + " UPDATE testOneInOutCall SET value = inout_value;" + + " inout_value := previous;" + + " END;")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the IN parameter's value to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneInOutCallAdd(?); END;") + .bind(0, new InOutParameter(1, R2dbcType.NUMERIC)) + .execute(), + result -> { + awaitNone(result.getRowsUpdated()); + }); + awaitQuery(asList(1), + row -> row.get("value", Integer.class), + connection.createStatement("SELECT * FROM testOneInOutCall")); + + // Execute the procedure again with one in-out parameter. Expect a single + // Result with one rows having the previous value. Expect the IN + // parameter's default value to have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneInOutCallAdd(:value); END;") + .bind("value", new InOutParameter(2, R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(1, result.map(row -> + row.get("value", Integer.class)))); + awaitQuery(asList(2), + row -> row.get(0, Integer.class), + connection.createStatement("SELECT * FROM testOneInOutCall")); + + // Execute the procedure with one in-out parameter having an inferred + // type. Expect a single Result with no update count. Expect the IN + // parameter's value to have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneInOutCallAdd(:value); END;") + .bind("value", new InOutParameter(3)) + .execute(), + result -> + awaitNone(result.getRowsUpdated())); + awaitQuery(asList(3), + row -> row.get(0, Integer.class), + connection.createStatement("SELECT * FROM testOneInOutCall"));; + + // Execute the procedure again with one in-out parameter. Expect a single + // Result with one rows having the previous value. Expect the IN + // parameter's value to have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneInOutCallAdd(?); END;") + .bind(0, new InOutParameter(4)) + .execute(), + result -> + awaitOne(3, result.map(row -> + row.get(0, Integer.class)))); + awaitQuery(asList(4), + row -> row.get(0, Integer.class), + connection.createStatement("SELECT * FROM testOneInOutCall")); } - catch (IllegalStateException illegalStateException) { - invalidBinds = illegalStateException; + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE testOneInOutCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testOneInOutCallAdd")); + tryAwaitNone(connection.close()); } - final IllegalStateException finalInvalidBinds = invalidBinds; - - int currentFetchSize = fetchSize; - Queue currentBatch = batch; - batch = new LinkedList<>(); - - return adapter.getLock().get(() -> { - PreparedStatement preparedStatement = - jdbcConnection.prepareStatement(sql); - preparedStatement.setFetchSize(currentFetchSize); - preparedStatement.setQueryTimeout(timeout); - return finalInvalidBinds == null - ? new JdbcBatch(preparedStatement, currentBatch) - : new JdbcBatchInvalidBinds( - preparedStatement, currentBatch, finalInvalidBinds); - }); } /** - * Creates a {@code JdbcStatement} that executes this statement as a - * procedural call returning one or more out-parameters. - * @return A JDBC call statement publisher + * Verifies {@link OracleStatementImpl#execute()} when calling a procedure + * having multiple in-out parameters. */ - private Publisher createJdbcCall() { - int currentFetchSize = fetchSize; - Object[] currentBinds = transferBinds(); - - return adapter.getLock().get(() -> { - CallableStatement callableStatement = jdbcConnection.prepareCall(sql); - callableStatement.setFetchSize(currentFetchSize); - callableStatement.setQueryTimeout(timeout); - return new JdbcCall(callableStatement, currentBinds, parameterNames); - }); - } + @Test + public void testMultiInOutCall() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); - /** - * Creates a {@code JdbcStatement} that executes this statement as a DML - * statement returning generated values. - * @return A JDBC call statement publisher - */ - private Publisher createJdbcReturningGenerated() { - int currentFetchSize = fetchSize; - Object[] currentBinds = transferBinds(); - String[] currentGeneratedColumns = generatedColumns.clone(); - - return adapter.getLock().get(() -> { - PreparedStatement preparedStatement = - currentGeneratedColumns.length == 0 - ? jdbcConnection.prepareStatement(sql, RETURN_GENERATED_KEYS) - : jdbcConnection.prepareStatement(sql, currentGeneratedColumns); - preparedStatement.setFetchSize(currentFetchSize); - preparedStatement.setQueryTimeout(timeout); - return new JdbcReturningGenerated(preparedStatement, currentBinds); - }); + try { + // Create a table with one value. Create a procedure that updates the + // value and returns the previous value + awaitExecution(connection.createStatement( + "CREATE TABLE testMultiInOutCall (value1 NUMBER, value2 NUMBER)")); + awaitUpdate(1, connection.createStatement( + "INSERT INTO testMultiInOutCall VALUES (0, 100)")); + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testMultiInOutCallAdd(" + + " inout_value1 IN OUT NUMBER," + + " inout_value2 IN OUT NUMBER) IS" + + " previous1 NUMBER;" + + " previous2 NUMBER;" + + " BEGIN " + + " SELECT value1, value2 INTO previous1, previous2" + + " FROM testMultiInOutCall;" + + " UPDATE testMultiInOutCall" + + " SET value1 = inout_value1, value2 = inout_value2;" + + " inout_value1 := previous1;" + + " inout_value2 := previous2;" + + " END;")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the IN parameter's value to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiInOutCallAdd(:value1, :value2); END;") + .bind("value1", new InOutParameter(1, R2dbcType.NUMERIC)) + .bind("value2", new InOutParameter(101, R2dbcType.NUMERIC)) + .execute(), + result -> + awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(1, 101)), + row -> asList( + row.get("value1", Integer.class),row.get("value2", Integer.class)), + connection.createStatement("SELECT * FROM testMultiInOutCall")); + + // Execute the procedure again with one in-out parameter. Expect a single + // Result with one rows having the previous value. Expect the IN + // parameter's default value to have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiInOutCallAdd(?, :value2); END;") + .bind(0, new InOutParameter(2, R2dbcType.NUMERIC)) + .bind("value2", new InOutParameter(102, R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(asList(1, 101), result.map(row -> + asList( + row.get(0, Integer.class), row.get("value2", Integer.class))))); + awaitQuery(asList(asList(2, 102)), + row -> + asList(row.get("value1", Integer.class), row.get(1, Integer.class)), + connection.createStatement("SELECT * FROM testMultiInOutCall")); + + // Execute the procedure with one in-out parameter having an inferred + // type. Expect a single Result with no update count. Expect the IN + // parameter's value to have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiInOutCallAdd(?, ?); END;") + .bind(0, new InOutParameter(3)) + .bind(1, new InOutParameter(103)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(3, 103)), + row -> asList(row.get(0, Integer.class), row.get(1, Integer.class)), + connection.createStatement("SELECT * FROM testMultiInOutCall"));; + + // Execute the procedure again with multiple in-out parameters having + // the same name. Expect a single Result with one rows having the + // previous value. Getting the parameter value by name should returned + // the value of the first parameter. Expect the IN parameter's value to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiInOutCallAdd(" + + "inout_value2 => :value2, inout_value1 => :value1); END;") + .bind("value1", new InOutParameter(4)) + .bind("value2", new InOutParameter(104)) + .execute(), + result -> + awaitOne(asList(3, 103), result.map(row -> + asList( + row.get("value1", Integer.class), row.get(0, Integer.class))))); + awaitQuery(asList(asList(4, 104)), + row -> asList(row.get(0, Integer.class), row.get(1, Integer.class)), + connection.createStatement("SELECT * FROM testMultiInOutCall")); + } + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE testMultiInOutCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testMultiInOutCallAdd")); + tryAwaitNone(connection.close()); + } } /** - * Binds a {@code value} to all named parameters matching the specified - * {@code name}. The match is case-sensitive. - * @param name A parameter name. Not null. - * @param value A value to bind. May be null. - * @throws NoSuchElementException if no named parameter matches the - * {@code identifier} + * Verifies {@link OracleStatementImpl#execute()} when calling a procedure + * having a single out parameter. */ - private void bindNamedParameter(String name, Object value) { - boolean isMatched = false; - - for (int i = 0; i < parameterNames.size(); i++) { - if (name.equals(parameterNames.get(i))) { - isMatched = true; - bindObject(i, value); - } + @Test + public void testOneOutCall() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testOneOutCall (" + + "id NUMBER GENERATED ALWAYS AS IDENTITY, value VARCHAR2(100))")); + + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testOneOutCallAdd(" + + " value IN VARCHAR2 DEFAULT 'Default Value'," + + " id OUT NUMBER) IS" + + " BEGIN " + + " INSERT INTO testOneOutCall(value) VALUES (value)" + + " RETURNING testOneOutCall.id INTO id;" + + " END;")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the IN parameter's default value to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(id => ?); END;") + .bind(0, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(1, "Default Value")), + row -> asList(row.get("id", Integer.class), row.get("value")), + connection.createStatement("SELECT * FROM testOneOutCall")); + + // Execute the procedure again with one in-out parameter. Expect a single + // Result with one rows Expect the IN parameter's default value to have + // been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(id => ?); END;") + .bind(0, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(2, result.map(row -> row.get(0, Integer.class)))); + awaitQuery(asList(asList(1, "Default Value"), asList(2, "Default Value")), + row -> asList(row.get("id", Integer.class), row.get("value")), + connection.createStatement("SELECT * FROM testOneOutCall")); + + // Delete the previously inserted rows + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testOneOutCall")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the an indexed based String bind to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(?, ?); END;") + .bind(0, "Indexed Bind") + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(3, "Indexed Bind")), + row -> asList(row.get(0, Integer.class), row.get(1)), + connection.createStatement("SELECT * FROM testOneOutCall")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the a named String bind to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(:parameter, :out); END;") + .bind("parameter", "Named Bind") + .bind("out", Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(4, + result.map(row -> row.get("out", Integer.class)))); + awaitQuery(asList(asList(3, "Indexed Bind"), asList(4, "Named Bind")), + row -> asList(row.get(0, Integer.class), row.get(1)), + connection.createStatement( + "SELECT * FROM testOneOutCall ORDER BY value")); + + // Delete the previously inserted rows + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testOneOutCall")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the an indexed based Parameter bind to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(?, ?); END;") + .bind(0, Parameters.in(R2dbcType.VARCHAR, "Indexed Parameter")) + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(5, "Indexed Parameter")), + row -> asList(row.get(0, Integer.class), row.get(1)), + connection.createStatement("SELECT * FROM testOneOutCall")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the a named Parameter bind to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(:parameter, :out); END;") + .bind("parameter", + Parameters.in(R2dbcType.VARCHAR, "Named Parameter")) + .bind("out", Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(6, + result.map(row -> row.get("out", Integer.class)))); + awaitQuery(asList( + asList(5, "Indexed Parameter"), asList(6, "Named Parameter")), + row -> asList(row.get(0, Integer.class), row.get(1)), + connection.createStatement( + "SELECT * FROM testOneOutCall ORDER BY value")); + + // Delete the previously inserted rows + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testOneOutCall")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the an indexed based Parameter.In bind to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(?, ?); END;") + .bind(0, Parameters.in(R2dbcType.VARCHAR, "Indexed Parameter.In")) + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(7, "Indexed Parameter.In")), + row -> asList(row.get(0, Integer.class), row.get(1)), + connection.createStatement("SELECT * FROM testOneOutCall")); + + // Execute the procedure with one in-out parameter. Expect a single Result + // with no update count. Expect the a named Parameter.In bind to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testOneOutCallAdd(:parameter, :out); END;") + .bind("parameter", + Parameters.in(R2dbcType.VARCHAR, "Named Parameter.In")) + .bind("out", Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(result.map(row -> row.get("out")))); + awaitQuery(asList( + asList(7, "Indexed Parameter.In"), asList(8, "Named Parameter.In")), + row -> asList(row.get(0, Integer.class), row.get(1)), + connection.createStatement( + "SELECT * FROM testOneOutCall ORDER BY value")); } - - if (! isMatched) { - throw new NoSuchElementException( - "Unrecognized parameter identifier: " + name); + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE testOneOutCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testOneOutCallAdd")); + tryAwaitNone(connection.close()); } } /** - * Binds an {@code object} to a parameter {@code index}. If the {@code object} - * is an instance of {@link Parameter}, then its Java type and SQL type are - * validated as types that Oracle R2DBC supports. If the {@code object} - * is not an instance of {@code Parameter}, then only its Java type is - * validated. - * - * @param object Bind value to retain. Not null. - * @throws IllegalArgumentException If {@code object} is a {@code Parameter}, - * and the class of the value is not supported as a bind value. - * @throws IllegalArgumentException If {@code object} is a {@code Parameter}, - * and the SQL type is not supported as a bind value. - * @throws IllegalArgumentException If {@code object} is not a - * {@code Parameter}, and the class of {@code object} is not supported as a - * bind value. + * Verifies {@link OracleStatementImpl#execute()} when calling a procedure + * having a single out parameters. */ - private void bindObject(int index, Object object) { - if (object == null){ - bindValues[index] = NULL_BIND; - } - else if (object instanceof Parameter) { - bindParameter(index, (Parameter)object); - } - else if (object instanceof Parameter.In - || object instanceof Parameter.Out) { - throw new IllegalArgumentException( - "Parameter.In and Parameter.Out bind values must implement Parameter"); + @Test + public void testMultiOutCall() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testMultiOutCall (" + + "id NUMBER GENERATED ALWAYS AS IDENTITY, value VARCHAR2(100))")); + + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testMultiOutCallAdd(" + + " value IN VARCHAR2 DEFAULT 'Default Value'," + + " id OUT NUMBER," + + " new_count OUT NUMBER) IS" + + " BEGIN " + + " INSERT INTO testMultiOutCall(value) VALUES (value)" + + " RETURNING testMultiOutCall.id INTO id;" + + " SELECT COUNT(*) INTO new_count FROM testMultiOutCall;" + + " END;")); + + // Execute the procedure with two out parameters. Expect a single Result + // with no update count. Expect the IN parameter's default value to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(id => ?, new_count => ?); END;") + .bind(0, Parameters.out(R2dbcType.NUMERIC)) + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(1, "Default Value")), + readable -> asList(readable.get("id", Integer.class), readable.get("value")), + connection.createStatement("SELECT * FROM testMultiOutCall")); + + // Execute the procedure again with two out parameters. Expect a single + // Result with one readables Expect the IN parameter's default value to have + // been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(id => ?, new_count => ?); END;") + .bind(0, Parameters.out(R2dbcType.NUMERIC)) + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(asList(2, 2), result.map(readable -> + asList(readable.get(0, Integer.class), readable.get(1, Integer.class))))); + awaitQuery(asList(asList(1, "Default Value"), asList(2, "Default Value")), + readable -> asList(readable.get("id", Integer.class), readable.get("value")), + connection.createStatement("SELECT * FROM testMultiOutCall")); + + // Delete the previously inserted readables + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testMultiOutCall")); + + // Execute the procedure with two out parameters. Expect a single Result + // with no update count. Expect the an indexed based String bind to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(?, ?, ?); END;") + .bind(0, "Indexed Bind") + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .bind(2, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(3, "Indexed Bind")), + readable -> asList(readable.get(0, Integer.class), readable.get(1)), + connection.createStatement("SELECT * FROM testMultiOutCall")); + + // Execute the procedure with two out parameters. Expect a single Result + // with no update count. Expect the a named String bind to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(:parameter, :out, :newCount); END;") + .bind("parameter", "Named Bind") + .bind("out", Parameters.out(R2dbcType.NUMERIC)) + .bind("newCount", Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(asList(4, 2), result.map(readable -> + asList(readable.get("out", Integer.class), + readable.get("newCount", Integer.class))))); + awaitQuery(asList(asList(3, "Indexed Bind"), asList(4, "Named Bind")), + readable -> asList(readable.get(0, Integer.class), readable.get(1)), + connection.createStatement( + "SELECT * FROM testMultiOutCall ORDER BY value")); + + // Delete the previously inserted readables + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testMultiOutCall")); + + // Execute the procedure with two out parameters. Expect a single Result + // with no update count. Expect the an indexed based Parameter bind to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(?, ?, ?); END;") + .bind(0, Parameters.in(R2dbcType.VARCHAR, "Indexed Parameter")) + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .bind(2, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(5, "Indexed Parameter")), + readable -> asList(readable.get(0, Integer.class), readable.get(1)), + connection.createStatement("SELECT * FROM testMultiOutCall")); + + // Execute the procedure with two out parameters. Expect a single Result + // with no update count. Expect the a named Parameter bind to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(:parameter, :out, :newCount); END;") + .bind("parameter", + Parameters.in(R2dbcType.VARCHAR, "Named Parameter")) + .bind("out", Parameters.out(R2dbcType.NUMERIC)) + .bind("newCount", Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> + awaitOne(asList(6, 2), result.map(readable -> + asList(readable.get("out", Integer.class), + readable.get("newCount", Integer.class))))); + awaitQuery(asList( + asList(5, "Indexed Parameter"), asList(6, "Named Parameter")), + readable -> asList(readable.get(0, Integer.class), readable.get(1)), + connection.createStatement( + "SELECT * FROM testMultiOutCall ORDER BY value")); + + // Delete the previously inserted readables + awaitExecution(connection.createStatement( + "TRUNCATE TABLE testMultiOutCall")); + + // Execute the procedure with two out parameters. Expect a single Result + // with no update count. Expect the an indexed based Parameter.In bind to + // have been inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(?, ?, ?); END;") + .bind(0, Parameters.in(R2dbcType.VARCHAR, "Indexed Parameter.In")) + .bind(1, Parameters.out(R2dbcType.NUMERIC)) + .bind(2, Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitNone(result.getRowsUpdated())); + awaitQuery(asList(asList(7, "Indexed Parameter.In")), + readable -> asList(readable.get(0, Integer.class), readable.get(1)), + connection.createStatement("SELECT * FROM testMultiOutCall")); + + // Execute the procedure with two out parameters. Expect a single Result + // with no update count. Expect the a named Parameter.In bind to have been + // inserted by the call. + consumeOne(connection.createStatement( + "BEGIN testMultiOutCallAdd(:parameter, :out, :newCount); END;") + .bind("parameter", + Parameters.in(R2dbcType.VARCHAR, "Named Parameter.In")) + .bind("out", Parameters.out(R2dbcType.NUMERIC)) + .bind("newCount", Parameters.out(R2dbcType.NUMERIC)) + .execute(), + result -> awaitOne(result.map(readable -> readable.get("out")))); + awaitQuery(asList( + asList(7, "Indexed Parameter.In"), asList(8, "Named Parameter.In")), + readable -> asList(readable.get(0, Integer.class), readable.get(1)), + connection.createStatement( + "SELECT * FROM testMultiOutCall ORDER BY value")); } - else { - requireSupportedJavaType(object); - bindValues[index] = object; + finally { + tryAwaitExecution(connection.createStatement("DROP TABLE testMultiOutCall")); + tryAwaitExecution(connection.createStatement( + "DROP PROCEDURE testMultiOutCallAdd")); + tryAwaitNone(connection.close()); } } /** - * Binds a {@code parameter} to a specified {@code index} of this - * {@code Statement}. - * @param index A 0-based parameter index - * @param parameter Parameter to bind - * @throws IllegalArgumentException If the Java or SQL type of the - * {@code parameter} is not supported. + * Verify {@link OracleStatementImpl#execute()} when calling a procedure + * having no out binds and returning implicit results. */ - private void bindParameter(int index, Parameter parameter) { + @Test + public void testNoOutImplicitResult() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testNoOutImplicitResult (count NUMBER)")); + + // Load [0,100] into the table + Statement insert = connection.createStatement( + "INSERT INTO testNoOutImplicitResult VALUES (?)"); + IntStream.range(0, 100) + .forEach(i -> insert.bind(0, i).add()); + insert.bind(0, 100); + awaitOne(101L, Flux.from(insert.execute()) + .flatMap(Result::getRowsUpdated) + .reduce(0L, (total, updateCount) -> total + updateCount)); + + // Create a procedure that returns a cursor + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE countDown (" + + " countFrom IN NUMBER DEFAULT 100)" + + " IS" + + " countDownCursor SYS_REFCURSOR;" + + " BEGIN" + + " OPEN countDownCursor FOR " + + " SELECT count FROM testNoOutImplicitResult" + + " WHERE count <= countFrom" + + " ORDER BY count DESC;" + + " DBMS_SQL.RETURN_RESULT(countDownCursor);" + + " END;")); + + // Execute without setting the countFrom parameter, and expect one + // Result with rows counting down from the default countFrom value, 100 + awaitQuery(Stream.iterate( + 100, previous -> previous >= 0, previous -> previous - 1) + .collect(Collectors.toList()), + row -> row.get(0, Integer.class), + connection.createStatement("BEGIN countDown; END;")); + + // Execute with with an in bind parameter, and expect one + // Result with rows counting down from the parameter value + awaitQuery(Stream.iterate( + 10, previous -> previous >= 0, previous -> previous - 1) + .collect(Collectors.toList()), + row -> row.get(0, Integer.class), + connection.createStatement("BEGIN countDown(?); END;") + .bind(0, 10)); + + // Create a procedure that returns multiple cursors + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE countDown (" + + " countFrom IN NUMBER DEFAULT 50)" + + " IS" + + " countDownCursor SYS_REFCURSOR;" + + " countUpCursor SYS_REFCURSOR;" + + " BEGIN" + + + " OPEN countDownCursor FOR " + + " SELECT count FROM testNoOutImplicitResult" + + " WHERE count <= countFrom" + + " ORDER BY count DESC;" + + " DBMS_SQL.RETURN_RESULT(countDownCursor);" + + + " OPEN countUpCursor FOR " + + " SELECT count FROM testNoOutImplicitResult" + + " WHERE count >= countFrom" + + " ORDER BY count;" + + " DBMS_SQL.RETURN_RESULT(countUpCursor);" + + + " END;")); + + + awaitMany(asList( + // countDownCursor + Stream.iterate( + 50, previous -> previous >= 0, previous -> previous - 1) + .collect(Collectors.toList()), + // countUpCursor + Stream.iterate( + 50, previous -> previous <= 100, previous -> previous + 1) + .collect(Collectors.toList())), + // Map rows of two Result.map(..) publishers into two Lists + Flux.from(connection.createStatement("BEGIN countDown; END;") + .execute()) + .concatMap(result -> + Flux.from(result.map(row -> + row.get(0, Integer.class))) + .collectList())); + + // Expect Implicit Results to have no update counts + AtomicLong count = new AtomicLong(-9); + awaitMany(asList(-9L, -10L), + Flux.from(connection.createStatement("BEGIN countDown; END;") + .execute()) + .concatMap(result -> + Flux.from(result.getRowsUpdated()) + .defaultIfEmpty(count.getAndDecrement()))); - if (parameter instanceof Parameter.Out) { - if (! batch.isEmpty()) - throw outParameterWithBatch(); - if (generatedColumns != null) - throw outParameterWithGeneratedValues(); } - - // TODO: This method should check if Java type can be converted to the - // specified SQL type. If the conversion is unsupported, then JDBC - // setObject(...) will throw when this statement is executed. The correct - // behavior is to throw IllegalArgumentException here, and not from - // execute() - Type r2dbcType = - requireNonNull(parameter.getType(), "Parameter type is null"); - SQLType jdbcType = toJdbcType(r2dbcType); - - if (jdbcType == null) { - throw new IllegalArgumentException( - "Unsupported SQL type: " + r2dbcType); + finally { + tryAwaitExecution(connection.createStatement("DROP PROCEDURE countDown")); + tryAwaitExecution(connection.createStatement( + "DROP TABLE testNoOutImplicitResult")); + tryAwaitNone(connection.close()); } - - requireSupportedJavaType(parameter.getValue()); - bindValues[index] = parameter; } /** - * Checks that the specified 0-based {@code index} is within the range of - * valid parameter indexes for this statement. - * @param index A 0-based parameter index - * @throws IndexOutOfBoundsException If the {@code index} is outside of the - * valid range. + * Verify {@link OracleStatementImpl#execute()} when calling a procedure + * having out binds and returning implicit results. */ - private void requireValidIndex(int index) { - if (parameterNames.isEmpty()) { - throw new IndexOutOfBoundsException( - "Statement has no parameter markers"); - } - else if (index < 0) { - throw new IndexOutOfBoundsException( - "Parameter index is non-positive: " + index); + @Test + public void testOutAndImplicitResult() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testOutAndImplicitResult (count NUMBER)")); + + // Load [0,100] into the table + Statement insert = connection.createStatement( + "INSERT INTO testOutAndImplicitResult VALUES (?)"); + IntStream.range(0, 100) + .forEach(i -> insert.bind(0, i).add()); + insert.bind(0, 100); + awaitOne(101L, Flux.from(insert.execute()) + .flatMap(Result::getRowsUpdated) + .reduce(0L, (total, updateCount) -> total + updateCount)); + + // Create a procedure that returns a cursor + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE countDown (" + + " outValue OUT VARCHAR2)" + + " IS" + + " countDownCursor SYS_REFCURSOR;" + + " BEGIN" + + " outValue := 'test';" + + " OPEN countDownCursor FOR " + + " SELECT count FROM testOutAndImplicitResult" + + " WHERE count <= 100" + + " ORDER BY count DESC;" + + " DBMS_SQL.RETURN_RESULT(countDownCursor);" + + " END;")); + + // Expect one Result with rows counting down from 100, then one Result + // with the out bind value + awaitMany(asList( + Stream.iterate( + 100, previous -> previous >= 0, previous -> previous - 1) + .map(String::valueOf) + .collect(Collectors.toList()), + asList("test")), + Flux.from(connection.createStatement("BEGIN countDown(:outValue); END;") + .bind("outValue", Parameters.out(R2dbcType.VARCHAR)) + .execute()) + .concatMap(result -> + Flux.from(result.map(row -> + row.get(0, String.class))) + .collectList())); + + // Create a procedure that returns multiple cursors + awaitExecution(connection.createStatement( + "CREATE OR REPLACE PROCEDURE countDown (" + + " outValue OUT VARCHAR2)" + + " IS" + + " countDownCursor SYS_REFCURSOR;" + + " countUpCursor SYS_REFCURSOR;" + + " BEGIN" + + + " outValue := 'test';" + + + " OPEN countDownCursor FOR " + + " SELECT count FROM testOutAndImplicitResult" + + " WHERE count <= 50" + + " ORDER BY count DESC;" + + " DBMS_SQL.RETURN_RESULT(countDownCursor);" + + + " OPEN countUpCursor FOR " + + " SELECT count FROM testOutAndImplicitResult" + + " WHERE count >= 50" + + " ORDER BY count;" + + " DBMS_SQL.RETURN_RESULT(countUpCursor);" + + + " END;")); + + + awaitMany(asList( + // countDownCursor + Stream.iterate( + 50, previous -> previous >= 0, previous -> previous - 1) + .map(String::valueOf) + .collect(Collectors.toList()), + // countUpCursor + Stream.iterate( + 50, previous -> previous <= 100, previous -> previous + 1) + .map(String::valueOf) + .collect(Collectors.toList()), + asList("test")), + // Map rows of two Result.map(..) publishers into two Lists + Flux.from(connection.createStatement("BEGIN countDown(:outValue); END;") + .bind("outValue", Parameters.out(R2dbcType.VARCHAR)) + .execute()) + .concatMap(result -> + Flux.from(result.map(row -> + row.get(0, String.class))) + .collectList())); + + // Expect Implicit Results to have no update counts + AtomicLong count = new AtomicLong(-8); + awaitMany(asList(-8L, -9L, -10L), + Flux.from(connection.createStatement("BEGIN countDown(?); END;") + .bind(0, Parameters.out(R2dbcType.VARCHAR)) + .execute()) + .concatMap(result -> + Flux.from(result.getRowsUpdated()) + .defaultIfEmpty(count.getAndDecrement()))); + } - else if (index >= parameterNames.size()) { - throw new IndexOutOfBoundsException( - "Parameter index is out of range: " + index - + ". Largest index is: " + (parameterNames.size() - 1)); + finally { + tryAwaitExecution(connection.createStatement("DROP PROCEDURE countDown")); + tryAwaitExecution(connection.createStatement( + "DROP TABLE testOutAndImplicitResult")); + tryAwaitNone(connection.close()); } } /** - * Adds the current set of {@link #bindValues} to the {@link #batch}, and - * then resets the {@code parameters} array to store {@code null} at all - * positions. - * @throws IllegalStateException If a parameter has not been set - * @throws IllegalStateException If an out parameter has been set + * Verifies that {@link OracleStatementImpl#execute()} emits a {@link Result} + * with a {@link Message} segment when the execution results in a + * warning. */ - private void addBatchValues() { - if (generatedColumns != null) - throw generatedValuesWithBatch(); + @Test + public void testWarningMessage() { + Connection connection = + Mono.from(sharedConnection()).block(connectTimeout()); + try { - for (Object parameter : bindValues) { - if (parameter == null) { - throw parameterNotSet(); - } - else if (parameter instanceof Parameter.Out) { - throw outParameterWithBatch(); - } + // Create a procedure using invalid syntax and expect the Result to + // have a Message with an R2dbcException having a SQLWarning as it's + // initial cause. Expect the Result to have an update count of zero as + // well, indicating that the statement completed after the warning. + AtomicInteger segmentCount = new AtomicInteger(0); + R2dbcException r2dbcException = + awaitOne(Flux.from(connection.createStatement( + "CREATE OR REPLACE PROCEDURE testWarningMessage" + + " IS BEGIN;") + .execute()) + .concatMap(result -> + result.flatMap(segment -> { + int index = segmentCount.getAndIncrement(); + if (index == 0) { + assertTrue(segment instanceof Message, + "Unexpected Segment: " + segment); + return Mono.just(((Message)segment).exception()); + } + else if (index == 1) { + assertTrue(segment instanceof UpdateCount, + "Unexpected Segment: " + segment); + assertEquals(0, ((UpdateCount)segment).value()); + return Mono.empty(); + } + else { + fail("Unexpected Segment: " + segment); + return Mono.error(new AssertionError("Should not reach here")); + } + }))); + + // Expect ORA-17110 for an execution that completed with a warning + assertEquals(17110, r2dbcException.getErrorCode()); + Throwable cause = r2dbcException.getCause(); + assertTrue(cause instanceof SQLWarning, "Unexpected cause: " + cause); + assertEquals(17110, ((SQLWarning)cause).getErrorCode()); + assertNull(cause.getCause()); } - - batch.add(bindValues.clone()); - Arrays.fill(bindValues, null); - } - - /** - * Returns {@code true} if {@link #bindValues} contains an out parameter. - * @return {@code true} if an out parameter is present, otherwise - * {@code false} - */ - private boolean isOutParameterPresent() { - for (Object value : bindValues) { - if (value instanceof Parameter.Out) - return true; + finally { + tryAwaitNone(connection.close()); } - return false; } /** - * Returns a copy of the current set of bind values. This method is called - * before executing with the current set of bind values, so it will verify - * that all values are set and then clear the current set for the next - * execution. - * @return A copy of the bind values + * Verifies that concurrent statement execution on a single + * connection does not cause threads to block when there are many threads + * available. */ - private Object[] transferBinds() { - requireAllParametersSet(); - Object[] currentBinds = bindValues.clone(); - Arrays.fill(bindValues, null); - return currentBinds; + @Test + public void testConcurrentExecuteManyThreads() throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(4); + try { + Connection connection = awaitOne(connect(executorService)); + try { + verifyConcurrentExecute(connection); + } + finally { + tryAwaitNone(connection.close()); + } + } + finally { + executorService.shutdown(); + executorService.awaitTermination( + sqlTimeout().toSeconds(), TimeUnit.SECONDS); + } } /** - * Checks that a bind value has been set for all positions in the - * current set of {@link #bindValues} - * @throws IllegalStateException if one or more parameters are not set. + * Verifies that concurrent statement execution on a single + * connection does not cause threads to block when there is just one thread + * available. */ - private void requireAllParametersSet() { - for (Object parameter : bindValues) { - if (parameter == null) - throw parameterNotSet(); + @Test + public void testConcurrentExecuteSingleThread() throws InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + Connection connection = awaitOne(connect(executorService)); + try { + verifyConcurrentExecute(connection); + } + finally { + tryAwaitNone(connection.close()); + } + } + finally { + executorService.shutdown(); + executorService.awaitTermination( + sqlTimeout().toSeconds(), TimeUnit.SECONDS); } } /** - * Returns an exception indicating that a parameter has not been set. - * @return Unset parameter exception + * Verifies that concurrent statement execution and row fetching on a single + * connection does not cause threads to block when there is just one thread + * available. */ - private static IllegalStateException parameterNotSet() { - return new IllegalStateException("One or more parameters are not set"); + @Test + public void testConcurrentFetchSingleThread() throws InterruptedException { + ExecutorService executorService = Executors.newSingleThreadExecutor(); + try { + Connection connection = awaitOne(connect(executorService)); + try { + verifyConcurrentFetch(connection); + } + finally { + tryAwaitNone(connection.close()); + } + } + finally { + executorService.shutdown(); + executorService.awaitTermination( + sqlTimeout().toSeconds(), TimeUnit.SECONDS); + } } /** - * Checks that the class type of an {@code object} is supported as a bind - * value. - * @param object Object to check. May be null. - * @throws IllegalArgumentException If the class type of {@code object} is not - * supported + * Verifies that concurrent statement execution and row fetching on a single + * connection does not cause threads to block when there are many threads + * available. */ - private static void requireSupportedJavaType(Object object) { - if (object != null && toJdbcType(object.getClass()) == null) { - throw new IllegalArgumentException( - "Unsupported Java type:" + object.getClass()); + @Test + public void testConcurrentFetchManyThreads() throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(4); + try { + Connection connection = awaitOne(connect(executorService)); + try { + verifyConcurrentFetch(connection); + } + finally { + tryAwaitNone(connection.close()); + } + } + finally { + executorService.shutdown(); + executorService.awaitTermination( + sqlTimeout().toSeconds(), TimeUnit.SECONDS); } } /** - * Returns an exception indicating that it is not possible to execute a - * statement that returns both out-parameters and generated values. There - * is no JDBC API to create a {@link CallableStatement} that returns - * generated values (aka: generated keys). - * @return Exception for configuring out-parameters with generated values. + * Verifies behavior when commitTransaction() and close() Publishers are + * subscribed to concurrently due to cancelling a Flux.usingWhen(...) + * operator. */ - private static IllegalStateException outParameterWithGeneratedValues() { - return new IllegalStateException( - "Statement can not return both out-parameters and generated values"); - } + @Test + public void testUsingWhenCancel() { + Connection connection = awaitOne(sharedConnection()); + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testUsingWhenCancel (value NUMBER)")); + + // Use more threads than what the FJP has available + Publisher[] publishers = + new Publisher[ForkJoinPool.getCommonPoolParallelism() * 4]; + + for (int i = 0; i < publishers.length; i++) { + + int value = i; + + // The hasElements operator below will cancel its subscription upon + // receiving onNext. This triggers a subscription to the + // commitTransaction() publisher, immediately followed by a subscription + // to the close() publisher. Expect the driver to defer the subscription + // to the close() publisher until the commitTransaction publisher has + // completed. If not deferred, then the thread subscribing to the close + // publisher will block, and this test will deadlock as the + // commitTransaction publisher has no available thread to complete with. + Mono mono = Flux.usingWhen( + newConnection(), + newConnection -> + Flux.usingWhen( + Mono.from(newConnection.beginTransaction()) + .thenReturn(newConnection), + newConnection0 -> + Flux.from(newConnection.createStatement( + "INSERT INTO testUsingWhenCancel VALUES (?)") + .bind(0, value) + .execute()) + .flatMap(Result::getRowsUpdated), + Connection::commitTransaction), + Connection::close) + .hasElements() + .cache(); + + mono.subscribe(); + publishers[i] = mono; + } - /** - * Returns an exception indicating that it is not possible to execute a - * statement with a batch of out-parameters. This is not supported by - * Oracle Database, although it would be possible to emulate it by - * executing a sequence of {@link CallableStatement}s individually (TODO?) - * @return Exception for batching out-parameters. - */ - private static IllegalStateException outParameterWithBatch() { - return new IllegalStateException( - "Batch execution with out parameters is not supported"); - } + awaitMany( + Stream.generate(() -> true) + .limit(publishers.length) + .collect(Collectors.toList()), + Flux.merge(publishers)); - /** - * Returns an exception indicating that it is not possible to execute a - * statement as a batch and returning generated values. This is not supported - * by Oracle Database, although it would be possible to emulate it by - * executing a sequence of {@link PreparedStatement}s individually (TODO?) - * @return Exception for batching with generated values - */ - private static IllegalStateException generatedValuesWithBatch() { - return new IllegalStateException( - "Batch execution returning generated values is not supported"); + } + finally { + // Note that Flux.usingWhen doesn't actually wait for the + // commitTransaction publisher to complete (because the downstream + // subscriber has already cancelled the subscription, so it can't + // receive the result anyway). + // This means the transactions may not have ended by the time the + // drop table command executes. Set a DDL wait timeout to avoid a + // "Resource busy..." error from the database. + tryAwaitExecution(connection.createStatement( + "ALTER SESSION SET ddl_lock_timeout=15")); + tryAwaitExecution(connection.createStatement( + "DROP TABLE testUsingWhenCancel")); + tryAwaitNone(connection.close()); + } } /** - *

- * A statement that is executed using JDBC. The base class is implemented to - * execute SQL that returns an update count, row data, or implicit results - * (ie: DBMS_SQL.RETURN_RESULT). - *

- * Subclasses may extend the base class to handle other types of results, - * such as DML returning generated values, a procedural call that - * returns out-parameters, or a batch of DML update counts. - *

- * The base class ensures that all resources allocated for the statement - * execution are eventually deallocated. This includes the - * {@link #preparedStatement}, along with resources allocated for bind - * values, such as {@code java.sql.Blob/Clob} objects. - *

+ * Verifies that {@link R2dbcException#getSql()} returns the SQL command + * that caused an exception. */ - private class JdbcStatement { - - /** The {@code PreparedStatement} that is executed */ - protected final PreparedStatement preparedStatement; - - /** The bind values that are set on the {@link #preparedStatement} */ - protected final Object[] binds; - - /** - * Publisher that deallocate resources after the - * {@link #preparedStatement} is executed - */ - private Publisher deallocators = Mono.empty(); - - /** - * Constructs a new {@code JdbcStatement} that executes a - * {@code preparedStatement} with the given {@code binds}. - * @param preparedStatement Statement to execute. Not null. Retained. - * @param binds Bind values. Not null. Retained. - */ - private JdbcStatement(PreparedStatement preparedStatement, Object[] binds) { - this.preparedStatement = preparedStatement; - this.binds = binds; - } - - /** - *

- * Executes this statement and returns a publisher that emits the results. - *

- * This method first subscribes to the {@link #bind()} publisher, and then - * subscribes to the {@link #executeJdbc()} publisher after the bind - * publisher has completed. Subclasses may override the {@code bind} and - * {@code getResults} methods as needed for different types of binds and - * results. - *

- * This method is implemented to create {@code Results} of - * {@link Result.Message} segments if an {@link R2dbcException} is - * emitted from the {@code bind} or {@code getResults} publishers, or if - * {@link PreparedStatement#getWarnings()} yields a warning. - *

- * After all {@code Results} have been consumed, the - * {@link #preparedStatement} is closed. - *

- * @return A publisher that emits the result of executing this statement - */ - final Publisher execute() { - - if(true)return Flux.usingWhen(Mono.just(new ArrayList<>(1)), - results -> - Mono.from(bind()) - .thenMany(executeJdbc()) - .map(this::getWarnings) - .doOnNext(results::add) - .onErrorResume(R2dbcException.class, r2dbcException -> - Mono.just(createErrorResult(r2dbcException))), - this::deallocate); - - List results = new ArrayList<>(1); - Publisher deallocate = - Mono.defer(() -> Mono.from(deallocate(results))) - .cast(OracleResultImpl.class); - - return Flux.concatDelayError( - Mono.from(bind()) - .thenMany(executeJdbc()) - .map(this::getWarnings) - .doOnNext(results::add) - .onErrorResume(R2dbcException.class, r2dbcException -> - Mono.just(createErrorResult(r2dbcException))), - deallocate) - .doOnCancel(() -> - Mono.from(deallocate).subscribe()); - } - - /** - *

- * Sets {@link #binds} on the {@link #preparedStatement}. The - * returned {@code Publisher} completes after all bind values have - * materialized and been set on the {@code preparedStatement}. - *

- * The base class implements this method to ignore any bind values that - * that are instances of {@link Parameter.Out}, and not also an instance of - * {@link Parameter.In}. Subclasses may override this method handle - * out-parameters, or to bind a batch of values. - *

- * @return A {@code Publisher} that emits {@code onComplete} when all - * {@code binds} have been set. - */ - protected Publisher bind() { - return bind(binds); - } - - protected final Publisher bind(Object[] binds) { - return adapter.getLock().flatMap(() -> { - List> bindPublishers = null; - for (int i = 0; i < binds.length; i++) { - - if (binds[i] instanceof Parameter.Out - && !(binds[i] instanceof Parameter.In)) - continue; - - Object jdbcValue = convertBind(binds[i]); - SQLType jdbcType = - binds[i] instanceof Parameter - ? toJdbcType(((Parameter) binds[i]).getType()) - : null; // JDBC infers the type - - if (jdbcValue instanceof Publisher) { - int indexFinal = i; - Publisher bindPublisher = - Mono.from((Publisher) jdbcValue) - .doOnSuccess(allocatedValue -> - setBind(indexFinal, allocatedValue, jdbcType)) - .then(); - - if (bindPublishers == null) - bindPublishers = new LinkedList<>(); - - bindPublishers.add(bindPublisher); - } - else { - setBind(i, jdbcValue, jdbcType); - } - } - - return bindPublishers == null - ? Mono.empty() - : Flux.concat(bindPublishers); - }); + @Test + public void testGetSql() { + Connection connection = awaitOne(sharedConnection()); + try { + String badSql = "SELECT 0 FROM dooool"; + Result result = awaitOne(connection.createStatement(badSql).execute()); + R2dbcException r2dbcException = assertThrows(R2dbcException.class, () -> + awaitOne(result.getRowsUpdated())); + assertEquals(badSql, r2dbcException.getSql()); } - - /** - * Executes the JDBC {@link #preparedStatement} and maps the - * results into R2DBC {@link Result} objects. The base class implements - * this method to get results of update count, row data, or implicit - * results (ie: DBMS_SQL.RETURN_RESULT). Subclasses may override this - * method to produce different types of results. - * @return A publisher that emits the results. - */ - protected Publisher executeJdbc() { - return Mono.from(adapter.publishSQLExecution(preparedStatement)) - .flatMapMany(this::getResults); + finally { + tryAwaitNone(connection.close()); } + } - /** - * Publishes the current result of the {@link #preparedStatement}, along - * with any results that follow after calling - * {@link PreparedStatement#getMoreResults()} - * - * @param isResultSet {@code true} if the current result is a - * {@code ResultSet}, otherwise {@code false}. - * @return A publisher that emits all results of the - * {@code preparedStatement} - */ - protected final Publisher getResults( - boolean isResultSet) { - - return adapter.getLock().flatMap(() -> { - - OracleResultImpl result = getCurrentResult(isResultSet); - OracleResultImpl nextResult = getCurrentResult( - preparedStatement.getMoreResults(KEEP_CURRENT_RESULT)); - - // Don't allocate a list unless there are multiple results. Multiple - // results should only happen when using DBMS_SQL.RETURN_RESULT - // within a PL/SQL call - if (nextResult == null) { - return Mono.justOrEmpty(result); + // TODO: Repalce with Parameters.inOut when that's available + private static final class InOutParameter + implements Parameter, Parameter.In, Parameter.Out { + final Type type; + final Object value; + + InOutParameter(Object value) { + this(value, new Type.InferredType() { + @Override + public Class getJavaType() { + return value.getClass(); } - else { - ArrayList results = new ArrayList<>(); - - // The first result may be null if additional results follow - if (result != null) - results.add(result); - - while (nextResult != null) { - results.add(nextResult); - nextResult = getCurrentResult( - preparedStatement.getMoreResults(KEEP_CURRENT_RESULT)); - } - return Flux.fromIterable(results); + @Override + public String getName() { + return "Inferred"; } }); } - /** - * Adds a {@code publisher} for deallocating a resource that this - * statement has allocated. The {@code publisher} is subscribed to after - * this statement has executed, possibly before all results have been - * consumed. If multiple dealloaction publishers are added, each one is - * subscribed to sequentially, and errors emitted by the publishers are - * suppressed until all publishers have been subscribed to. - * @param publisher Resource deallocation publisher - */ - protected void addDeallocation(Publisher publisher) { - deallocators = Flux.concatDelayError(deallocators, publisher); - } - - /** - * Returns the current {@code Result} of the {@link #preparedStatement}. - * This method returns a result of row data if {@code isResultSet} is - * {@code true}. Otherwise, this method returns a result of an update - * count if {@link PreparedStatement#getUpdateCount()} returns a value of 0 - * or greater. Otherwise, this method returns {@code null} if - * {@code isResultSet} is {@code false} and {@code getUpdateCount} - * returns a negative number. - * @param isResultSet {@code true} if the current result is row data, - * otherwise false. - * @return The current {@code Result} of the {@code preparedStatement} - */ - private OracleResultImpl getCurrentResult(boolean isResultSet) { - return fromJdbc(() -> { - if (isResultSet) { - return createQueryResult( - preparedStatement.getResultSet(), adapter); - } - else { - long updateCount = preparedStatement.getLargeUpdateCount(); - return updateCount >= 0 - ? createUpdateCountResult(updateCount) - : null; - } - }); - } - - /** - * Returns a {@code Result} that publishes any {@link SQLWarning}s of the - * {@link #preparedStatement} as {@link io.r2dbc.spi.Result.Message} - * segments followed by any {@code Segments} of a {@code result}. This - * method returns the provided {@code result} if the {@code - * preparedStatement} has - * no warnings. - * @param result Result of executing the {@code preparedStatement} - * @return A {@code Result} having any warning messages of the - * {@code preparedStatement} along with its execution {@code result}. - */ - private OracleResultImpl getWarnings(OracleResultImpl result) { - return fromJdbc(() -> { - SQLWarning warning = preparedStatement.getWarnings(); - preparedStatement.clearWarnings(); - return warning == null - ? result - : OracleResultImpl.createWarningResult(warning, result); - }); - } - - /** - *

- * Deallocates all resources that have been allocated by this statement. - * If the deallocation of any resource results in an error, an attempt is - * made to deallocate any remaining resources before emitting the error. - *

- * The returned publisher subscribes to the {@link #deallocators} - * publisher, and may close the {@link #preparedStatement} if all {@code - * results} have been consumed when this method is called. - *

- * If one or more {@code results} have yet to be consumed, then this method - * arranges for the {@link #preparedStatement} to be closed after all - * results have been consumed. A result may be backed by a - * {@link java.sql.ResultSet} or by {@link CallableStatement}, so the - * {@link #preparedStatement} must remain open until all results have - * been consumed. - *

- * @param results Results that must be consumed before closing the - * {@link #preparedStatement} - * @return A publisher that completes when all resources have been - * deallocated - */ - private Publisher deallocate(Collection results) { - - // Close the statement after all results are consumed - AtomicInteger unconsumed = new AtomicInteger(results.size()); - Publisher closeStatement = adapter.getLock().run(() -> { - if (unconsumed.decrementAndGet() == 0) - preparedStatement.close(); - }); - - for (OracleResultImpl result : results) { - if (!result.onConsumed(closeStatement)) - unconsumed.decrementAndGet(); - } - - // If all results have already been consumed, the returned - // publisher closes the statement - if (unconsumed.get() == 0) - addDeallocation(adapter.getLock().run(preparedStatement::close)); - - return deallocators; - } - - /** - * Sets the {@code value} of a {@code preparedStatement} parameter at the - * specified {@code index}. If a non-null {@code type} is provided, then it is - * specified as the SQL type for the bind. Otherwise, if the - * {@code type} is {@code null}, then the JDBC driver infers the SQL type - * of the bind. - * @param index 0-based parameter index - * @param value Value. May be null. - * @param type SQL type. May be null. - */ - private void setBind(int index, Object value, SQLType type) { - runJdbc(() -> { - int jdbcIndex = index + 1; - if (type != null) - preparedStatement.setObject(jdbcIndex, value, type); - else - preparedStatement.setObject(jdbcIndex, value); - }); - } - - /** - *

- * Converts a {@code value} of a type that is supported by R2DBC into an - * equivalent type that is supported by JDBC. The object returned by this - * method will express the same information as the original {@code value} - * For instance, if this method is called with an {@code io.r2dbc.spi.Blob} - * type {@code value}, it will convert it into an {@code java.sql.Blob} - * type value that stores the same content as the R2DBC {@code Blob}. - *

- * If no conversion is necessary, this method returns the original - * {@code value}. If the conversion requires a database call, this - * method returns a {@code Publisher} that emits the converted value. If - * the conversion requires resource allocation, a {@code Publisher} that - * deallocates resources is added to the {@code discardQueue}. - *

- * - * @param value Bind value to convert. May be null. - * @return Value to set as a bind on the JDBC statement. May be null. - * @throws IllegalArgumentException If the JDBC driver can not convert a - * bind value into a SQL value. - */ - private Object convertBind(Object value) { - if (value == null || value == NULL_BIND) { - return null; - } - else if (value instanceof Parameter) { - return convertBind(((Parameter) value).getValue()); - } - else if (value instanceof io.r2dbc.spi.Blob) { - return convertBlobBind((io.r2dbc.spi.Blob) value); - } - else if (value instanceof io.r2dbc.spi.Clob) { - return convertClobBind((io.r2dbc.spi.Clob) value); - } - else if (value instanceof ByteBuffer) { - return convertByteBufferBind((ByteBuffer) value); - } - else { - return value; - } - } - - /** - * Converts an R2DBC Blob to a JDBC Blob. The returned {@code Publisher} - * asynchronously writes the {@code r2dbcBlob's} content to a JDBC Blob and - * then emits the JDBC Blob after all content has been written. The JDBC - * Blob allocates a temporary database BLOB that is freed by a {@code - * Publisher} added to the {@code discardQueue}. - * @param r2dbcBlob An R2DBC Blob. Not null. Retained. - * @return A JDBC Blob. Not null. - */ - private Publisher convertBlobBind( - io.r2dbc.spi.Blob r2dbcBlob) { - return Mono.usingWhen( - adapter.getLock().get(jdbcConnection::createBlob), - jdbcBlob -> - Mono.from(adapter.publishBlobWrite(r2dbcBlob.stream(), jdbcBlob)) - .thenReturn(jdbcBlob), - jdbcBlob -> { - addDeallocation(adapter.publishBlobFree(jdbcBlob)); - return r2dbcBlob.discard(); - }); + InOutParameter(Object value, Type type) { + this.value = value; + this.type = type; } - /** - *

- * Converts an R2DBC Clob to a JDBC Clob. The returned {@code Publisher} - * asynchronously writes the {@code r2dbcClob} content to a JDBC Clob and - * then emits the JDBC Clob after all content has been written. The JDBC - * Clob allocates a temporary database Clob that is freed by a - * {@code Publisher} added to the {@code discardQueue}. - *

- * This method allocates an {@code NClob} in order to have to JDBC - * encode the data with a unicode character set. - *

- * @param r2dbcClob An R2DBC Clob. Not null. Retained. - * @return A JDBC Clob. Not null. - */ - private Publisher convertClobBind( - io.r2dbc.spi.Clob r2dbcClob) { - return Mono.usingWhen( - adapter.getLock().get(jdbcConnection::createNClob), - jdbcClob -> - Mono.from(adapter.publishClobWrite(r2dbcClob.stream(), jdbcClob)) - .thenReturn(jdbcClob), - jdbcClob -> { - addDeallocation(adapter.publishClobFree(jdbcClob)); - return r2dbcClob.discard(); - }); + @Override + public Type getType() { + return type; } - /** - * Converts a ByteBuffer to a byte array. The {@code byteBuffer} contents, - * delimited by it's position and limit, are copied into the returned byte - * array. No state of the {@code byteBuffer} is mutated, including it's - * position, limit, or mark. - * @param byteBuffer A ByteBuffer. Not null. Not retained. - * @return A byte array storing the {@code byteBuffer's} content. Not null. - */ - private byte[] convertByteBufferBind(ByteBuffer byteBuffer) { - ByteBuffer slice = byteBuffer.slice(); // Don't mutate position/limit/mark - byte[] byteArray = new byte[slice.remaining()]; - slice.get(byteArray); - return byteArray; + @Override + public Object getValue() { + return value; } - } /** - * A statement that is executed to return out-parameters with JDBC. This - * subclass of {@link JdbcStatement} overrides the base class behavior to - * register out-parameters with a {@link CallableStatement}, and to return - * a {@link Result} of out-parameters. + * Connect to the database configured by {@link DatabaseConfig}, with a + * the connection configured to use a given {@code executor} for async + * callbacks. + * @param executor Executor for async callbacks + * @return Connection that uses the {@code executor} */ - private class JdbcCall extends JdbcStatement { - - /** - * The indexes of out-parameter binds in the {@link #preparedStatement}. - * The array is sorted such that {@code outBindIndexes[0]} is the index - * of the first out-parameter, and {@code outBindIndexes[0]} is the index - * of the second out-parameter, and so on. - */ - private final int[] outBindIndexes; - - /** - * Metadata for out-parameter binds in the {@link #preparedStatement}. - */ - private final OutParametersMetadataImpl metadata; - - /** - * Constructs a new {@code JdbcCall} that executes a - * {@code callableStatement} with the given {@code bindValues} and - * {@code parameterNames}. - */ - private JdbcCall( - CallableStatement callableStatement, - Object[] bindValues, List parameterNames) { - - super(callableStatement, bindValues); - - outBindIndexes = IntStream.range(0, bindValues.length) - .filter(i -> bindValues[i] instanceof Parameter.Out) - .toArray(); - - OutParameterMetadata[] metadataArray = - new OutParameterMetadata[outBindIndexes.length]; - - for (int i = 0; i < metadataArray.length; i++) { - int bindIndex = outBindIndexes[i]; - - // Use the parameter name, or the index if the parameter is unnamed - String name = requireNonNullElse( - parameterNames.get(bindIndex), String.valueOf(i)); - metadataArray[i] = createParameterMetadata( - name, ((Parameter)bindValues[bindIndex]).getType()); - } - - this.metadata = createOutParametersMetadata(metadataArray); - } + private static Publisher connect(Executor executor) { + return ConnectionFactories.get( + ConnectionFactoryOptions.parse(format( + "r2dbc:oracle://%s:%d/%s", host(), port(), serviceName())) + .mutate() + .option( + ConnectionFactoryOptions.USER, user()) + .option( + ConnectionFactoryOptions.PASSWORD, password()) + .option( + OracleR2dbcOptions.EXECUTOR, executor) + .build()) + .create(); + } - @Override - protected Publisher bind() { - return Flux.concat(super.bind(), registerOutParameters()); - } + /** + * Verifies concurrent statement execution the given {@code connection} + * @param connection Connection to verify + */ + private void verifyConcurrentExecute(Connection connection) { - /** - * Invokes {@link CallableStatement#registerOutParameter(int, int)} to - * register each instance of {@link Parameter.Out} in the given - * {@code values} - * @return A publisher that completes when all out-parameter binds are - * registered. - */ - private Publisher registerOutParameters() { - return adapter.getLock().run(() -> { - CallableStatement callableStatement = - preparedStatement.unwrap(CallableStatement.class); - - for (int i : outBindIndexes) { - Type type = ((Parameter) binds[i]).getType(); - SQLType jdbcType = toJdbcType(type); - callableStatement.registerOutParameter(i + 1, jdbcType); - } - }); - } + // Create many statements and execute them in parallel. + Publisher[] publishers = new Publisher[8]; - @Override - protected Publisher executeJdbc() { - return Flux.concat( - super.executeJdbc(), - Mono.just(createCallResult( - createOutParameters(new JdbcOutParameters(), metadata, adapter), - adapter))); - } + for (int i = 0; i < publishers.length; i++) { + Flux flux = Flux.from(connection.createStatement( + "SELECT " + i + " FROM sys.dual") + .execute()) + .flatMap(result -> + result.map(row -> row.get(0, Integer.class))) + .cache(); - /** - * Out parameter values returned by the database. - */ - private final class JdbcOutParameters implements JdbcReadable { - - /** - * {@inheritDoc} - *

- * Returns the out-parameter value from the {@code CallableStatement} by - * mapping an R2DBC out-parameter index to a JDBC parameter index. The - * difference between the two is that R2DBC indexes are relative only - * to other out-parameters. So for index 0, R2DBC returns the first - * out-parameter, even if there are in-parameters at lower indexes in - * the parameterized SQL expression. Likewise, for index 1, R2DBC - * returns the second out-parameter, even if there are 1 or more - * in-parameters between the first and second out-parameter. - *

- */ - @Override - public T getObject(int index, Class type) { - // TODO: Throw IllegalArgumentException or IndexOutOfBoundsException - // based on the error code of any SQLException thrown - return fromJdbc(() -> - preparedStatement.unwrap(CallableStatement.class) - .getObject(outBindIndexes[index] + 1, type)); - } + flux.subscribe(); + publishers[i] = flux; } + awaitMany( + IntStream.range(0, publishers.length) + .boxed() + .collect(Collectors.toList()), + Flux.concat(publishers)); } /** - * A statement that executes with a batch of bind values. This subclass of - * {@link JdbcStatement} overrides the base class to bind a batch of - * values, and to execute the JDBC statement using - * {@link ReactiveJdbcAdapter#publishBatchUpdate(PreparedStatement)}. + * Verifies concurrent row fetching with the given {@code connection} + * @param connection Connection to verify */ - private class JdbcBatch extends JdbcStatement { - - /** Batch of bind values. */ - private final Queue batch; + private void verifyConcurrentFetch(Connection connection) { + try { + awaitExecution(connection.createStatement( + "CREATE TABLE testConcurrentFetch (value NUMBER)")); - /** Number of batched bind values */ - private final int batchSize; + // Create many statements and execute them in parallel. + Publisher[] publishers = new Publisher[8]; - private JdbcBatch( - PreparedStatement preparedStatement, Queue batch) { - super(preparedStatement, null); - this.batch = batch; - this.batchSize = batch.size(); - } + for (int i = 0; i < publishers.length; i++) { - /** - * {@code inheritDoc} - *

- * Binds the first set of values in {@link #binds}, then copies each - * remaining set of value into {@link #binds} and binds those as well. Calls - * {@link PreparedStatement#addBatch()} before binding each set of values - * after the first. - *

- */ - @Override - protected Publisher bind() { - @SuppressWarnings("unchecked") - Publisher[] bindPublishers = new Publisher[batchSize]; - for (int i = 0; i < batchSize; i++) { - bindPublishers[i] = Flux.concat( - bind(batch.remove()), - adapter.getLock().run(preparedStatement::addBatch)); - } - return Flux.concat(bindPublishers); - } + Statement statement = connection.createStatement( + "INSERT INTO testConcurrentFetch VALUES (?)"); - /** - * {@inheritDoc} - *

- * The returned {@code Publisher} emits 1 {@code Result} having an - * {@link io.r2dbc.spi.Result.UpdateCount} segment for each set of bind - * values in the {@link #batch}. - *

- */ - @Override - protected Publisher executeJdbc() { - AtomicInteger index = new AtomicInteger(0); - - return Flux.from(adapter.publishBatchUpdate(preparedStatement)) - .collect( - () -> new long[batchSize], - (updateCounts, updateCount) -> - updateCounts[index.getAndIncrement()] = updateCount) - .map(OracleResultImpl::createBatchUpdateResult) - .onErrorResume( - error -> - error instanceof R2dbcException - && error.getCause() instanceof BatchUpdateException, - error -> - Mono.just(createBatchUpdateErrorResult( - (BatchUpdateException) error.getCause()))); - } - } - - /** - * A JDBC batch execution where one or more binds are missing in the final - * set of bind values. - */ - private final class JdbcBatchInvalidBinds extends JdbcBatch { + // Each publisher batch inserts a range of 10 values + int start = i * 10; + statement.bind(0, start); + IntStream.range(start + 1, start + 10) + .forEach(value -> { + statement.add().bind(0, value); + }); - /** Exception thrown when one or more bind values are missing */ - private final IllegalStateException missingBinds; + Mono mono = Flux.from(statement.execute()) + .flatMap(Result::getRowsUpdated) + .collect(Collectors.summingLong(Long::longValue)) + .cache(); - private JdbcBatchInvalidBinds( - PreparedStatement preparedStatement, Queue batch, - IllegalStateException missingBinds) { - super(preparedStatement, batch); - this.missingBinds = missingBinds; - } + // Execute in parallel, and retain the result for verification later + mono.subscribe(); + publishers[i] = mono; + } - /** - * {@inheritDoc} - *

- * Allows the batch to execute with all previously added binds, and then - * emits an error result for the missing binds. - *

- */ - @Override - protected Publisher executeJdbc() { - return Flux.from(super.executeJdbc()) - .concatWithValues(createErrorResult( - newNonTransientException( - "One or more binds not set after calling add()", sql, - missingBinds))); - } - } + // Expect each publisher to emit an update count of 100 + awaitMany( + Stream.generate(() -> 10L) + .limit(publishers.length) + .collect(Collectors.toList()), + Flux.merge(publishers)); + + // Create publishers that fetch rows in parallel + Publisher>[] fetchPublishers = + new Publisher[publishers.length]; + + for (int i = 0; i < fetchPublishers.length; i++) { + Mono> mono = Flux.from(connection.createStatement( + "SELECT value FROM testConcurrentFetch ORDER BY value") + .execute()) + .flatMap(result -> + result.map(row -> row.get(0, Integer.class))) + .sort() + .collect(Collectors.toList()) + .cache(); + + // Execute in parallel, and retain the result for verification later + mono.subscribe(); + fetchPublishers[i] = mono; + } - /** - * A statement that returns values generated by a DML command, such as an - * column declared with an auto-generated value: - * {@code id NUMBER GENERATED ALWAYS AS IDENTITY} - */ - private final class JdbcReturningGenerated extends JdbcStatement { + // Expect each fetch publisher to get the same result + List expected = IntStream.range(0, publishers.length * 10) + .boxed() + .collect(Collectors.toList()); - private JdbcReturningGenerated( - PreparedStatement preparedStatement, Object[] binds) { - super(preparedStatement, binds); + for (Publisher> publisher : fetchPublishers) + awaitOne(expected, publisher); } - - /** - * {@inheritDoc} - *

- * Overrides the base implementation to include - * {@link PreparedStatement#getGeneratedKeys()} with the first result, if - * the generated keys {@code ResultSet} is not empty. - *

- * Oracle JDBC throws a {@code SQLException} when invoking - * {@code getMetadata()} on an empty generated keys {@code ResultSet}, so - * Oracle R2DBC should not even attempt to map that into a {@code Result} of - * {@code Row} segments. - *

- * If the generated keys {@code ResultSet} is empty, then this method - * behaves as if {@link Statement#returnGeneratedValues(String...)} had - * never been called at all; It will return whatever results are available - * from executing the statement, even if there are no generated values to - * return. - *

- * The generated keys {@code ResultSet} will be empty if the - * SQL was not an UPDATE or INSERT, because Oracle Database does not - * support returning generated values for any other type of statement. - *

- */ - @Override - protected Publisher executeJdbc() { - return Mono.from(adapter.publishSQLExecution(preparedStatement)) - .flatMapMany(isResultSet -> { - if (isResultSet) { - return super.getResults(true); - } - else { - return adapter.getLock().flatMap(() -> { - ResultSet generatedKeys = preparedStatement.getGeneratedKeys(); - - if (generatedKeys.isBeforeFirst()) { - return Mono.just(createGeneratedValuesResult( - preparedStatement.getLargeUpdateCount(), generatedKeys, - adapter)) - .concatWith(super.getResults( - preparedStatement.getMoreResults(KEEP_CURRENT_RESULT))); - } - else { - return super.getResults(false); - } - }); - } - }); + finally { + tryAwaitExecution(connection.createStatement( + "DROP TABLE testConcurrentFetch")); } - } -} \ No newline at end of file +} From b6e0d0b46d89c7a653dfbfaa2c9a2fd3147f1d25 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 14 May 2022 17:54:01 -0700 Subject: [PATCH 13/26] Revert to int update counts --- .../oracle/r2dbc/impl/OracleStatementImplTest.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index 946b5ca..93e5c6b 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -53,10 +53,8 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; import java.util.stream.IntStream; -import java.util.stream.LongStream; import java.util.stream.Stream; import static java.lang.String.format; @@ -828,7 +826,7 @@ public void testAdd() { // Expect the statement to execute with previously added binds, and // then emit an error if binds are missing in the final set of binds. - List> signals = + List> signals = awaitOne(Flux.from(connection.createStatement( "INSERT INTO testAdd VALUES (:x, :y)") .bind("x", 0).bind("y", 1).add() @@ -1861,7 +1859,7 @@ public void testNoOutImplicitResult() { .collectList())); // Expect Implicit Results to have no update counts - AtomicLong count = new AtomicLong(-9); + AtomicInteger count = new AtomicInteger(-9); awaitMany(asList(-9L, -10L), Flux.from(connection.createStatement("BEGIN countDown; END;") .execute()) @@ -1978,7 +1976,7 @@ public void testOutAndImplicitResult() { .collectList())); // Expect Implicit Results to have no update counts - AtomicLong count = new AtomicLong(-8); + AtomicInteger count = new AtomicInteger(-8); awaitMany(asList(-8L, -9L, -10L), Flux.from(connection.createStatement("BEGIN countDown(?); END;") .bind(0, Parameters.out(R2dbcType.VARCHAR)) @@ -2332,7 +2330,7 @@ private void verifyConcurrentFetch(Connection connection) { "CREATE TABLE testConcurrentFetch (value NUMBER)")); // Create many statements and execute them in parallel. - Publisher[] publishers = new Publisher[8]; + Publisher[] publishers = new Publisher[8]; for (int i = 0; i < publishers.length; i++) { @@ -2347,9 +2345,9 @@ private void verifyConcurrentFetch(Connection connection) { statement.add().bind(0, value); }); - Mono mono = Flux.from(statement.execute()) + Mono mono = Flux.from(statement.execute()) .flatMap(Result::getRowsUpdated) - .collect(Collectors.summingLong(Long::longValue)) + .collect(Collectors.summingInt(Integer::intValue)) .cache(); // Execute in parallel, and retain the result for verification later From 003f4ffac095297bf547a377ae4f03485ab036a3 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sun, 15 May 2022 12:18:37 -0700 Subject: [PATCH 14/26] Configure unique container names and clean them up --- .github/workflows/test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index d4500f1..ff92223 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -74,7 +74,7 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # database has started. # The database port number, 1521, is mapped to the host system. The Oracle # R2DBC test suite is configured to connect with this port. -docker run --name test_db --detach --rm -p $2:1521 -v $startUp:$startUpMount oracle/database:$1-xe +docker run --name test_db_$1 --detach --rm -p $2:1521 -v $startUp:$startUpMount oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "ready" in the startup scripts directory. When that file exists, @@ -100,3 +100,6 @@ echo "PASSWORD=test" >> src/test/resources/$1.properties echo "CONNECT_TIMEOUT=30" >> src/test/resources/$1.properties echo "SQL_TIMEOUT=30" >> src/test/resources/$1.properties mvn -Doracle.r2dbc.config=$1.properties clean compile test + +# Stop the database container to free up resources +docker stop test_db_$1 \ No newline at end of file From 5de1c2ffb48daa50c9bd2360c3d00df12825ae52 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sun, 15 May 2022 12:55:33 -0700 Subject: [PATCH 15/26] Fix docker logs command and add badge --- .github/workflows/test.sh | 7 ++++--- README.md | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index ff92223..ab50242 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -74,7 +74,8 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # database has started. # The database port number, 1521, is mapped to the host system. The Oracle # R2DBC test suite is configured to connect with this port. -docker run --name test_db_$1 --detach --rm -p $2:1521 -v $startUp:$startUpMount oracle/database:$1-xe +containerName=test_db_$1 +docker run --name $containerName --detach --rm -p $2:1521 -v $startUp:$startUpMount oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "ready" in the startup scripts directory. When that file exists, @@ -82,7 +83,7 @@ docker run --name test_db_$1 --detach --rm -p $2:1521 -v $startUp:$startUpMount echo "Waiting for database to start..." until [ -f $startUp/$readyFile ] do - docker logs --since 1s test_db + docker logs --since 1s $containerName sleep 1 done @@ -102,4 +103,4 @@ echo "SQL_TIMEOUT=30" >> src/test/resources/$1.properties mvn -Doracle.r2dbc.config=$1.properties clean compile test # Stop the database container to free up resources -docker stop test_db_$1 \ No newline at end of file +docker stop $containerName \ No newline at end of file diff --git a/README.md b/README.md index f886a06..f364883 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ The Oracle R2DBC Driver is a Java library that supports reactive programming wit Oracle R2DBC implements the R2DBC Service Provider Interface (SPI) as specified by the Reactive Relational Database Connectivity (R2DBC) project. The R2DBC SPI exposes Reactive Streams as an abstraction for remote database operations. Reactive Streams is a well defined standard for asynchronous, non-blocking, and back-pressured communication. This standard allows an R2DBC driver to interoperate with other reactive libraries and frameworks, such as Spring, Project Reactor, RxJava, and Akka Streams. +![](https://github.com/oracle/oracle-r2dbc/actions/workflows/build-and-test.yml/badge.svg) ### Learn More About R2DBC: [R2DBC Project Home Page](https://r2dbc.io) From 578a0eea0df9027e9becaf64250f943f92288855 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 21 May 2022 10:18:29 -0700 Subject: [PATCH 16/26] Make startup script directory writable --- .github/workflows/test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index ab50242..4af9b91 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -52,6 +52,7 @@ startUp=$PWD/$1/startup mkdir -p $startUp cp $PWD/startup/* $startUp +chmod -r u+rw $startUp # Create the 99_ready.sh script. It will touch a file in the mounted startup # directory. @@ -103,4 +104,4 @@ echo "SQL_TIMEOUT=30" >> src/test/resources/$1.properties mvn -Doracle.r2dbc.config=$1.properties clean compile test # Stop the database container to free up resources -docker stop $containerName \ No newline at end of file +docker stop $containerName From 943c386ae90094d983c70c14c6a0abcc65cb7102 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 21 May 2022 10:52:31 -0700 Subject: [PATCH 17/26] Correct -R option --- .github/workflows/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 4af9b91..c90cc66 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -52,7 +52,7 @@ startUp=$PWD/$1/startup mkdir -p $startUp cp $PWD/startup/* $startUp -chmod -r u+rw $startUp +chmod -R u+rw $startUp # Create the 99_ready.sh script. It will touch a file in the mounted startup # directory. From 21f9126d12d0b3cf5fe07c52bf6c5c1fa3d7bc84 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 21 May 2022 11:54:46 -0700 Subject: [PATCH 18/26] Write ready file to $HOME --- .github/workflows/.test.sh.swo | Bin 0 -> 16384 bytes .github/workflows/test.sh | 10 ++++------ 2 files changed, 4 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/.test.sh.swo diff --git a/.github/workflows/.test.sh.swo b/.github/workflows/.test.sh.swo new file mode 100644 index 0000000000000000000000000000000000000000..5518aa2732810dd2061e3bf42b0625c5b89504ce GIT binary patch literal 16384 zcmeHOO>87b749sAzYst{2?rGEW!4hAFw^aEfK4{>3a>p$hHPxdWABepw5sW@ndxkI zS8rFh$3_CgfkOxq2Sg4C!G%LEIRR15NCZVfpyYtygv5=X8{vxhUR8C^tg}j3BqW4- zv~TU1se1M5d%yLnJzl*$e1;zJ`yO7O@VvLWFN|Nh`ex^Q_ub{CaX1q)NyAjkRK^2t z|H3#krk9HF_9N`4U%!=MP^Nh>jmudz?iDgufht6p$X>A)jYII#r7Qy94hZI|xG_o8 zTnDAprLSl2wovrj-$Ac|UIk4+p8~z{F}w%; z40IFJ1DyhSpx@l+aLA3S3oaLA#&< zs0aEq$OHZ5Zmb3RA?UlH3FtED0nq)RU%ub-4nb!@e?ZgY7ohKec0oR<2f7>d8X6`U z=rZUY&~JesWBBvBaf0Rkd#_MsrHC_GY>QN`^Ai!*qw)2XzIU4T*0wHg5`RRxic3kc zCJdj%)2fgWm5SeGGAwD1M=z+H;rm<_rH{G$Gf9ys#aL)*KBTNl$Fk^B{|jsVF0)ES zo?A902G0~PbBrf7Qs2zpF^_P7Rl{Z`H0e?lrHpuqu#f`6k+33GS{8O@q{17rVBze} zAYPJCQDs&{9Tst3lFCAf52O>;Fs^cn;sQHT#jz1lRT-~F!?I?_>)DkLctL$LA;++s zi4x5A%sK!o6;u45VSFS@5zesJMkH83Fs|Yx+N^~f@^CziLcK#;=~M5>3nQ{S=^0i! zu!w9qRD)5RO%plHAUU=Y*s+s1kvj13s0)RO%7ANG=K6f#o{sTDmAZ#x+yuv*AC7_W zf|59t8FVJ5g_Nnxj5Z_Y#9P6?s#Ga(k7y$FELNFMJ3z4@8O0^uYMQGmiKvh_D?ka3 zH9U$$7|NWNf%c!Tpzo=|9@vl!7R-KdlowKK8dz?0=IY?Ag|bUy8476GYB9v%u_Q1v z#wTbdvgmkeG0W0IM`}VAJ*wgJ=?PU(rsmycs{lK;T>JRYtq5U9xiT)mxWXp(+1|~E zd7%#D2&TAYEYIkG1cpsLPlhu^ol;dHR6C&+V`m5XGi+JG3QRaUegZn{(1qr^;L%5~ zn=km;q-Mmr`ORCNzaFT(tkr2@)`2r*?4&Ve_LfadhYPc-EwOia83Q+7Vg(h%dti<8 z_|$rV^}Q`b4BTSCFB($zlwwJU@$~cF7jXS|9VQQHw*~>FyQ;=IDqBYTD!P!M3 z(lcYIu~rEq1CN;$Ds3h#xB~=HvgT{%3pg8y>kx=lIYJ?jIDPnpz*j9s@D{*}bp=5+RT)e4E^c6jI-wI| zEsz+T*xMU3`iv&n8bW`LxP)wE6NT{$c_hOMRwA-v;wa5JSqlEzv8oqZC>mR<{Rinw zki{7wYD5eR$BA_1BiWy(M8XI(Lfi}+WoJMNVwl(ophaVntdl3rQ;(c=ki(tUF*hTa zAPWnLjN4fnme>*9_xT6+|nr z@hF}cYeHTCHxn@(s;Rhv?PW*`B~m`}F_#lgaI3U;q;YH^krU((u(H)e?ZZd}E(Rkl z!6zirZR5zikJ@!?ps;Qc(fGLDAhyu}#0H*=0uCt?P7ab&c$LLBD_hB!jVuDoa&=?U z!=ge0e^!x}u&~JROxd9jZ6{3-GrN&C>p`zZ#q@Y8o>#>M|8e-9`I(C{oL#76V^d3+ zyⅇ6z8eaf5iX1zuGw|p9`qBmu(Uk$iuu{C)z#)UYcRVgKCV))F?8dwL|a*U21(t zz34JCc5iA%L>1%B2&P=hafxEX6?ngMhTYmlC5rd12mG>|J-Lbn6kQZ8M4_@BC=`4Mch;Y5tCNn9BHgMKf#hR4r_E} zxc}77!9HEt*xTFK-X9J|w6jN>JKI~s{o&3w-aSDZ+t=vn;r3RSB-|CA;S`RDisB|u zV(V?l*t#s@F#=8GLfewitia^~z!LCuHbyiYb?D;8XgKP!KnEF0K(0(o0cNZml{V@+ zF-Fcz#GF?(<(;YFIh+?L-Lh$yw3?K2gma^H!mF+x=kvKQaxP+hRZIg{B|<7Dr;}vKHh3_fHfCy$O~77JMWcxR zH(>Coh5&`1(wwd6%)#!Zv(�Pp-_B)&P~CS2xwU6GI18p0K8F-4C;~VbrP0SVbk0 zo||WM%Fr&1{vbnUEi}^dt}|bh7Y+oAz$TPb#M4Y0CfK%Sg`rii)KyTx0|2pVLps#_x z3fcob1=<8%1pNc&{g*-4K)ayFK!3z}|0kg5LFYiHKyToz|03vF&;y|RL4R`Vzs@qW zuNHw8ffj)lffj)lffj)lffj-HM!>Zlbg9s;Y+6HnJA&GK#GmpZqCN^Q9Y1kOo}g#W zjTf}ARaJyidtQ2&QE$uD_vY_WQRVY_czvbsqiKN?Wf`LrVtXHa-r>$GcT`;Kc=YV0 z>-)oJ1~_A`KfH=Q@deUFX#RFJXx0gCVdiGy47xqo-2V?}?{17nS9bQc%$?AkEPF5- z?A`IC-JQMt^_8_dS?8&p(f)d(LXq&@(VfiP+SuQ~d3msYB=cz8zmut(xAZcW`mgqU z>W)iIvtosrzqh_{)wA*>`UnRXuLJU@N4p!F16W(ruxqTXzGSf-vPItwt%0Sc78|JR zL-L-ax(a-`$&}2+@nVwM2xeU!G?s1e1Wbz@W;#am;F$17+!^{IJlb~nz--fA zEbvKxxy@q^^4v8<3(sEOqtZ7}l zZu>1)4$G~7hSXe!vKD$h2K)qg_|#rc;+G;dcQk9~`Ht3HM4BBO $startUp/99_ready.sh +readyFile='$HOME/oracle-r2dbc-ready' +echo "touch -f $readyFile" > $startUp/99_ready.sh # The oracle/docker-images repo is cloned. This repo provides Dockerfiles along # with a handy script to build images of Oracle Database. For now, this script @@ -76,13 +74,13 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # The database port number, 1521, is mapped to the host system. The Oracle # R2DBC test suite is configured to connect with this port. containerName=test_db_$1 -docker run --name $containerName --detach --rm -p $2:1521 -v $startUp:$startUpMount oracle/database:$1-xe +docker run --name $containerName --detach --rm -p $2:1521 -v $startUp:/opt/oracle/scripts/startup oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "ready" in the startup scripts directory. When that file exists, # it means the database is ready for testing. echo "Waiting for database to start..." -until [ -f $startUp/$readyFile ] +until docker exec -it $containerName sh -c "test -f $readyFile" do docker logs --since 1s $containerName sleep 1 From 2a8287bc30e272f3aee44b891737a42a24eb0e42 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 21 May 2022 11:55:50 -0700 Subject: [PATCH 19/26] Delete .swo --- .github/workflows/.test.sh.swo | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .github/workflows/.test.sh.swo diff --git a/.github/workflows/.test.sh.swo b/.github/workflows/.test.sh.swo deleted file mode 100644 index 5518aa2732810dd2061e3bf42b0625c5b89504ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeHOO>87b749sAzYst{2?rGEW!4hAFw^aEfK4{>3a>p$hHPxdWABepw5sW@ndxkI zS8rFh$3_CgfkOxq2Sg4C!G%LEIRR15NCZVfpyYtygv5=X8{vxhUR8C^tg}j3BqW4- zv~TU1se1M5d%yLnJzl*$e1;zJ`yO7O@VvLWFN|Nh`ex^Q_ub{CaX1q)NyAjkRK^2t z|H3#krk9HF_9N`4U%!=MP^Nh>jmudz?iDgufht6p$X>A)jYII#r7Qy94hZI|xG_o8 zTnDAprLSl2wovrj-$Ac|UIk4+p8~z{F}w%; z40IFJ1DyhSpx@l+aLA3S3oaLA#&< zs0aEq$OHZ5Zmb3RA?UlH3FtED0nq)RU%ub-4nb!@e?ZgY7ohKec0oR<2f7>d8X6`U z=rZUY&~JesWBBvBaf0Rkd#_MsrHC_GY>QN`^Ai!*qw)2XzIU4T*0wHg5`RRxic3kc zCJdj%)2fgWm5SeGGAwD1M=z+H;rm<_rH{G$Gf9ys#aL)*KBTNl$Fk^B{|jsVF0)ES zo?A902G0~PbBrf7Qs2zpF^_P7Rl{Z`H0e?lrHpuqu#f`6k+33GS{8O@q{17rVBze} zAYPJCQDs&{9Tst3lFCAf52O>;Fs^cn;sQHT#jz1lRT-~F!?I?_>)DkLctL$LA;++s zi4x5A%sK!o6;u45VSFS@5zesJMkH83Fs|Yx+N^~f@^CziLcK#;=~M5>3nQ{S=^0i! zu!w9qRD)5RO%plHAUU=Y*s+s1kvj13s0)RO%7ANG=K6f#o{sTDmAZ#x+yuv*AC7_W zf|59t8FVJ5g_Nnxj5Z_Y#9P6?s#Ga(k7y$FELNFMJ3z4@8O0^uYMQGmiKvh_D?ka3 zH9U$$7|NWNf%c!Tpzo=|9@vl!7R-KdlowKK8dz?0=IY?Ag|bUy8476GYB9v%u_Q1v z#wTbdvgmkeG0W0IM`}VAJ*wgJ=?PU(rsmycs{lK;T>JRYtq5U9xiT)mxWXp(+1|~E zd7%#D2&TAYEYIkG1cpsLPlhu^ol;dHR6C&+V`m5XGi+JG3QRaUegZn{(1qr^;L%5~ zn=km;q-Mmr`ORCNzaFT(tkr2@)`2r*?4&Ve_LfadhYPc-EwOia83Q+7Vg(h%dti<8 z_|$rV^}Q`b4BTSCFB($zlwwJU@$~cF7jXS|9VQQHw*~>FyQ;=IDqBYTD!P!M3 z(lcYIu~rEq1CN;$Ds3h#xB~=HvgT{%3pg8y>kx=lIYJ?jIDPnpz*j9s@D{*}bp=5+RT)e4E^c6jI-wI| zEsz+T*xMU3`iv&n8bW`LxP)wE6NT{$c_hOMRwA-v;wa5JSqlEzv8oqZC>mR<{Rinw zki{7wYD5eR$BA_1BiWy(M8XI(Lfi}+WoJMNVwl(ophaVntdl3rQ;(c=ki(tUF*hTa zAPWnLjN4fnme>*9_xT6+|nr z@hF}cYeHTCHxn@(s;Rhv?PW*`B~m`}F_#lgaI3U;q;YH^krU((u(H)e?ZZd}E(Rkl z!6zirZR5zikJ@!?ps;Qc(fGLDAhyu}#0H*=0uCt?P7ab&c$LLBD_hB!jVuDoa&=?U z!=ge0e^!x}u&~JROxd9jZ6{3-GrN&C>p`zZ#q@Y8o>#>M|8e-9`I(C{oL#76V^d3+ zyⅇ6z8eaf5iX1zuGw|p9`qBmu(Uk$iuu{C)z#)UYcRVgKCV))F?8dwL|a*U21(t zz34JCc5iA%L>1%B2&P=hafxEX6?ngMhTYmlC5rd12mG>|J-Lbn6kQZ8M4_@BC=`4Mch;Y5tCNn9BHgMKf#hR4r_E} zxc}77!9HEt*xTFK-X9J|w6jN>JKI~s{o&3w-aSDZ+t=vn;r3RSB-|CA;S`RDisB|u zV(V?l*t#s@F#=8GLfewitia^~z!LCuHbyiYb?D;8XgKP!KnEF0K(0(o0cNZml{V@+ zF-Fcz#GF?(<(;YFIh+?L-Lh$yw3?K2gma^H!mF+x=kvKQaxP+hRZIg{B|<7Dr;}vKHh3_fHfCy$O~77JMWcxR zH(>Coh5&`1(wwd6%)#!Zv(�Pp-_B)&P~CS2xwU6GI18p0K8F-4C;~VbrP0SVbk0 zo||WM%Fr&1{vbnUEi}^dt}|bh7Y+oAz$TPb#M4Y0CfK%Sg`rii)KyTx0|2pVLps#_x z3fcob1=<8%1pNc&{g*-4K)ayFK!3z}|0kg5LFYiHKyToz|03vF&;y|RL4R`Vzs@qW zuNHw8ffj)lffj)lffj)lffj-HM!>Zlbg9s;Y+6HnJA&GK#GmpZqCN^Q9Y1kOo}g#W zjTf}ARaJyidtQ2&QE$uD_vY_WQRVY_czvbsqiKN?Wf`LrVtXHa-r>$GcT`;Kc=YV0 z>-)oJ1~_A`KfH=Q@deUFX#RFJXx0gCVdiGy47xqo-2V?}?{17nS9bQc%$?AkEPF5- z?A`IC-JQMt^_8_dS?8&p(f)d(LXq&@(VfiP+SuQ~d3msYB=cz8zmut(xAZcW`mgqU z>W)iIvtosrzqh_{)wA*>`UnRXuLJU@N4p!F16W(ruxqTXzGSf-vPItwt%0Sc78|JR zL-L-ax(a-`$&}2+@nVwM2xeU!G?s1e1Wbz@W;#am;F$17+!^{IJlb~nz--fA zEbvKxxy@q^^4v8<3(sEOqtZ7}l zZu>1)4$G~7hSXe!vKD$h2K)qg_|#rc;+G;dcQk9~`Ht3HM4BBO Date: Sat, 21 May 2022 13:12:41 -0700 Subject: [PATCH 20/26] Remove -it from docker exec --- .github/workflows/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index d41d9bc..21fe2f8 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -80,7 +80,7 @@ docker run --name $containerName --detach --rm -p $2:1521 -v $startUp:/opt/oracl # a file named "ready" in the startup scripts directory. When that file exists, # it means the database is ready for testing. echo "Waiting for database to start..." -until docker exec -it $containerName sh -c "test -f $readyFile" +until docker exec $containerName sh -c "test -f $readyFile" do docker logs --since 1s $containerName sleep 1 From e8efdf987771bfcfe72e28920118e19aa8948f66 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Mon, 23 May 2022 10:09:15 -0700 Subject: [PATCH 21/26] Isolate clones of docker-images repo --- .github/workflows/test.sh | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 21fe2f8..53c18cc 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -29,32 +29,21 @@ # The database port number is configured by the second parameter. If multiple # databases are created by running this script in parallel, then a unique port # number should be provided for each database. -# -# This script makes no attempt to clean up. The docker container is left -# running, and the database retains the test user and any other modifications -# that the test suite may have performed. -# It is assumed that the Github Runner will clean up any state this script -# leaves behind. # The startup directory is mounted as a volume inside the docker container. # The container's entry point script will execute any .sh and .sql scripts # it finds under /opt/oracle/scripts/startup. The startup scripts are run -# after the database instance is active.A numeric prefix on the script name -# determines the order in which scripts are run. The final script, prefixed -# with "99_" will create a file named "ready" in the mounted volume, indicating -# that all scripts have completed and the database is ready for testing. - -# Create directory with the startup scripts. Naming the directory with the -# version number should isolate it from database container that is running -# concurrently, assuming multiple containers are not running the same database -# version. +# after the database instance is active. startUp=$PWD/$1/startup mkdir -p $startUp cp $PWD/startup/* $startUp -# Create the 99_ready.sh script. It will touch a file in the mounted startup -# directory. +# Create a 99_ready.sh script. The numeric prefix of the file name determines +# the order in which scripts are run. The final script, prefixed with "99_" +# will create a file named "oracle-r2dbc-ready" in the $HOME directory within +# the container. The existence of this file is waited for before any tests are +# run. readyFile='$HOME/oracle-r2dbc-ready' echo "touch -f $readyFile" > $startUp/99_ready.sh @@ -63,6 +52,8 @@ echo "touch -f $readyFile" > $startUp/99_ready.sh # is just going to build an Express Edition (XE) image, because this can be # done in an automated fashion. Other editions would require a script to accept # a license agreement. +# Parallel executions of this script clone the repo into isolated directories. +cd $PWD/$1 git clone https://github.com/oracle/docker-images.git cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ ./buildContainerImage.sh -v $1 -x @@ -91,7 +82,7 @@ done # database editions. The test user is created by the startup/01_createUser.sql # script cd $GITHUB_WORKSPACE -echo "Configuration for testing with Oracle Database $1" > src/test/resources/$1.properties +echo "# Configuration for testing with Oracle Database $1" > src/test/resources/$1.properties echo "DATABASE=xepdb1" >> src/test/resources/$1.properties echo "HOST=localhost" >> src/test/resources/$1.properties echo "PORT=$2" >> src/test/resources/$1.properties From 35fee42fd1fe6b42a68b307fa1b4d3f2d1f3597c Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 28 May 2022 11:31:45 -0700 Subject: [PATCH 22/26] Use any available port --- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/test.sh | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c4be872..5a55f46 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -58,7 +58,7 @@ jobs: steps: - uses: actions/checkout@v2 - name: Test with Oracle Database 18.4.0 XE - run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 18.4.0 50184 + run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 18.4.0 # Tests the Oracle R2DBC Driver with a 21.3 XE Oracle Database test-21-3-xe: needs: build @@ -66,4 +66,4 @@ jobs: steps: - uses: actions/checkout@v2 - name: Test with Oracle Database 21.3.0 XE - run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 21.3.0 50213 + run: cd $GITHUB_WORKSPACE/.github/workflows && bash test.sh 21.3.0 diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 53c18cc..d2fb31c 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -62,10 +62,10 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # The startup directory is mounted. It contains a createUser.sql script that # creates a test user. The docker container will run this script once the # database has started. -# The database port number, 1521, is mapped to the host system. The Oracle -# R2DBC test suite is configured to connect with this port. +# The -P option has the the database port number, 1521, mapped to an available +# port on the host system. containerName=test_db_$1 -docker run --name $containerName --detach --rm -p $2:1521 -v $startUp:/opt/oracle/scripts/startup oracle/database:$1-xe +docker run --name $containerName --detach --rm -P -v $startUp:/opt/oracle/scripts/startup oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "ready" in the startup scripts directory. When that file exists, @@ -77,6 +77,13 @@ do sleep 1 done +# Find out which port number on the host system has been mapped to the 1521 +# port of the container. 1521 is the Oracle Database port number. The Oracle +# R2DBC test suite is configured to connect to the mapped port on the host +# system. The docker port command outputs a string in the format of: +# ":", and the sed command extracts the port number with a regex. +dbPort=$(docker port $containerName 1521 | sed 's/^.*:\([0-9]*\)$/\1/') + # Create a configuration file and run the tests. The service name, "xepdb1", # is always created for the XE database. It would probably change for other # database editions. The test user is created by the startup/01_createUser.sql @@ -85,11 +92,11 @@ cd $GITHUB_WORKSPACE echo "# Configuration for testing with Oracle Database $1" > src/test/resources/$1.properties echo "DATABASE=xepdb1" >> src/test/resources/$1.properties echo "HOST=localhost" >> src/test/resources/$1.properties -echo "PORT=$2" >> src/test/resources/$1.properties +echo "PORT=$dbPort" >> src/test/resources/$1.properties echo "USER=test" >> src/test/resources/$1.properties echo "PASSWORD=test" >> src/test/resources/$1.properties -echo "CONNECT_TIMEOUT=30" >> src/test/resources/$1.properties -echo "SQL_TIMEOUT=30" >> src/test/resources/$1.properties +echo "CONNECT_TIMEOUT=60" >> src/test/resources/$1.properties +echo "SQL_TIMEOUT=60" >> src/test/resources/$1.properties mvn -Doracle.r2dbc.config=$1.properties clean compile test # Stop the database container to free up resources From c78c95dbc7f26361c4a3d8e8712ccefec4b3c5a9 Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 28 May 2022 12:08:45 -0700 Subject: [PATCH 23/26] Use -p with a dynamic range --- .github/workflows/test.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index d2fb31c..a252fc0 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -62,10 +62,10 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # The startup directory is mounted. It contains a createUser.sql script that # creates a test user. The docker container will run this script once the # database has started. -# The -P option has the the database port number, 1521, mapped to an available -# port on the host system. +# The -p option has the database port number, 1521, mapped to any available +# port in the range between 49152 and 65535 containerName=test_db_$1 -docker run --name $containerName --detach --rm -P -v $startUp:/opt/oracle/scripts/startup oracle/database:$1-xe +docker run --name $containerName --detach --rm -p 49152–65535:1521 -v $startUp:/opt/oracle/scripts/startup oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "ready" in the startup scripts directory. When that file exists, From d1f55c2a30845f6e1d254fedf1eadddbab2f46fa Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Sat, 28 May 2022 12:29:45 -0700 Subject: [PATCH 24/26] Use correct hyphen character --- .github/workflows/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index a252fc0..95c34cb 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -65,7 +65,7 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # The -p option has the database port number, 1521, mapped to any available # port in the range between 49152 and 65535 containerName=test_db_$1 -docker run --name $containerName --detach --rm -p 49152–65535:1521 -v $startUp:/opt/oracle/scripts/startup oracle/database:$1-xe +docker run --name $containerName --detach --rm -p 49152-65535:1521 -v $startUp:/opt/oracle/scripts/startup oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "ready" in the startup scripts directory. When that file exists, From 743c5a57e71a0a2b2b28cccf6844402542cebd6e Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Mon, 20 Jun 2022 15:52:29 -0700 Subject: [PATCH 25/26] Use fixed port number --- .github/workflows/test.sh | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 682f04e..2495518 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -62,11 +62,11 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # The startup directory is mounted. It contains a createUser.sql script that # creates a test user. The docker container will run this script once the # database has started. -# The -p option has the database port number, 1521, mapped to any available -# port in the range between 49152 and 65535 containerName=test_db_$1 -echo "Staring container: $containerName" -docker run --name $containerName --detach --rm -p 49152-65535:1521 -v $startUp:/opt/oracle/scripts/startup -e ORACLE_PDB=xepdb1 oracle/database:$1-xe +dbPort=$(echo "60$1" | tr -d '.') +echo "Starting container: $containerName" +echo "Host port: $dbPort" +docker run --name $containerName --detach --rm -p $dbPort:1521 -v $startUp:/opt/oracle/scripts/startup -e ORACLE_PDB=xepdb1 oracle/database:$1-xe # Wait for the database instance to start. The final startup script will create # a file named "ready" in the startup scripts directory. When that file exists, @@ -78,14 +78,6 @@ do sleep 1 done -# Find out which port number on the host system has been mapped to the 1521 -# port of the container. 1521 is the Oracle Database port number. The Oracle -# R2DBC test suite is configured to connect to the mapped port on the host -# system. The docker port command outputs a string in the format of: -# ":", and the sed command extracts the port number with a regex. -dbPort=$(docker port $containerName 1521 | sed 's/^.*:\([0-9]*\)$/\1/') -echo "Host port: $dbPort" - # Create a configuration file and run the tests. The service name, "xepdb1", # is always created for the XE database. It would probably change for other # database editions. The test user is created by the startup/01_createUser.sql From a012124f093e2deed9e5cc8885d8f039c312415c Mon Sep 17 00:00:00 2001 From: Michael-A-McMahon Date: Mon, 20 Jun 2022 16:03:19 -0700 Subject: [PATCH 26/26] 5 digit port numbers --- .github/workflows/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.sh b/.github/workflows/test.sh index 2495518..24a92e8 100755 --- a/.github/workflows/test.sh +++ b/.github/workflows/test.sh @@ -63,7 +63,7 @@ cd docker-images/OracleDatabase/SingleInstance/dockerfiles/ # creates a test user. The docker container will run this script once the # database has started. containerName=test_db_$1 -dbPort=$(echo "60$1" | tr -d '.') +dbPort=$(echo "5$1" | tr -d '.') echo "Starting container: $containerName" echo "Host port: $dbPort" docker run --name $containerName --detach --rm -p $dbPort:1521 -v $startUp:/opt/oracle/scripts/startup -e ORACLE_PDB=xepdb1 oracle/database:$1-xe