diff --git a/.github/scripts/ensure_no_leak.sh b/.github/scripts/ensure_no_leak.sh new file mode 100755 index 000000000..6c6ebeead --- /dev/null +++ b/.github/scripts/ensure_no_leak.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Copyright 2024 asyncer.io projects +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -e + +if [ "$#" -ne 1 ] || [ "${1##*.}" != "log" ]; then + echo "Please provide a single log file with a .log extension." + exit 1 +fi + +if grep -q 'LEAK' "$1" ; then + echo "LEAK FOUND: The log file $1 contains a memory leak." + exit 1 +fi + +echo "No Leak: The log file $1 does not contain any memory leaks." +exit 0 diff --git a/.github/scripts/ensure_prepared.sh b/.github/scripts/ensure_prepared.sh index 1e4df3995..73e5f2586 100755 --- a/.github/scripts/ensure_prepared.sh +++ b/.github/scripts/ensure_prepared.sh @@ -16,7 +16,7 @@ # set -e -TAG=$(grep scm.tag= release.properties | cut -d'=' -f2) +TAG=$(grep scm.tag= r2dbc-mysql/release.properties | cut -d'=' -f2) echo "checkout tag $TAG" git checkout "$TAG" -exit 0 \ No newline at end of file +exit 0 diff --git a/.github/scripts/release_rollback.sh b/.github/scripts/release_rollback.sh index dd505e92c..b67e429c8 100755 --- a/.github/scripts/release_rollback.sh +++ b/.github/scripts/release_rollback.sh @@ -16,9 +16,9 @@ # set -e -TAG=$(grep scm.tag= release.properties | cut -d'=' -f2) +TAG=$(grep scm.tag= r2dbc-mysql/release.properties | cut -d'=' -f2) git remote set-url origin git@github.com:asyncer-io/r2dbc-mysql.git git fetch git checkout "$1" ./mvnw -B --file pom.xml release:rollback -git push origin :"$TAG" \ No newline at end of file +git push origin :"$TAG" diff --git a/.github/scripts/upgrade_native_image_version.sh b/.github/scripts/upgrade_native_image_version.sh new file mode 100755 index 000000000..7c978d0b0 --- /dev/null +++ b/.github/scripts/upgrade_native_image_version.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# Copyright 2024 asyncer.io projects +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -e + +VERSION=$(grep 'project.dev.io.asyncer\\:r2dbc-mysql=' r2dbc-mysql/release.properties | cut -d'=' -f2) + +echo 'Set test-native-image version to' $VERSION +./mvnw -pl test-native-image versions:set -DnewVersion=$VERSION +git add test-native-image/pom.xml diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 2c3b26efa..75b75ccde 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -21,13 +21,13 @@ jobs: prepare: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@v4 - name: Set Up Java 8 - uses: actions/setup-java@v3 + + uses: actions/setup-java@v4 with: - distribution: 'temurin' # gh runner local caches lts temurins - java-version: '8' + distribution: "temurin" # gh runner local caches lts temurins + java-version: "8" - name: Setup Git Configs run: | @@ -47,19 +47,28 @@ jobs: key: ${{ runner.os }}-prepare-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-prepare- + - name: DryRun Release Prepare + run: | + ./mvnw -B -ntp -pl r2dbc-mysql release:prepare -DpreparationGoals=clean -DdryRun=true -DskipTests=true + + - name: Upgrade Native Image Version + run: ./.github/scripts/upgrade_native_image_version.sh + - name: Run release prepare command run: | - ./mvnw -B -ntp --file pom.xml release:prepare -DpreparationGoals=clean -DskipTests=true + ./mvnw -B -ntp -pl r2dbc-mysql release:prepare -DpreparationGoals=clean -Dresume=false -DskipTests=true ./mvnw -B -ntp clean - name: Ensure Prepared run: ./.github/scripts/ensure_prepared.sh - name: Upload workspace - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: prepare-workspace path: ${{ github.workspace }} + include-hidden-files: true + - name: Rollback Release working-directory: ./prepare-workspace/ if: ${{ failure() }} @@ -70,7 +79,7 @@ jobs: needs: prepare steps: - name: Download workspace - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: prepare-workspace path: ./prepare-workspace/ @@ -81,10 +90,10 @@ jobs: chmod 755 ./prepare-workspace/.github/scripts/release_rollback.sh - name: Set up Java 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' # gh runner local caches lts temurins - java-version: '8' + distribution: "temurin" # gh runner local caches lts temurins + java-version: "8" - name: Setup git configs run: | @@ -109,27 +118,29 @@ jobs: with: servers: | [{ - "id": "ossrh-staging", - "username": "${{ secrets.OSSRH_USERNAME }}", - "password": "${{ secrets.OSSRH_PASSWORD }}" + "id": "central", + "username": "${{ secrets.CENTRAL_USERNAME }}", + "password": "${{ secrets.CENTRAL_PASSWORD }}" }] - name: Create Local Deploy Directory run: mkdir -p ~/local-staging + - name: Prepare Internal Dependencies + working-directory: ./prepare-workspace/ + run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip + - name: Import GPG & Deploy Local Staging working-directory: ./prepare-workspace/ run: | - cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import - ./mvnw -B -ntp -am clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" + cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import + ./mvnw -B -ntp -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" - name: Deploy Local Staged Artifacts working-directory: ./prepare-workspace/ - run: ./mvnw -B -ntp --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true - + run: ./mvnw -B -ntp -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true -Dcheckstyle.skip - name: Rollback Release working-directory: ./prepare-workspace/ if: ${{ failure() }} - run: ./.github/scripts/release_rollback.sh - + run: ./.github/scripts/release_rollback.sh trunk diff --git a/.github/workflows/cd-snapshot.yml b/.github/workflows/cd-snapshot.yml index cafd99ded..8cd96d40c 100644 --- a/.github/workflows/cd-snapshot.yml +++ b/.github/workflows/cd-snapshot.yml @@ -46,13 +46,16 @@ jobs: with: servers: | [{ - "id": "ossrh-snapshots", - "username": "${{ secrets.OSSRH_USERNAME }}", - "password": "${{ secrets.OSSRH_PASSWORD }}" + "id": "central-portal-snapshots", + "username": "${{ secrets.CENTRAL_USERNAME }}", + "password": "${{ secrets.CENTRAL_PASSWORD }}" }] + - name: Prepare Internal Dependencies + run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip + - name: Deploy Local Staging - run: ./mvnw -B -ntp clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true + run: ./mvnw -B -ntp -pl r2dbc-mysql clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip=true - name: Deploy Local Staged Artifacts - run: ./mvnw -B --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DaltStagingDirectory=/home/runner/local-staging + run: ./mvnw -B -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DaltStagingDirectory=/home/runner/local-staging diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml new file mode 100644 index 000000000..cc2347469 --- /dev/null +++ b/.github/workflows/ci-codeql.yml @@ -0,0 +1,80 @@ +# Copyright 2024 asyncer.io proejcts +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "CodeQL" + +on: + push: + branches: [ "trunk", "0.9.x" ] + pull_request: + branches: [ "trunk", "0.9.x" ] + schedule: + - cron: '24 3 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: java-kotlin + build-mode: none # This mode only analyzes Java. Set this to 'autobuild' or 'manual' to analyze Kotlin too. + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/ci-graalvm-tests.yml b/.github/workflows/ci-graalvm-tests.yml new file mode 100644 index 000000000..54db266ff --- /dev/null +++ b/.github/workflows/ci-graalvm-tests.yml @@ -0,0 +1,49 @@ +# Copyright 2024 asyncer.io proejcts +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Native Image Build Test + +on: + pull_request: + branches: ["trunk", "0.9.x"] + +jobs: + graalvm-build-pr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: graalvm/setup-graalvm@v1 + with: + java-version: 21 + distribution: "graalvm" + native-image-job-reports: true + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Start Bundled MySQL + run: sudo service mysql start + + - name: Cache & Load Local Maven Repository + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-prepare-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-prepare- + + - name: Build and run native image + run: | + echo "JAVA_HOME=$JAVA_HOME" + echo "./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true" + ./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true + ./test-native-image/target/test-native-image diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 881d87903..3ac7fb159 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -2,14 +2,15 @@ name: Integration Tests on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: integration-tests-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - mysql-version: [ 5.5, 5.6, 5.7, 8.0, 8.1, 8.2 ] + mysql-version: + ['5.5', '5.6.45', '5.6', '5.7.28', '5.7', '8.0', '8.1', '8.2', '8.3', '8.4', '9.0', '9.1', '9.2'] name: Integration test with MySQL ${{ matrix.mysql-version }} steps: - uses: actions/checkout@v3 @@ -21,16 +22,17 @@ jobs: cache: maven - name: Shutdown the Default MySQL run: sudo service mysql stop - - name: Set up MySQL ${{ matrix.mysql-version }} - env: - MYSQL_DATABASE: r2dbc - MYSQL_ROOT_PASSWORD: r2dbc-password!@ - MYSQL_VERSION: ${{ matrix.mysql-version }} - run: docker-compose -f ${{ github.workspace }}/containers/mysql-compose.yml up -d - name: Integration test with MySQL ${{ matrix.mysql-version }} run: | + set -o pipefail ./mvnw -B verify -Dmaven.javadoc.skip=true \ -Dmaven.surefire.skip=true \ - -Dtest.mysql.password=r2dbc-password!@ \ - -Dtest.mysql.version=${{ matrix.mysql-version }} \ - -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN + -Dtest.db.type=mysql \ + -Dtest.db.version=${{ matrix.mysql-version }} \ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dio.netty.leakDetectionLevel=paranoid \ + -Dio.netty.leakDetection.targetRecords=32 \ + | tee test.log + set +o pipefail + - name: ensure no leaks + run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/.github/workflows/ci-mariadb-intergration-tests.yml b/.github/workflows/ci-mariadb-intergration-tests.yml index 78d577d97..fcf38e35c 100644 --- a/.github/workflows/ci-mariadb-intergration-tests.yml +++ b/.github/workflows/ci-mariadb-intergration-tests.yml @@ -2,14 +2,15 @@ name: Integration Tests for MariaDB on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: mariadb-integration-tests-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - mariadb-version: [ 10.6, 10.11 ] + mariadb-version: + ['10.0', '10.1', '10.2.15', '10.2', '10.3.7', '10.3', '10.5.1', '10.5', '10.6', '10.11'] name: Integration test with MariaDB ${{ matrix.mariadb-version }} steps: - uses: actions/checkout@v3 @@ -21,17 +22,17 @@ jobs: cache: maven - name: Shutdown the Default MySQL run: sudo service mysql stop - - name: Set up MariaDB ${{ matrix.mariadb-version }} - env: - MYSQL_DATABASE: r2dbc - MYSQL_ROOT_PASSWORD: r2dbc-password!@ - MARIADB_VERSION: ${{ matrix.mariadb-version }} - run: docker-compose -f ${{ github.workspace }}/containers/mariadb-compose.yml up -d - - name: Integration test with MySQL ${{ matrix.mysql-version }} + - name: Integration test with MariaDB ${{ matrix.mysql-version }} run: | + set -o pipefail ./mvnw -B verify -Dmaven.javadoc.skip=true \ -Dmaven.surefire.skip=true \ - -Dtest.mysql.password=r2dbc-password!@ \ - -Dtest.mysql.version=${{ matrix.mariadb-version }} \ -Dtest.db.type=mariadb \ - -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN + -Dtest.db.version=${{ matrix.mariadb-version }} \ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dio.netty.leakDetectionLevel=paranoid \ + -Dio.netty.leakDetection.targetRecords=32 \ + | tee test.log + set +o pipefail + - name: ensure no leaks + run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml index 694f30c1d..0247ca1d7 100644 --- a/.github/workflows/ci-unit-tests.yml +++ b/.github/workflows/ci-unit-tests.yml @@ -2,14 +2,14 @@ name: Unit tests on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: unit-tests-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - java-version: [ 8, 11, 17, 21 ] + java-version: [8, 11, 17, 21] name: linux-java-${{ matrix.java-version }} steps: - uses: actions/checkout@v3 @@ -20,4 +20,12 @@ jobs: java-version: ${{ matrix.java-version }} cache: maven - name: Unit test with Maven - run: ./mvnw -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ No newline at end of file + run: | + set -o pipefail + ./mvnw -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dio.netty.leakDetectionLevel=paranoid \ + -Dio.netty.leakDetection.targetRecords=32 \ + | tee test.log + set +o pipefail + - name: ensure no leaks + run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 110dad7d8..d58dfb70b 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -6,7 +6,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -14,5 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar \ No newline at end of file +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/README.md b/README.md index 97b52af43..85039510a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Reactive Relational Database Connectivity MySQL Implementation + ![Maven Central](https://img.shields.io/maven-central/v/io.asyncer/r2dbc-mysql?color=blue) ![LICENSE](https://img.shields.io/github/license/asyncer-io/r2dbc-mysql) @@ -7,19 +8,24 @@ This implementation is not intended to be used directly, but rather to be used as the backing implementation for a humane client library to delegate to. See [R2DBC Homepage](https://r2dbc.io). +See [R2DBC MySQL wiki](https://github.com/asyncer-io/r2dbc-mysql/wiki) for more information. + ## Spring-framework and R2DBC-SPI Compatibility + Refer to the table below to determine the appropriate version of r2dbc-mysql for your project. | spring-boot-starter-data-r2dbc | spring-data-r2dbc | r2dbc-spi | r2dbc-mysql(recommended) | |--------------------------------|-------------------|---------------|------------------------------| -| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.0.6 | +| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.4.0 | | 2.7.* | 1.5.* | 0.9.1.RELEASE | io.asyncer:r2dbc-mysql:0.9.7 | | 2.6.* and below | 1.4.* and below | 0.8.6.RELEASE | dev.miku:r2dbc-mysql:0.8.2 | ## Supported Features + This driver provides the following features: - [x] Unix domain socket. +- [x] Compression protocols, including zstd and zlib. - [x] Execution of simple or batch statements without bindings. - [x] Execution of prepared statements with bindings. - [x] Reactive LOB types (e.g. BLOB, CLOB) @@ -33,21 +39,20 @@ This driver provides the following features: - [x] Extensible, e.g. extend built-in `Codec`(s). - [x] MariaDB `RETURNING` clause. -## Maintainer - -This project is currently being maintained by [@jchrys](https://github.com/jchrys), since the previous owner has been inactive. We are committed to keeping this project up-to-date and improving it in collaboration with the community. - ## Version compatibility / Integration tests states + ![MySQL 5.5 status](https://img.shields.io/badge/MySQL%205.5-pass-blue) ![MySQL 5.6 status](https://img.shields.io/badge/MySQL%205.6-pass-blue) ![MySQL 5.7 status](https://img.shields.io/badge/MySQL%205.7-pass-blue) ![MySQL 8.0 status](https://img.shields.io/badge/MySQL%208.0-pass-blue) ![MySQL 8.1 status](https://img.shields.io/badge/MySQL%208.1-pass-blue) ![MySQL 8.2 status](https://img.shields.io/badge/MySQL%208.2-pass-blue) +![MySQL 8.3 status](https://img.shields.io/badge/MySQL%208.3-pass-blue) +![MySQL 8.4 status](https://img.shields.io/badge/MySQL%208.4-pass-blue) +![MySQL 9.0 status](https://img.shields.io/badge/MySQL%209.0-pass-blue) ![MariaDB 10.6 status](https://img.shields.io/badge/MariaDB%2010.6-pass-blue) ![MariaDB 10.11 status](https://img.shields.io/badge/MariaDB%2010.11-pass-blue) - In fact, it supports lower versions, in the theory, such as 4.1, 4.0, etc. However, Docker-certified images do not have these versions lower than 5.5.0, so tests are not integrated on these versions. @@ -59,7 +64,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so io.asyncer r2dbc-mysql - 1.0.6 + 1.4.0 ``` @@ -69,7 +74,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so ```groovy dependencies { - implementation 'io.asyncer:r2dbc-mysql:1.0.6' + implementation 'io.asyncer:r2dbc-mysql:1.4.0' } ``` @@ -78,7 +83,7 @@ dependencies { ```kotlin dependencies { // Maybe should to use `compile` instead of `implementation` on the lower version of Gradle. - implementation("io.asyncer:r2dbc-mysql:1.0.6") + implementation("io.asyncer:r2dbc-mysql:1.4.0") } ``` @@ -86,484 +91,32 @@ dependencies { Here is a quick teaser of how to use R2DBC MySQL in Java: -### URL Connection Factory Discovery - ```java // Notice: the query string must be URL encoded -ConnectionFactory connectionFactory = ConnectionFactories.get( - "r2dbcs:mysql://root:database-password-in-here@127.0.0.1:3306/r2dbc?" + - "zeroDate=use_round&" + - "sslMode=verify_identity&" + - "useServerPrepareStatement=true&" + - "tlsVersion=TLSv1.3%2CTLSv1.2%2CTLSv1.1&" + - "sslCa=%2Fpath%2Fto%2Fmysql%2Fca.pem&" + - "sslKey=%2Fpath%2Fto%2Fmysql%2Fclient-key.pem&" + - "sslCert=%2Fpath%2Fto%2Fmysql%2Fclient-cert.pem&" + - "sslKeyPassword=key-pem-password-in-here" -) - -// Creating a Mono using Project Reactor -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -> It is just example, see also Programmatic Connection Factory Discovery for more options. - -Or use unix domain socket like following: - -```java -// Minimum configuration for unix domain socket -ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:mysql://root@unix?unixSocket=%2Fpath%2Fto%2Fmysql.sock") - -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -### Programmatic Connection Factory Discovery - -```java -ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() - .option(DRIVER, "mysql") - .option(HOST, "127.0.0.1") - .option(USER, "root") - .option(PORT, 3306) // optional, default 3306 - .option(PASSWORD, "database-password-in-here") // optional, default null, null means has no password - .option(DATABASE, "r2dbc") // optional, default null, null means not specifying the database - .option(Option.valueOf("createDatabaseIfNotExist"), true) // optional, default false, create database if not exist (since 1.0.6 / 0.9.7) - .option(CONNECT_TIMEOUT, Duration.ofSeconds(3)) // optional, default null, null means no timeout - .option(SSL, true) // optional, default sslMode is "preferred", it will be ignore if sslMode is set - .option(Option.valueOf("sslMode"), "verify_identity") // optional, default "preferred" - .option(Option.valueOf("sslCa"), "/path/to/mysql/ca.pem") // required when sslMode is verify_ca or verify_identity, default null, null means has no server CA cert - .option(Option.valueOf("sslCert"), "/path/to/mysql/client-cert.pem") // optional, default null, null means has no client cert - .option(Option.valueOf("sslKey"), "/path/to/mysql/client-key.pem") // optional, default null, null means has no client key - .option(Option.valueOf("sslKeyPassword"), "key-pem-password-in-here") // optional, default null, null means has no password for client key (i.e. "sslKey") - .option(Option.valueOf("tlsVersion"), "TLSv1.3,TLSv1.2,TLSv1.1") // optional, default is auto-selected by the server - .option(Option.valueOf("sslHostnameVerifier"), "com.example.demo.MyVerifier") // optional, default is null, null means use standard verifier - .option(Option.valueOf("sslContextBuilderCustomizer"), "com.example.demo.MyCustomizer") // optional, default is no-op customizer - .option(Option.valueOf("zeroDate"), "use_null") // optional, default "use_null" - .option(Option.valueOf("useServerPrepareStatement"), true) // optional, default false - .option(Option.valueOf("allowLoadLocalInfileInPath"), "/opt") // optional, default null, null means LOCAL INFILE not be allowed - .option(Option.valueOf("tcpKeepAlive"), true) // optional, default false - .option(Option.valueOf("tcpNoDelay"), true) // optional, default false - .option(Option.valueOf("autodetectExtensions"), false) // optional, default false - .option(Option.valueOf("passwordPublisher"), Mono.just("password")) // optional, default null, null means has no passwordPublisher (since 1.0.5 / 0.9.6) - .build(); -ConnectionFactory connectionFactory = ConnectionFactories.get(options); +ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbcs:mysql://root:database-password-in-here@127.0.0.1:3306/r2dbc"); // Creating a Mono using Project Reactor Mono connectionMono = Mono.from(connectionFactory.create()); ``` -Or use unix domain socket like following: - -```java -// Minimum configuration for unix domain socket -ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() - .option(DRIVER, "mysql") - .option(Option.valueOf("unixSocket"), "/path/to/mysql.sock") - .option(USER, "root") - .build(); -ConnectionFactory connectionFactory = ConnectionFactories.get(options); - -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -### Programmatic Configuration - -```java -MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() - .host("127.0.0.1") - .user("root") - .port(3306) // optional, default 3306 - .password("database-password-in-here") // optional, default null, null means has no password - .database("r2dbc") // optional, default null, null means not specifying the database - .createDatabaseIfNotExist(true) // optional, default false, create database if not exist (since 1.0.6 / 0.9.7) - .serverZoneId(ZoneId.of("Continent/City")) // optional, default null, null means query server time zone when connection init - .connectTimeout(Duration.ofSeconds(3)) // optional, default null, null means no timeout - .sslMode(SslMode.VERIFY_IDENTITY) // optional, default SslMode.PREFERRED - .sslCa("/path/to/mysql/ca.pem") // required when sslMode is VERIFY_CA or VERIFY_IDENTITY, default null, null means has no server CA cert - .sslCert("/path/to/mysql/client-cert.pem") // optional, default has no client SSL certificate - .sslKey("/path/to/mysql/client-key.pem") // optional, default has no client SSL key - .sslKeyPassword("key-pem-password-in-here") // optional, default has no client SSL key password - .tlsVersion(TlsVersions.TLS1_3, TlsVersions.TLS1_2, TlsVersions.TLS1_1) // optional, default is auto-selected by the server - .sslHostnameVerifier(MyVerifier.INSTANCE) // optional, default is null, null means use standard verifier - .sslContextBuilderCustomizer(MyCustomizer.INSTANCE) // optional, default is no-op customizer - .zeroDateOption(ZeroDateOption.USE_NULL) // optional, default ZeroDateOption.USE_NULL - .useServerPrepareStatement() // Use server-preparing statements, default use client-preparing statements - .allowLoadLocalInfileInPath("/opt") // optional, default null, null means LOCAL INFILE not be allowed - .tcpKeepAlive(true) // optional, controls TCP Keep Alive, default is false - .tcpNoDelay(true) // optional, controls TCP No Delay, default is false - .autodetectExtensions(false) // optional, controls extension auto-detect, default is true - .extendWith(MyExtension.INSTANCE) // optional, manual extend an extension into extensions, default using auto-detect - .passwordPublisher(Mono.just("password")) // optional, default null, null means has no password publisher (since 1.0.5 / 0.9.6) - .build(); -ConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration); - -// Creating a Mono using Project Reactor -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -Or use unix domain socket like following: - -```java -// Minimum configuration for unix domain socket -MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() - .unixSocket("/path/to/mysql.sock") - .user("root") - .build(); -ConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration); - -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -### Configuration items - -| name | valid values | required | description | -|---|---|---|---| -| driver | A constant "mysql" | Required in R2DBC discovery | This driver needs to be discovered by name in R2DBC | -| host | A hostname or IP | Required when `unixSocket` does not exists | The host of MySQL database server | -| unixSocket | An absolute or relative path | Required when `host` does not exists | The `.sock` file of Unix Domain Socket | -| port | A positive integer less than 65536 | Optional, default 3306 | The port of MySQL database server | -| user | A valid MySQL username and not be empty | Required | Who wants to connect to the MySQL database | -| password | Any printable string | Optional, default no password | The password of the MySQL database user | -| database | A valid MySQL database name | Optional, default does not initialize database | Database used by the MySQL connection | -| createDatabaseIfNotExist | `true` or `false` | Optional, default `false` | Create database if not exist | -| connectTimeout | A `Duration` which must be positive duration | Optional, default has no timeout | TCP connect timeout | -| serverZoneId | An id of `ZoneId` | Optional, default query time zone when connection init | Server time zone id | -| tcpKeepAlive | `true` or `false` | Optional, default disabled | Controls TCP KeepAlive | -| tcpNoDelay | `true` or `false` | Optional, default disabled | Controls TCP NoDelay | -| sslMode | A value of `SslMode` | Optional, default `PREFERRED` when using hosting connection, `DISABLED` when using Unix Domain Socket | SSL mode, see following notice | -| sslCa | A path of local file which type is `PEM` | Required when `sslMode` is `VERIFY_CA` or `VERIFY_IDENTITY` | The CA cert of MySQL database server | -| sslCert | A path of local file which type is `PEM` | Required when `sslKey` exists | The SSL cert of client | -| sslKey | A path of local file which type is `PEM` | Required when `sslCert` exists | The SSL key of client | -| sslKeyPassword | Any valid password for `PEM` file | Optional, default `sslKey` has no password | The password for client SSL key (i.e. `sslKey`) | -| tlsVersion | Any value list of `TlsVersions` | Optional, default is auto-selected by the server | The TLS version for SSL, see following notice | -| sslHostnameVerifier | A `HostnameVerifier` | Optional, default use RFC standard | Used only if `SslMode` is `VERIFY_CA` or higher | -| sslContextBuilderCustomizer | A `Function` | Optional, default is NO-OP function | Used only if `SslMode` is not `DISABLED` | -| zeroDateOption | Any value of `ZeroDateOption` | Optional, default `USE_NULL` | The option indicates "zero date" handling, see following notice | -| autodetectExtensions | `true` or `false` | Optional, default is `true` | Controls auto-detect `Extension`s | -| useServerPrepareStatement | `true`, `false` or `Predicate` | Optional, default is `false` | See following notice | -| allowLoadLocalInfileInPath | A path | Optional, default is `null` | The path that allows `LOAD DATA LOCAL INFILE` to load file data | -| passwordPublisher | A `Publisher` | Optional, default is `null` | The password publisher, see following notice | - -- `SslMode` Considers security level and verification for SSL, make sure the database server supports SSL before you want change SSL mode to `REQUIRED` or higher. **The Unix Domain Socket only offers "DISABLED" available** - - `DISABLED` I don't care about security and don't want to pay the overhead for encryption - - `PREFERRED` I don't care about encryption but will pay the overhead of encryption if the server supports it. **Unavailable on Unix Domain Socket** - - `REQUIRED` I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want. **Unavailable on Unix Domain Socket** - - `VERIFY_CA` I want my data encrypted, and I accept the overhead. I want to be sure I connect to a server that I trust. **Unavailable on Unix Domain Socket** - - `VERIFY_IDENTITY` (the highest level, most like web browser): I want my data encrypted, and I accept the overhead. I want to be sure I connect to a server I trust, and that it's the one I specify. **Unavailable on Unix Domain Socket** - - `TUNNEL` Use SSL tunnel to connect to MySQL, it may be useful for some RDS that's using SSL proxy **Unavailable on Unix Domain Socket** -- `TlsVersions` Considers TLS version names for SSL, can be **multi-values** in the configuration, make sure the database server supports selected TLS versions. Usually sorted from higher to lower. **Unavailable on Unix Domain Socket** - - `TLS1` (i.e. "TLSv1") Under generic circumstances, MySQL database supports it if database supports SSL - - `TLS1_1` (i.e. "TLSv1.1") Under generic circumstances, MySQL database supports it if database supports SSL - - `TLS1_2` (i.e. "TLSv1.2") Supported only in Community Edition `8.0.4` or higher, and Enterprise Edition `5.6.0` or higher - - `TLS1_3` (i.e. "TLSv1.3") Supported only available as of MySQL `8.0.16` or higher, and requires compiling MySQL using OpenSSL `1.1.1` or higher -- `ZeroDateOption` Considers special handling when MySQL database server returning "zero date" (i.e. `0000-00-00 00:00:00`) - - `EXCEPTION` Just throw an exception when MySQL database server return "zero date" - - `USE_NULL` Use `null` when MySQL database server return "zero date" - - `USE_ROUND` **NOT** RECOMMENDED, only for compatibility. Use "round" date (i.e. `0001-01-01 00:00:00`) when MySQL database server return "zero date" -- Prepare Statement: Considers based on server-preparing or client-preparing, some database server maybe not support server-preparing binary-query, such as Vitess - - `useClientPrepareStatement()` default preparing mode, use client-preparing text-query for parametrized statements - - `useServerPrepareStatement()` use server-preparing binary-query for parametrized statements - - `useServerPrepareStatement(Predicate)` use server-preparing binary-query for parametrized statements, and enforce server-preparing usage for simple query (not parametrized statements). The usage is judged by `Predicate`, it's parameter is the simple SQL statement, enforce server-preparing if return `true` -- `extendWith` Manual extend `Extension`, only available in **programmatic configuration** - - It is **NOT** RECOMMENDED, enable the `autodetectExtensions` is the best way for extensions - - The `Extensions` will not remove duplicates, make sure it would be not extended twice or more - - The auto-detected `Extension`s will not affect manual extends and will not remove duplicates -- `passwordPublisher` Every time the client attempts to authenticate, it will use the password provided by the `passwordPublisher`.(Since `1.0.5` / `0.9.6`) e.g., You can employ this method for IAM-based authentication when connecting to an AWS Aurora RDS database. - -Should use `enum` in [Programmatic](#programmatic-configuration) configuration that not like discovery configurations, except `TlsVersions` (All elements of `TlsVersions` will be always `String` which is case-sensitive). +See [Getting Started](https://github.com/asyncer-io/r2dbc-mysql/wiki/getting-started) and [Configuration Options](https://github.com/asyncer-io/r2dbc-mysql/wiki/Configuration-Options) wiki for more information. ### Pooling See [r2dbc-pool](https://github.com/r2dbc/r2dbc-pool). -### Simple statement +### Usage ```java connection.createStatement("INSERT INTO `person` (`first_name`, `last_name`) VALUES ('who', 'how')") .execute(); // return a Publisher include one Result ``` -### Parametrized statement - -```java -connection.createStatement("INSERT INTO `person` (`birth`, `nickname`, `show_name`) VALUES (?, ?name, ?name)") - .bind(0, LocalDateTime.of(2019, 6, 25, 12, 12, 12)) - .bind("name", "Some one") // Not one-to-one binding, call twice of native index-bindings, or call once of name-bindings. - .add() - .bind(0, LocalDateTime.of(2009, 6, 25, 12, 12, 12)) - .bind(1, "My Nickname") - .bind(2, "Naming show") - .returnGeneratedValues("generated_id") - .execute(); // return a Publisher include two Results. -``` - -- All parameters must be bound before execute, even parameter is `null` (use `bindNull` to bind `null`). -- It will be using client-preparing by default, see `useServerPrepareStatement` in configuration. -- In one-to-one binding, because native MySQL prepared statements use index-based parameters, *index-bindings* will have **better** performance than *name-bindings*. - -### Batch statement - -```java -connection.createBatch() - .add("INSERT INTO `person` (`first_name`, `last_name`) VALUES ('who', 'how')") - .add("UPDATE `earth` SET `count` = `count` + 1 WHERE `id` = 'human'") - .execute(); // return a Publisher include two Results. -``` - -> The last `;` will be removed if and only if last statement contains ';', and statement has only whitespace follow the last `;`. - -### Transactions - -```java -connection.beginTransaction() - .then(Mono.from(connection.createStatement("INSERT INTO `person` (`first_name`, `last_name`) VALUES ('who', 'how')").execute())) - .flatMap(Result::getRowsUpdated) - .thenMany(connection.createStatement("INSERT INTO `person` (`birth`, `nickname`, `show_name`) VALUES (?, ?name, ?name)") - .bind(0, LocalDateTime.of(2019, 6, 25, 12, 12, 12)) - .bind("name", "Some one") - .add() - .bind(0, LocalDateTime.of(2009, 6, 25, 12, 12, 12)) - .bind(1, "My Nickname") - .bind(2, "Naming show") - .returnGeneratedValues("generated_id") - .execute()) - .flatMap(Result::getRowsUpdated) - .then(connection.commitTransaction()); -``` - -## Data Type Mapping - -The default built-in `Codec`s reference table shows the type mapping between [MySQL][m] and Java data types: - -| MySQL Type | Unsigned | Support Data Type | -|---|---|---| -| `INT` | `UNSIGNED` | [**`Long`**][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `INT` | `SIGNED` | [**`Integer`**][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `TINYINT` | `UNSIGNED` | [**`Short`**][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref], [`Boolean`][java-Boolean-ref] (Size is 1) | -| `TINYINT` | `SIGNED` | [**`Byte`**][java-Byte-ref], [`Short`][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref], [`Boolean`][java-Boolean-ref] (Size is 1) | -| `SMALLINT` | `UNSIGNED` | [**`Integer`**][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `SMALLINT` | `SIGNED` | [**`Short`**][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `MEDIUMINT` | `SIGNED/UNSIGNED` | [**`Integer`**][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `BIGINT` | `UNSIGNED` | [**`BigInteger`**][java-BigInteger-ref], [`Long`][java-Long-ref] (Not check overflow) | -| `BIGINT` | `SIGNED` | [**`Long`**][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `FLOAT` | `SIGNED` / `UNSIGNED` | [**`Float`**][java-Float-ref], [`BigDecimal`][java-BigDecimal-ref] | -| `DOUBLE` | `SIGNED` / `UNSIGNED` | [**`Double`**][java-Double-ref], [`BigDecimal`][java-BigDecimal-ref] | -| `DECIMAL` | `SIGNED` / `UNSIGNED` | [**`BigDecimal`**][java-BigDecimal-ref], [`Float`][java-Float-ref] (Size less than 7), [`Double`][java-Double-ref] (Size less than 16) | -| `BIT` | - | [**`ByteBuffer`**][java-ByteBuffer-ref], [`BitSet`][java-BitSet-ref], [`Boolean`][java-Boolean-ref] (Size is 1), `byte[]` | -| `DATETIME` / `TIMESTAMP` | - | [**`LocalDateTime`**][java-LocalDateTime-ref], [`ZonedDateTime`][java-ZonedDateTime-ref], [`OffsetDateTime`][java-OffsetDateTime-ref], [`Instant`][java-Instant-ref] | -| `DATE` | - | [**`LocalDate`**][java-LocalDate-ref] | -| `TIME` | - | [**`LocalTime`**][java-LocalTime-ref], [`Duration`][java-Duration-ref], [`OffsetTime`][java-OffsetTime-ref] | -| `YEAR` | - | [**`Short`**][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref], [`Year`][java-Year-ref] | -| `VARCHAR` / `NVARCHAR` | - | [**`String`**][java-String-ref] | -| `VARBINARY` | - | [**`ByteBuffer`**][java-ByteBuffer-ref], `Blob`, `byte[]` | -| `CHAR` / `NCHAR` | - | [**`String`**][java-String-ref] | -| `ENUM` | - | [**`String`**][java-String-ref], [`Enum`][java-Enum-ref] | -| `SET` | - | **`String[]`**, [`String`][java-String-ref], [`Set`][java-Set-ref] and [`Set>`][java-Set-ref] ([`Set`][java-Set-ref] need use [`ParameterizedType`][java-ParameterizedType-ref]) | -| `BLOB`s (`LONGBLOB`, etc.) | - | [**`ByteBuffer`**][java-ByteBuffer-ref], `Blob`, `byte[]` | -| `TEXT`s (`LONGTEXT`, etc.) | - | [**`String`**][java-String-ref], `Clob` | -| `JSON` | - | [**`String`**][java-String-ref], `Clob` | -| `GEOMETRY` | - | **`byte[]`**, `Blob` | - -## Statements Logging/Debugging - -Use the `io.asyncer.r2dbc.mysql.QUERY` logger and the `DEBUG` log level to log statements and their bound -parameters (if it is prepared statement). - -For example, in `logback.xml`: - -```xml - - - - - - -``` - -Note that it will print the SQL statement and all parameters, so this may be a security risk. Don't use it -in an environment with sensitive data. It should be used for debugging purposes only. - -The log format may be different for server-preparing and client-preparing. This is because the actual -generated statements and commands are different in these two modes. For example, a server-preparing statement -has its statement ID that's generated by server, but client-preparing does not. - -## Add a `Codec` - -This is an extension of a highly customized driver behavior of encoding parameter or decoding field data. - -Example for an extending `Codec` of JSON based-on Jackson. - -First, implement a `Codec`, `ParametrizedCodec`, `MassiveCodec` or `MassiveParametrizedCodec`. - -- `Codec` the normal codec - - Data type is `Class` - - Data buffer size is less than or equal to `Integer.MAX_VALUE` -- `ParametrizedCodec` - - Data type is `Class` or `ParametrizedType` - - Data buffer size is less than or equal to `Integer.MAX_VALUE` -- `MassiveCodec` - - Data type is `Class` - - Data buffer size is less than or equal to `UnsignedInteger.MAX_VALUE` (Java does not have unsigned integers, it just only represents the range) -- `MassiveParametrizedCodec` - - Data type is `Class` or `ParametrizedType` - - Data buffer size is less than or equal to `UnsignedInteger.MAX_VALUE` - -Actually, `JSON` can store large json data, and its byte size can be `UnsignedInteger.MAX_VALUE`. However, this is just an example. - -```java -public final class JacksonCodec implements Codec { - - /** - * JUST for example, should configure it in real applications. - */ - private static final ObjectMapper MAPPER = new ObjectMapper(); - - private final ByteBufAllocator allocator; - - /** - * Used for encoding/decoding mode, see also registrar in second step. - */ - private final boolean encoding; - - public JacksonCodec(ByteBufAllocator allocator, boolean encoding) { - this.allocator = allocator; - this.encoding = encoding; - } - - @Override - public Object decode(ByteBuf value, FieldInformation info, Class target, boolean binary, CodecContext context) { - // If you ensure server is using UTF-8, you can just use InputStream - try (Reader r = new InputStreamReader(new ByteBufInputStream(value), CharCollation.fromId(info.getCollationId(), context.getServerVersion()).getCharset())) { - return MAPPER.readValue(r, target); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public Parameter encode(Object value, CodecContext context) { - return new JacksonParameter(allocator, value, context); - } - - @Override - public boolean canDecode(FieldInformation info, Class target) { - return !encoding && info.getType() == DataTypes.JSON && info.getCollationId() != CharCollation.BINARY_ID; - } - - @Override - public boolean canEncode(Object value) { - return encoding; - } - - private static final class JacksonParameter implements MySqlParameter { - - private final ByteBufAllocator allocator; - - private final Object value; - - private final CodecContext context; - - private JacksonParameter(ByteBufAllocator allocator, Object value, CodecContext context) { - this.allocator = allocator; - this.value = value; - this.context = context; - } - - @Override - public Mono publishBinary() { - // JSON in binary protocol should be a var-integer sized encoded string. - // That means we should write a var-integer as a size of following content - // bytes firstly, then write the encoded string as content. - // - // Binary protocol may be different for each type of encoding, so if do not - // use binary protocol, just return a Mono.error() instead. - return Mono.fromSupplier(() -> { - Charset charset = context.getClientCollation().getCharset(); - ByteBuf content = allocator.buffer(); - - // Encode and calculate content bytes first, we should know bytes size. - try (Writer w = new OutputStreamWriter(new ByteBufOutputStream(content), charset)) { - MAPPER.writeValue(w, value); - } catch (IOException e) { - content.release(); - throw new CustomRuntimeException(e); - } catch (Throwable e) { - content.release(); - throw e; - } - - ByteBuf buf = null; - try { - buf = allocator.buffer(); - // VarIntUtils is an unstable, internal utility. - VarIntUtils.writeVarInt(buf, content.readableBytes()); - return buf.writeBytes(content); - } catch (Throwable e) { - if (buf != null) { - buf.release(); - } - throw e; - } finally { - content.release(); - } - }); - } - - @Override - public Mono publishText(ParameterWriter writer) { - return Mono.fromRunnable(() -> { - try { - MAPPER.writeValue(writer, value); - } catch (IOException e) { - throw new CustomRuntimeException(e); - } - }); - } - - @Override - public short getType() { - return DataTypes.VARCHAR; - } - - /** - * Optional, for statements/parameters logging. - */ - @Override - public String toString() { - return value.toString(); - } - } -} -``` - -Second, implement a `CodecRegistrar`. - -```java -// It is just an example of package name and does not represent any company, individual or organization. -package org.example.demo.json; - -// Some imports... - -public final class JacksonCodecRegistrar implements CodecRegistrar { - - @Override - public void register(ByteBufAllocator allocator, CodecRegistry registry) { - // Decoding JSON by highest priority, encoding anything by lowest priority. - registry.addFirst(new JacksonCodec(allocator, false)) - .addLast(new JacksonCodec(allocator, true)); - } -} -``` - -Finally, create a file in `META-INF/services`, which file name is `io.asyncer.r2dbc.mysql.extension.Extension`, it contains this line: - -``` -org.example.demo.json.JacksonCodecRegistrar -``` +See [Usage](https://github.com/asyncer-io/r2dbc-mysql/wiki/usage) wiki for more information. ## Reporting Issues -The R2DBC MySQL Implementation uses GitHub as issue tracking system to record bugs and feature requests. +The R2DBC MySQL Implementation uses GitHub as issue tracking system to record bugs and feature requests. If you want to raise an issue, please follow the recommendations below: - Before log a bug, please search the [issue tracker](https://github.com/asyncer-io/r2dbc-mysql/issues) to see if someone has already reported the problem. @@ -594,35 +147,15 @@ This project is released under version 2.0 of the [Apache License](https://www.a ## Contributors + + + + Thanks a lot for your support! ## Supports -- [R2DBC Team](https://r2dbc.io) - Thanks for their support by sharing all relevant resources around R2DBC +- [R2DBC Team](https://r2dbc.io) - Thanks for their support by sharing all relevant resources around R2DBC projects. [m]: https://www.mysql.com -[java-BigDecimal-ref]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html -[java-BigInteger-ref]: https://docs.oracle.com/javase/8/docs/api/java/math/BigInteger.html -[java-BitSet-ref]: https://docs.oracle.com/javase/8/docs/api/java/util/BitSet.html -[java-Boolean-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Boolean.html -[java-Byte-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Byte.html -[java-ByteBuffer-ref]: https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html -[java-Double-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html -[java-Float-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Float.html -[java-Integer-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html -[java-Long-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Long.html -[java-LocalDateTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html -[java-ZonedDateTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html -[java-OffsetDateTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html -[java-Instant-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html -[java-LocalDate-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html -[java-Duration-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html -[java-LocalTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html -[java-OffsetTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/OffsetTime.html -[java-Year-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/Year.html -[java-Short-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Short.html -[java-Enum-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Enum.html -[java-String-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/String.html -[java-Set-ref]: https://docs.oracle.com/javase/8/docs/api/java/util/Set.html -[java-ParameterizedType-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/ParameterizedType.html diff --git a/build-tools/pom.xml b/build-tools/pom.xml new file mode 100644 index 000000000..96fc8ec27 --- /dev/null +++ b/build-tools/pom.xml @@ -0,0 +1,25 @@ + + + + + 4.0.0 + io.asyncer + build-tools + INTERNAL + diff --git a/build-tools/src/main/resources/io/asyncer/checkstyle-suppressions.xml b/build-tools/src/main/resources/io/asyncer/checkstyle-suppressions.xml new file mode 100644 index 000000000..ad162be87 --- /dev/null +++ b/build-tools/src/main/resources/io/asyncer/checkstyle-suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/build-tools/src/main/resources/io/asyncer/checkstyle.xml b/build-tools/src/main/resources/io/asyncer/checkstyle.xml new file mode 100644 index 000000000..641d43fec --- /dev/null +++ b/build-tools/src/main/resources/io/asyncer/checkstyle.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/containers/mariadb-compose.yml b/containers/mariadb-compose.yml deleted file mode 100644 index a126b551b..000000000 --- a/containers/mariadb-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "3" - -services: - mariadb: - image: mariadb:${MARIADB_VERSION} - container_name: mariadb_${MARIADB_VERSION} - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE: ${MYSQL_DATABASE} - command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - ports: - - "3306:3306" diff --git a/containers/mysql-compose.yml b/containers/mysql-compose.yml deleted file mode 100644 index 2929f250e..000000000 --- a/containers/mysql-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "3" - -services: - mariadb: - image: mysql:${MYSQL_VERSION} - container_name: mysql_${MYSQL_VERSION} - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE: ${MYSQL_DATABASE} - command: mysqld --local-infile=true --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - ports: - - "3306:3306" diff --git a/mvnw b/mvnw index 66df28542..19529ddf8 100755 --- a/mvnw +++ b/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,290 +19,241 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.2.0 -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false +# OS specific support. +native_path() { printf %s\\n "$1"; } case "$(uname)" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME - else - JAVA_HOME="/Library/Java/Home"; export JAVA_HOME - fi - fi - ;; +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; esac -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=$(java-config --jre-home) - fi -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --unix "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --unix "$CLASSPATH") -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && - JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=$(which readlink) - if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then - if $darwin ; then - javaHome="$(dirname "\"$javaExecutable\"")" - javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" - else - javaExecutable="$(readlink -f "\"$javaExecutable\"")" - fi - javaHome="$(dirname "\"$javaExecutable\"")" - javaHome=$(expr "$javaHome" : '\(.*\)/bin') - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi fi else - JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi +} - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=$(cd "$wdir/.." || exit 1; pwd) - fi - # end of workaround +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done - printf '%s' "$(cd "$basedir" || exit 1; pwd)" + printf %x\\n $h } -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - # Remove \r in case we run on Windows within Git Bash - # and check out the repository with auto CRLF management - # enabled. Otherwise, we may read lines that are delimited with - # \r\n and produce $'-Xarg\r' rather than -Xarg due to word - # splitting rules. - tr -s '\r\n' ' ' < "$1" - fi +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 } -log() { - if [ "$MVNW_VERBOSE" = true ]; then - printf '%s\n' "$1" - fi +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } -BASE_DIR=$(find_maven_basedir "$(dirname "$0")") -if [ -z "$BASE_DIR" ]; then - exit 1; +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR -log "$MAVEN_PROJECTBASEDIR" +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" -if [ -r "$wrapperJarPath" ]; then - log "Found $wrapperJarPath" +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT else - log "Couldn't find $wrapperJarPath, downloading it ..." + die "cannot create temp dir" +fi - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - fi - while IFS="=" read -r key value; do - # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) - safeValue=$(echo "$value" | tr -d '\r') - case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; - esac - done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" - log "Downloading from: $wrapperUrl" +mkdir -p -- "${MAVEN_HOME%/*}" - if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") - fi +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - if command -v wget > /dev/null; then - log "Found wget ... using wget" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - log "Found curl ... using curl" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - fi - else - log "Falling back to using Java to download" - javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=$(cygpath --path --windows "$javaSource") - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - log " - Compiling MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - log " - Running MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" - fi - fi - fi +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -########################################################################################## -# End of extension -########################################################################################## -# If specified, validate the SHA-256 sum of the Maven wrapper jar file -wrapperSha256Sum="" -while IFS="=" read -r key value; do - case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; - esac -done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" -if [ -n "$wrapperSha256Sum" ]; then - wrapperSha256Result=false - if command -v sha256sum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then - wrapperSha256Result=true +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true fi - elif command -v shasum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then - wrapperSha256Result=true + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." - echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi - if [ $wrapperSha256Result = false ]; then - echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 - echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 - echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -# shellcheck disable=SC2086 # safe args -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 93880f5d9..b150b91ed 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,3 +1,4 @@ +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -7,7 +8,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,188 +19,131 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.2.0 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file -SET WRAPPER_SHA_256_SUM="" -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) -IF NOT %WRAPPER_SHA_256_SUM%=="" ( - powershell -Command "&{"^ - "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ - "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ - " exit 1;"^ - "}"^ - "}" - if ERRORLEVEL 1 goto error -) - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml index 9b034e46f..305230922 100644 --- a/pom.xml +++ b/pom.xml @@ -17,460 +17,14 @@ 4.0.0 - io.asyncer - r2dbc-mysql - 1.1.0 - jar - - Reactive Relational Database Connectivity - MySQL - https://github.com/asyncer-io/r2dbc-mysql - R2DBC MySQL Implementation - - - - The Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - - - - - asyncer.io - https://github.com/asyncer-io/r2dbc-mysql - - - - - jchrys - jchrys - jchrys@me.com - - Project Lead - - - - - 2018 - - scm:git:git://github.com/asyncer-io/r2dbc-mysql.git - scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git - https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.1.0 - - - - UTF-8 - UTF-8 - 1.8 - false - - 1.0.0.RELEASE - 2022.0.9 - 3.24.2 - 1.37 - 5.10.1 - 1.4.14 - 4.11.0 - 8.2.0 - 1.19.3 - 4.0.3 - 5.3.31 - 2.16.0 - 0.3.0.RELEASE - 3.0.2 - 24.1.0 - - - - - - io.projectreactor - reactor-bom - ${reactor.version} - pom - import - - - org.junit - junit-bom - ${junit.version} - pom - import - - - org.testcontainers - testcontainers-bom - ${testcontainers.version} - pom - import - - - com.fasterxml.jackson - jackson-bom - ${jackson.version} - pom - import - - - org.jetbrains - annotations - ${java-annotations.version} - provided - - - - - - - io.projectreactor - reactor-core - - - io.projectreactor.netty - reactor-netty - - - io.r2dbc - r2dbc-spi - ${r2dbc-spi.version} - - - org.jetbrains - annotations - - - - com.google.code.findbugs - jsr305 - ${jsr305.version} - provided - - - - ch.qos.logback - logback-classic - ${logback.version} - test - - - io.projectreactor - reactor-test - test - - - io.r2dbc - r2dbc-spi-test - ${r2dbc-spi.version} - test - - - org.assertj - assertj-core - ${assertj.version} - test - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-params - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - com.mysql - mysql-connector-j - ${mysql.version} - test - - - com.zaxxer - HikariCP - ${hikari-cp.version} - test - - - org.slf4j - slf4j-api - - - - - org.springframework - spring-jdbc - ${spring-framework.version} - test - - - org.testcontainers - mysql - test - - - org.slf4j - slf4j-api - - - - - com.fasterxml.jackson.core - jackson-core - test - - - com.fasterxml.jackson.core - jackson-databind - test - - - com.fasterxml.jackson.core - jackson-annotations - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.11.0 - - - -Xlint:all - -Xlint:-options - -Xlint:-processing - -Xlint:-serial - - true - ${java.version} - ${java.version} - - - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - true - true - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 3.1.1 - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.6.3 - - - io.asyncer.r2dbc.mysql.authentication,io.asyncer.r2dbc.mysql.client,io.asyncer.r2dbc.mysql.util,io.asyncer.r2dbc.mysql.codec.lob,io.asyncer.r2dbc.mysql.message - - - https://r2dbc.io/spec/${r2dbc-spi.version}/api/ - https://projectreactor.io/docs/core/release/api/ - https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/ - - en_US - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.0 - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.2.3 - - random - - **/*Test.java - - - **/*TestKit.java - **/*IntegrationTest.java - - ${maven.surefire.skip} - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.2.3 - - - - integration-test - verify - - - - - random - - **/*TestKit.java - **/*IntegrationTest.java - - - - - - - ${project.basedir} - - LICENSE - - META-INF - - - ${project.basedir}/src/main/resources - - - - - - - jmh - - - com.github.mp911de.microbenchmark-runner - microbenchmark-runner-junit5 - ${mbr.version} - test - - - org.openjdk.jmh - jmh-core - ${jmh.version} - test - - - org.openjdk.jmh - jmh-generator-annprocess - ${jmh.version} - test - - - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.5.0 - - - add-source - generate-sources - - add-test-source - - - - src/jmh/java - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - true - - - - org.apache.maven.plugins - maven-failsafe-plugin - - true - - - - org.codehaus.mojo - exec-maven-plugin - 3.1.1 - - - run-benchmarks - pre-integration-test - - exec - - - test - java - - -classpath - - org.openjdk.jmh.Main - .* - - - - - - - - - - - - - jitpack.io - https://jitpack.io - - - - - - - false - - - true - - ossrh-snapshots - Sonatype Nexus Snapshots - https://s01.oss.sonatype.org/content/repositories/snapshots/ - - + r2dbc-mysql-parent + INTERNAL + pom + + + r2dbc-mysql + test-native-image + build-tools + diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml new file mode 100644 index 000000000..a903116a8 --- /dev/null +++ b/r2dbc-mysql/pom.xml @@ -0,0 +1,574 @@ + + + + 4.0.0 + + io.asyncer + r2dbc-mysql + 1.4.2-SNAPSHOT + + Reactive Relational Database Connectivity - MySQL + https://github.com/asyncer-io/r2dbc-mysql + R2DBC MySQL Implementation + + + + The Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + asyncer.io + https://github.com/asyncer-io/r2dbc-mysql + + + + + jchrys + jchrys + jchrys@me.com + + Maintainer + + + + mirromutth + mirromutth + mirromutth@gmail.com + + Maintainer + + + + + 2018 + + scm:git:git://github.com/asyncer-io/r2dbc-mysql.git + scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git + https://github.com/asyncer-io/r2dbc-mysql + HEAD + + + + UTF-8 + UTF-8 + 1.8 + 8 + 8 + false + + 1.0.0.RELEASE + 2024.0.3 + 4.1.118.Final + 3.25.3 + 1.37 + 5.10.2 + 1.5.3 + 4.11.0 + 8.3.0 + 3.3.3 + 1.21.0 + 4.0.3 + 5.3.32 + 2.16.1 + 0.4.0.RELEASE + 3.0.2 + 1.5.5-11 + 24.1.0 + 1.77 + + + + + + io.projectreactor + reactor-bom + ${reactor.version} + pom + import + + + io.netty + netty-bom + ${netty.version} + pom + import + + + org.junit + junit-bom + ${junit.version} + pom + import + + + org.testcontainers + testcontainers-bom + ${testcontainers.version} + pom + import + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + org.jetbrains + annotations + ${java-annotations.version} + provided + + + org.bouncycastle + bcpkix-jdk18on + ${bouncy-castle.version} + test + + + + + + + io.projectreactor + reactor-core + + + io.projectreactor.netty + reactor-netty-core + + + io.netty + netty-transport-native-epoll + linux-x86_64 + true + + + io.netty + netty-transport-native-kqueue + osx-x86_64 + true + + + io.r2dbc + r2dbc-spi + ${r2dbc-spi.version} + + + org.jetbrains + annotations + + + + com.google.code.findbugs + jsr305 + ${jsr305.version} + provided + + + + com.github.luben + zstd-jni + ${zstd-jni.version} + true + + + + ch.qos.logback + logback-classic + ${logback.version} + test + + + io.projectreactor + reactor-test + test + + + io.r2dbc + r2dbc-spi-test + ${r2dbc-spi.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.mysql + mysql-connector-j + ${mysql.version} + test + + + org.mariadb.jdbc + mariadb-java-client + ${mariadb.version} + test + + + com.zaxxer + HikariCP + ${hikari-cp.version} + test + + + org.slf4j + slf4j-api + + + + + org.springframework + spring-jdbc + ${spring-framework.version} + test + + + org.testcontainers + mysql + test + + + org.slf4j + slf4j-api + + + + + org.testcontainers + mariadb + test + + + org.slf4j + slf4j-api + + + + + com.fasterxml.jackson.core + jackson-core + test + + + com.fasterxml.jackson.core + jackson-databind + test + + + com.fasterxml.jackson.core + jackson-annotations + test + + + org.bouncycastle + bcpkix-jdk18on + test + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.1 + + + validate + validate + + io/asyncer/checkstyle.xml + io/asyncer/checkstyle-suppressions.xml + true + true + true + + + check + + + + + + io.asyncer + build-tools + INTERNAL + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.12.1 + + + -Xlint:all + -Xlint:-options + -Xlint:-processing + -Xlint:-serial + + true + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + true + true + + + + + + org.apache.maven.plugins + maven-release-plugin + 3.0.1 + + r2dbc-mysql-@{project.version} + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.6.3 + + + io.asyncer.r2dbc.mysql.authentication,io.asyncer.r2dbc.mysql.client,io.asyncer.r2dbc.mysql.util,io.asyncer.r2dbc.mysql.codec.lob,io.asyncer.r2dbc.mysql.message + + + https://r2dbc.io/spec/${r2dbc-spi.version}/api/ + https://projectreactor.io/docs/core/release/api/ + https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/ + + en_US + + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + random + + **/*Test.java + + + **/*TestKit.java + **/*IntegrationTest.java + + ${maven.surefire.skip} + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + + + integration-test + verify + + + + + random + + **/*TestKit.java + **/*IntegrationTest.java + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + + central + + + + + + ${project.basedir} + + LICENSE + + META-INF + + + ${project.basedir}/src/main/resources + + + + + + + jmh + + + com.github.mp911de.microbenchmark-runner + microbenchmark-runner-junit5 + ${mbr.version} + test + + + org.openjdk.jmh + jmh-core + ${jmh.version} + test + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + test + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.5.0 + + + add-source + generate-sources + + add-test-source + + + + src/jmh/java + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + org.apache.maven.plugins + maven-failsafe-plugin + + true + + + + org.codehaus.mojo + exec-maven-plugin + 3.2.0 + + + run-benchmarks + pre-integration-test + + exec + + + test + java + + -classpath + + org.openjdk.jmh.Main + .* + + + + + + + + + + + + + jitpack.io + https://jitpack.io + + + + + + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + + diff --git a/src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java similarity index 100% rename from src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java rename to r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java diff --git a/src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java similarity index 100% rename from src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java rename to r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java diff --git a/src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java similarity index 100% rename from src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java rename to r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Binding.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java similarity index 97% rename from src/main/java/io/asyncer/r2dbc/mysql/Binding.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java index a98b612d0..fc0166a09 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/Binding.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java @@ -22,9 +22,9 @@ import java.util.Arrays; /** - * A collection of {@link MySqlParameter} for one bind invocation of a parametrized statement. + * A collection of {@link MySqlParameter} for one bind invocation of a parameterized statement. * - * @see ParametrizedStatementSupport + * @see ParameterizedStatementSupport */ final class Binding { @@ -40,7 +40,7 @@ final class Binding { * Add a {@link MySqlParameter} to the binding. * * @param index the index of the {@link MySqlParameter} - * @param value the {@link MySqlParameter} from {@link PrepareParametrizedStatement} + * @param value the {@link MySqlParameter} from {@link PrepareParameterizedStatement} */ void add(int index, MySqlParameter value) { if (index < 0 || index >= this.values.length) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java similarity index 86% rename from src/main/java/io/asyncer/r2dbc/mysql/Capability.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java index 133d52e48..26299a08b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java @@ -144,7 +144,11 @@ public final class Capability { private static final long VAR_INT_SIZED_AUTH = 1L << 21; // private static final long HANDLE_EXPIRED_PASSWORD = 1L << 22; // Client can handle expired passwords. -// private static final long SESSION_TRACK = 1L << 23; + + /** + * Server can send session state information in the OK packet. + */ + private static final long SESSION_TRACK = 1L << 23; /** * The MySQL server marks the EOF message as deprecated and use OK message instead. @@ -154,7 +158,7 @@ public final class Capability { // Allow the server not to send column metadata in result set, // should NEVER enable this option. // private static final long OPTIONAL_RESULT_SET_METADATA = 1L << 25; -// private static final long Z_STD_COMPRESSION = 1L << 26; + private static final long ZSTD_COMPRESS = 1L << 26; // A reserved flag, used to extend the 32-bits capability bitmap to 64-bits. // There is no available MySql server version/edition to support it. @@ -171,7 +175,12 @@ public final class Capability { private static final long ALL_SUPPORTED = CLIENT_MYSQL | FOUND_ROWS | LONG_FLAG | CONNECT_WITH_DB | NO_SCHEMA | COMPRESS | LOCAL_FILES | IGNORE_SPACE | PROTOCOL_41 | INTERACTIVE | SSL | TRANSACTIONS | SECURE_SALT | MULTI_STATEMENTS | MULTI_RESULTS | PS_MULTI_RESULTS | - PLUGIN_AUTH | CONNECT_ATTRS | VAR_INT_SIZED_AUTH | DEPRECATE_EOF; + PLUGIN_AUTH | CONNECT_ATTRS | VAR_INT_SIZED_AUTH | SESSION_TRACK | DEPRECATE_EOF | ZSTD_COMPRESS; + + /** + * The default capabilities for a MySQL connection. It contains all client supported capabilities. + */ + public static final Capability DEFAULT = new Capability(ALL_SUPPORTED); private final long bitmap; @@ -212,9 +221,9 @@ public boolean isProtocol41() { } /** - * Checks if can use var-integer sized bytes to encode client authentication. + * Checks if allow to use var-integer sized bytes to encode client authentication. * - * @return if can use var-integer sized authentication. + * @return if allow to use var-integer sized authentication. */ public boolean isVarIntSizedAuthAllowed() { return (bitmap & VAR_INT_SIZED_AUTH) != 0; @@ -232,7 +241,7 @@ public boolean isPluginAuthAllowed() { /** * Checks if the connection contains connection attributes. * - * @return if has connection attributes. + * @return if connection attributes exists. */ public boolean isConnectionAttributesAllowed() { return (bitmap & CONNECT_ATTRS) != 0; @@ -274,6 +283,33 @@ public boolean isTransactionAllowed() { return (bitmap & TRANSACTIONS) != 0; } + /** + * Checks if any compression enabled. + * + * @return if any compression enabled. + */ + public boolean isCompression() { + return (bitmap & (COMPRESS | ZSTD_COMPRESS)) != 0; + } + + /** + * Checks if zlib compression enabled. + * + * @return if zlib compression enabled. + */ + public boolean isZlibCompression() { + return (bitmap & COMPRESS) != 0; + } + + /** + * Checks if zstd compression enabled. + * + * @return if zstd compression enabled. + */ + public boolean isZstdCompression() { + return (bitmap & ZSTD_COMPRESS) != 0; + } + /** * Extends MariaDB capabilities. * @@ -342,7 +378,8 @@ private Capability(long bitmap) { * @return the {@link Capability} without unknown flags. */ public static Capability of(long capabilities) { - return new Capability(capabilities & ALL_SUPPORTED); + long c = capabilities & ALL_SUPPORTED; + return c == ALL_SUPPORTED ? DEFAULT : new Capability(c); } static final class Builder { @@ -358,9 +395,17 @@ void disableDatabasePinned() { } void disableCompression() { + this.bitmap &= ~(COMPRESS | ZSTD_COMPRESS); + } + + void disableZlibCompression() { this.bitmap &= ~COMPRESS; } + void disableZstdCompression() { + this.bitmap &= ~ZSTD_COMPRESS; + } + void disableLoadDataLocalInfile() { this.bitmap &= ~LOCAL_FILES; } @@ -377,6 +422,10 @@ void disableSsl() { this.bitmap &= ~SSL; } + void disableSessionTrack() { + this.bitmap &= ~SESSION_TRACK; + } + void disableConnectAttributes() { this.bitmap &= ~CONNECT_ATTRS; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java new file mode 100644 index 000000000..26ec660c4 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -0,0 +1,345 @@ +/* + * Copyright 2023 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.cache.PrepareCache; +import io.asyncer.r2dbc.mysql.codec.CodecContext; +import io.asyncer.r2dbc.mysql.collation.CharCollation; +import io.asyncer.r2dbc.mysql.constant.ServerStatuses; +import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; +import io.r2dbc.spi.IsolationLevel; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Path; +import java.time.Duration; +import java.time.ZoneId; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; + +/** + * The MySQL connection context considers the behavior of server or client. + *

+ * WARNING: Do NOT change any data outside of this project, try to configure {@code ConnectionFactoryOptions} or + * {@code MySqlConnectionConfiguration} to control connection context and client behavior. + */ +public final class ConnectionContext implements CodecContext { + + private static final ServerVersion NONE_VERSION = ServerVersion.create(0, 0, 0); + + private static final ServerVersion MYSQL_5_7_4 = ServerVersion.create(5, 7, 4); + + private static final ServerVersion MARIA_10_1_1 = ServerVersion.create(10, 1, 1, true); + + private final ZeroDateOption zeroDateOption; + + @Nullable + private final Path localInfilePath; + + private final int localInfileBufferSize; + + private final boolean tinyInt1isBit; + + private final boolean preserveInstants; + + private int connectionId = -1; + + private ServerVersion serverVersion = NONE_VERSION; + + private Capability capability = Capability.DEFAULT; + + private PrepareCache prepareCache; + + @Nullable + private ZoneId timeZone; + + private String product = "Unknown"; + + /** + * Current isolation level inferred by past statements. + *

+ * Inference rules: + *

  1. In the beginning, it is also {@link #sessionIsolationLevel}.
  2. + *
  3. A transaction has began with a {@link IsolationLevel}, it will be changed to the value
  4. + *
  5. The transaction end (commit or rollback), it will recover to {@link #sessionIsolationLevel}.
+ */ + private volatile IsolationLevel currentIsolationLevel; + + /** + * Session isolation level. + * + *
  1. It is applied to all subsequent transactions performed within the current session.
  2. + *
  3. Calls {@link io.r2dbc.spi.Connection#setTransactionIsolationLevel}, it will change to the value.
  4. + *
  5. It can be changed within transactions, but does not affect the current ongoing transaction.
+ */ + private volatile IsolationLevel sessionIsolationLevel; + + private boolean lockWaitTimeoutSupported = false; + + /** + * Current lock wait timeout in seconds. + */ + private volatile Duration currentLockWaitTimeout; + + /** + * Session lock wait timeout in seconds. + */ + private volatile Duration sessionLockWaitTimeout; + + /** + * Assume that the auto commit is always turned on, it will be set after handshake V10 request message, or OK + * message which means handshake V9 completed. + */ + private volatile short serverStatuses = ServerStatuses.AUTO_COMMIT; + + ConnectionContext( + ZeroDateOption zeroDateOption, + @Nullable Path localInfilePath, + int localInfileBufferSize, + boolean tinyInt1isBit, + boolean preserveInstants, + @Nullable ZoneId timeZone + ) { + this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null"); + this.localInfilePath = localInfilePath; + this.localInfileBufferSize = localInfileBufferSize; + this.tinyInt1isBit = tinyInt1isBit; + this.preserveInstants = preserveInstants; + this.timeZone = timeZone; + } + + /** + * Initializes handshake information after connection is established. + * + * @param connectionId the connection identifier that is specified by server. + * @param version the server version. + * @param capability the connection capabilities. + */ + void initHandshake(int connectionId, ServerVersion version, Capability capability) { + this.connectionId = connectionId; + this.serverVersion = version; + this.capability = capability; + } + + /** + * Initializes session information after logged-in. + * + * @param prepareCache the prepare cache. + * @param isolationLevel the session isolation level. + * @param lockWaitTimeoutSupported if the server supports lock wait timeout. + * @param lockWaitTimeout the lock wait timeout. + * @param product the server product name. + * @param timeZone the server timezone. + */ + void initSession( + PrepareCache prepareCache, + IsolationLevel isolationLevel, + boolean lockWaitTimeoutSupported, + Duration lockWaitTimeout, + @Nullable String product, + @Nullable ZoneId timeZone + ) { + this.prepareCache = prepareCache; + this.currentIsolationLevel = this.sessionIsolationLevel = isolationLevel; + this.lockWaitTimeoutSupported = lockWaitTimeoutSupported; + this.currentLockWaitTimeout = this.sessionLockWaitTimeout = lockWaitTimeout; + this.product = product == null ? "Unknown" : product; + + if (timeZone != null) { + if (isTimeZoneInitialized()) { + throw new IllegalStateException("Connection timezone have been initialized"); + } + this.timeZone = timeZone; + } + } + + /** + * Get the connection identifier that is specified by server. + * + * @return the connection identifier. + */ + public int getConnectionId() { + return connectionId; + } + + @Override + public ServerVersion getServerVersion() { + return serverVersion; + } + + public Capability getCapability() { + return capability; + } + + @Override + public CharCollation getClientCollation() { + return CharCollation.clientCharCollation(); + } + + @Override + public boolean isPreserveInstants() { + return preserveInstants; + } + + @Override + public ZoneId getTimeZone() { + if (timeZone == null) { + throw new IllegalStateException("Server timezone have not initialization"); + } + return timeZone; + } + + String getProduct() { + return product; + } + + PrepareCache getPrepareCache() { + return prepareCache; + } + + boolean isTimeZoneInitialized() { + return timeZone != null; + } + + @Override + public boolean isMariaDb() { + Capability capability = this.capability; + return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb(); + } + + @Override + public boolean isTinyInt1isBit() { + return tinyInt1isBit; + } + + public boolean isNoBackslashEscapes() { + return (serverStatuses & ServerStatuses.NO_BACKSLASH_ESCAPES) != 0; + } + + @Override + public ZeroDateOption getZeroDateOption() { + return zeroDateOption; + } + + /** + * Gets the allowed local infile path. + * + * @return the path. + */ + @Nullable + public Path getLocalInfilePath() { + return localInfilePath; + } + + /** + * Gets the local infile buffer size. + * + * @return the buffer size. + */ + public int getLocalInfileBufferSize() { + return localInfileBufferSize; + } + + /** + * Checks if the server supports InnoDB lock wait timeout. + * + * @return if the server supports InnoDB lock wait timeout. + */ + public boolean isLockWaitTimeoutSupported() { + return lockWaitTimeoutSupported; + } + + /** + * Checks if the server supports statement timeout. + * + * @return if the server supports statement timeout. + */ + public boolean isStatementTimeoutSupported() { + boolean isMariaDb = isMariaDb(); + return (isMariaDb && serverVersion.isGreaterThanOrEqualTo(MARIA_10_1_1)) || + (!isMariaDb && serverVersion.isGreaterThanOrEqualTo(MYSQL_5_7_4)); + } + + /** + * Get the bitmap of server statuses. + * + * @return the bitmap. + */ + public short getServerStatuses() { + return serverStatuses; + } + + /** + * Updates server statuses. + * + * @param serverStatuses the bitmap of server statuses. + */ + public void setServerStatuses(short serverStatuses) { + this.serverStatuses = serverStatuses; + } + + IsolationLevel getCurrentIsolationLevel() { + return currentIsolationLevel; + } + + void setCurrentIsolationLevel(IsolationLevel isolationLevel) { + this.currentIsolationLevel = isolationLevel; + } + + void resetCurrentIsolationLevel() { + this.currentIsolationLevel = this.sessionIsolationLevel; + } + + IsolationLevel getSessionIsolationLevel() { + return sessionIsolationLevel; + } + + void setSessionIsolationLevel(IsolationLevel isolationLevel) { + this.sessionIsolationLevel = isolationLevel; + } + + void setCurrentLockWaitTimeout(Duration timeoutSeconds) { + this.currentLockWaitTimeout = timeoutSeconds; + } + + void resetCurrentLockWaitTimeout() { + this.currentLockWaitTimeout = this.sessionLockWaitTimeout; + } + + boolean isLockWaitTimeoutChanged() { + return currentLockWaitTimeout != sessionLockWaitTimeout; + } + + Duration getSessionLockWaitTimeout() { + return sessionLockWaitTimeout; + } + + void setAllLockWaitTimeout(Duration timeoutSeconds) { + this.currentLockWaitTimeout = this.sessionLockWaitTimeout = timeoutSeconds; + } + + boolean isInTransaction() { + return (serverStatuses & ServerStatuses.IN_TRANSACTION) != 0; + } + + boolean isAutoCommit() { + // Within transaction, autocommit remains disabled until end the transaction with COMMIT or ROLLBACK. + // The autocommit mode then reverts to its previous state. + short serverStatuses = this.serverStatuses; + return (serverStatuses & ServerStatuses.IN_TRANSACTION) == 0 && + (serverStatuses & ServerStatuses.AUTO_COMMIT) != 0; + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java similarity index 73% rename from src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java index 716b65a37..a501d1887 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 asyncer.io projects + * Copyright 2024 asyncer.io projects * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,12 @@ /** * The engine of {@code START TRANSACTION WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar * syntax. + * + * @deprecated since 1.1.3, use directly {@link String} instead, e.g. {@code "ROCKSDB"} + * @see io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition#consistent(String) + * @see io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition#consistent(String, long) */ +@Deprecated public enum ConsistentSnapshotEngine { ROCKSDB, diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Extensions.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Extensions.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/Extensions.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Extensions.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java new file mode 100644 index 000000000..a7c13c596 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java @@ -0,0 +1,754 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.authentication.MySqlAuthProvider; +import io.asyncer.r2dbc.mysql.cache.Caches; +import io.asyncer.r2dbc.mysql.cache.PrepareCache; +import io.asyncer.r2dbc.mysql.client.Client; +import io.asyncer.r2dbc.mysql.client.FluxExchangeable; +import io.asyncer.r2dbc.mysql.codec.Codecs; +import io.asyncer.r2dbc.mysql.codec.CodecsBuilder; +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; +import io.asyncer.r2dbc.mysql.constant.SslMode; +import io.asyncer.r2dbc.mysql.extension.CodecRegistrar; +import io.asyncer.r2dbc.mysql.internal.util.StringUtils; +import io.asyncer.r2dbc.mysql.message.client.AuthResponse; +import io.asyncer.r2dbc.mysql.message.client.ClientMessage; +import io.asyncer.r2dbc.mysql.message.client.HandshakeResponse; +import io.asyncer.r2dbc.mysql.message.client.InitDbMessage; +import io.asyncer.r2dbc.mysql.message.client.SslRequest; +import io.asyncer.r2dbc.mysql.message.client.SubsequenceClientMessage; +import io.asyncer.r2dbc.mysql.message.server.AuthMoreDataMessage; +import io.asyncer.r2dbc.mysql.message.server.ChangeAuthMessage; +import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; +import io.asyncer.r2dbc.mysql.message.server.ErrorMessage; +import io.asyncer.r2dbc.mysql.message.server.HandshakeHeader; +import io.asyncer.r2dbc.mysql.message.server.HandshakeRequest; +import io.asyncer.r2dbc.mysql.message.server.OkMessage; +import io.asyncer.r2dbc.mysql.message.server.ServerMessage; +import io.asyncer.r2dbc.mysql.message.server.SyntheticSslResponseMessage; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.Readable; +import org.jetbrains.annotations.Nullable; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.core.publisher.SynchronousSink; +import reactor.util.concurrent.Queues; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.time.DateTimeException; +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * A message flow utility that can initializes the session of {@link Client}. + *

+ * It should not use server-side prepared statements, because {@link PrepareCache} will be initialized after the session + * is initialized. + */ +final class InitFlow { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(InitFlow.class); + + private static final ServerVersion MARIA_11_1_1 = ServerVersion.create(11, 1, 1, true); + + private static final ServerVersion MYSQL_8_0_3 = ServerVersion.create(8, 0, 3); + + private static final ServerVersion MYSQL_5_7_20 = ServerVersion.create(5, 7, 20); + + private static final ServerVersion MYSQL_8 = ServerVersion.create(8, 0, 0); + + private static final BiConsumer> INIT_DB = (message, sink) -> { + if (message instanceof ErrorMessage) { + ErrorMessage msg = (ErrorMessage) message; + logger.debug("Use database failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), msg.getMessage()); + sink.next(false); + sink.complete(); + } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { + sink.next(true); + sink.complete(); + } else { + ReferenceCountUtil.safeRelease(message); + } + }; + + private static final BiConsumer> INIT_DB_AFTER = (message, sink) -> { + if (message instanceof ErrorMessage) { + sink.error(((ErrorMessage) message).toException()); + } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { + sink.complete(); + } else { + ReferenceCountUtil.safeRelease(message); + } + }; + + /** + * Initializes handshake and login a {@link Client}. + * + * @param client the {@link Client} to exchange messages with. + * @param sslMode the {@link SslMode} defines SSL capability and behavior. + * @param database the database that will be connected. + * @param user the user that will be login. + * @param password the password of the {@code user}. + * @param compressionAlgorithms the list of compression algorithms. + * @param zstdCompressionLevel the zstd compression level. + * @return a {@link Mono} that indicates the initialization is done, or an error if the initialization failed. + */ + static Mono initHandshake(Client client, SslMode sslMode, String database, String user, + @Nullable CharSequence password, Set compressionAlgorithms, int zstdCompressionLevel) { + return client.exchange(new HandshakeExchangeable( + client, + sslMode, + database, + user, + password, + compressionAlgorithms, + zstdCompressionLevel + )).then(); + } + + /** + * Initializes the session and {@link Codecs} of a {@link Client}. + * + * @param client the client + * @param database the database to use after session initialization + * @param prepareCacheSize the size of prepare cache + * @param sessionVariables the session variables to set + * @param forceTimeZone if the timezone should be set to session + * @param lockWaitTimeout the lock wait timeout that should be set to session + * @param statementTimeout the statement timeout that should be set to session + * @return a {@link Mono} that indicates the {@link Codecs}, or an error if the initialization failed + */ + static Mono initSession( + Client client, + String database, + int prepareCacheSize, + List sessionVariables, + boolean forceTimeZone, + @Nullable Duration lockWaitTimeout, + @Nullable Duration statementTimeout, + Extensions extensions + ) { + return Mono.defer(() -> { + ByteBufAllocator allocator = client.getByteBufAllocator(); + CodecsBuilder builder = Codecs.builder(); + + extensions.forEach(CodecRegistrar.class, registrar -> + registrar.register(allocator, builder)); + + Codecs codecs = builder.build(); + + List variables = mergeSessionVariables(client, sessionVariables, forceTimeZone, statementTimeout); + + logger.debug("Initializing client session: {}", variables); + + return QueryFlow.setSessionVariables(client, variables) + .then(loadSessionVariables(client, codecs)) + .flatMap(data -> loadAndInitInnoDbEngineStatus(data, client, codecs, lockWaitTimeout)) + .flatMap(data -> { + ConnectionContext context = client.getContext(); + + logger.debug("Initializing connection {} context: {}", context.getConnectionId(), data); + context.initSession( + Caches.createPrepareCache(prepareCacheSize), + data.level, + data.lockWaitTimeoutSupported, + data.lockWaitTimeout, + data.product, + data.timeZone + ); + + if (!data.lockWaitTimeoutSupported) { + logger.info( + "Lock wait timeout is not supported by server, all related operations will be ignored"); + } + + return database.isEmpty() ? Mono.just(codecs) : + initDatabase(client, database).then(Mono.just(codecs)); + }); + }); + } + + private static Mono loadAndInitInnoDbEngineStatus( + SessionState data, + Client client, + Codecs codecs, + @Nullable Duration lockWaitTimeout + ) { + return new TextSimpleStatement( + client, + codecs, + "SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'" + ).execute().flatMap(r -> r.map(readable -> { + String value = readable.get(1, String.class); + + if (value == null || value.isEmpty()) { + return data; + } else { + return data.lockWaitTimeout(Duration.ofSeconds(Long.parseLong(value))); + } + })).single(data).flatMap(d -> { + if (lockWaitTimeout != null) { + // Do not use context.isLockWaitTimeoutSupported() here, because its session variable is not set + if (d.lockWaitTimeoutSupported) { + return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(lockWaitTimeout)) + .then(Mono.fromSupplier(() -> d.lockWaitTimeout(lockWaitTimeout))); + } + + logger.warn("Lock wait timeout is not supported by server, ignore initial setting"); + return Mono.just(d); + } + return Mono.just(d); + }); + } + + private static Mono loadSessionVariables(Client client, Codecs codecs) { + ConnectionContext context = client.getContext(); + StringBuilder query = new StringBuilder(128) + .append("SELECT ") + .append(transactionIsolationColumn(context)) + .append(",@@version_comment AS v"); + + Function> handler; + + if (context.isTimeZoneInitialized()) { + handler = r -> convertSessionData(r, false); + } else { + query.append(",@@system_time_zone AS s,@@time_zone AS t"); + handler = r -> convertSessionData(r, true); + } + + return new TextSimpleStatement(client, codecs, query.toString()) + .execute() + .flatMap(handler) + .last(); + } + + private static Mono initDatabase(Client client, String database) { + return client.exchange(new InitDbMessage(database), INIT_DB) + .last() + .flatMap(success -> { + if (success) { + return Mono.empty(); + } + + String sql = "CREATE DATABASE IF NOT EXISTS " + StringUtils.quoteIdentifier(database); + + return QueryFlow.executeVoid(client, sql) + .then(client.exchange(new InitDbMessage(database), INIT_DB_AFTER).then()); + }); + } + + private static List mergeSessionVariables( + Client client, + List sessionVariables, + boolean forceTimeZone, + @Nullable Duration statementTimeout + ) { + ConnectionContext context = client.getContext(); + + if ((!forceTimeZone || !context.isTimeZoneInitialized()) && statementTimeout == null) { + return sessionVariables; + } + + List variables = new ArrayList<>(sessionVariables.size() + 2); + + variables.addAll(sessionVariables); + + if (forceTimeZone && context.isTimeZoneInitialized()) { + variables.add(timeZoneVariable(context.getTimeZone())); + } + + if (statementTimeout != null) { + if (context.isStatementTimeoutSupported()) { + variables.add(StringUtils.statementTimeoutVariable(statementTimeout, context.isMariaDb())); + } else { + logger.warn("Statement timeout is not supported in {}, ignore initial setting", + context.getServerVersion()); + } + } + + return variables; + } + + private static String timeZoneVariable(ZoneId timeZone) { + String offerStr = timeZone instanceof ZoneOffset && "Z".equalsIgnoreCase(timeZone.getId()) ? + "+00:00" : timeZone.getId(); + + return "time_zone='" + offerStr + "'"; + } + + private static Flux convertSessionData(MySqlResult r, boolean timeZone) { + return r.map(readable -> { + IsolationLevel level = convertIsolationLevel(readable.get(0, String.class)); + String product = readable.get(1, String.class); + + return new SessionState(level, product, timeZone ? readZoneId(readable) : null); + }); + } + + /** + * Resolves the column of session isolation level, the {@literal @@tx_isolation} has been marked as deprecated. + *

+ * If server is MariaDB, {@literal @@transaction_isolation} is used starting from {@literal 11.1.1}. + *

+ * If the server is MySQL, use {@literal @@transaction_isolation} starting from {@literal 8.0.3}, or between + * {@literal 5.7.20} and {@literal 8.0.0} (exclusive). + */ + private static String transactionIsolationColumn(ConnectionContext context) { + ServerVersion version = context.getServerVersion(); + + if (context.isMariaDb()) { + return version.isGreaterThanOrEqualTo(MARIA_11_1_1) ? "@@transaction_isolation AS i" : + "@@tx_isolation AS i"; + } + + return version.isGreaterThanOrEqualTo(MYSQL_8_0_3) || + (version.isGreaterThanOrEqualTo(MYSQL_5_7_20) && version.isLessThan(MYSQL_8)) ? + "@@transaction_isolation AS i" : "@@tx_isolation AS i"; + } + + private static ZoneId readZoneId(Readable readable) { + String systemTimeZone = readable.get(2, String.class); + String timeZone = readable.get(3, String.class); + + if (timeZone == null || timeZone.isEmpty() || "SYSTEM".equalsIgnoreCase(timeZone)) { + if (systemTimeZone == null || systemTimeZone.isEmpty()) { + logger.warn("MySQL does not return any timezone, trying to use system default timezone"); + return ZoneId.systemDefault().normalized(); + } else { + return convertZoneId(systemTimeZone); + } + } else { + return convertZoneId(timeZone); + } + } + + private static ZoneId convertZoneId(String id) { + try { + return StringUtils.parseZoneId(id); + } catch (DateTimeException e) { + logger.warn("The server timezone is unknown <{}>, trying to use system default timezone", id, e); + + return ZoneId.systemDefault().normalized(); + } + } + + private static IsolationLevel convertIsolationLevel(@Nullable String name) { + if (name == null) { + logger.warn("Isolation level is null in current session, fallback to repeatable read"); + + return IsolationLevel.REPEATABLE_READ; + } + + switch (name) { + case "READ-UNCOMMITTED": + return IsolationLevel.READ_UNCOMMITTED; + case "READ-COMMITTED": + return IsolationLevel.READ_COMMITTED; + case "REPEATABLE-READ": + return IsolationLevel.REPEATABLE_READ; + case "SERIALIZABLE": + return IsolationLevel.SERIALIZABLE; + } + + logger.warn("Unknown isolation level {} in current session, fallback to repeatable read", name); + + return IsolationLevel.REPEATABLE_READ; + } + + private InitFlow() { + } + + private static final class SessionState { + + private final IsolationLevel level; + + @Nullable + private final String product; + + @Nullable + private final ZoneId timeZone; + + private final Duration lockWaitTimeout; + + private final boolean lockWaitTimeoutSupported; + + SessionState(IsolationLevel level, @Nullable String product, @Nullable ZoneId timeZone) { + this(level, product, timeZone, Duration.ZERO, false); + } + + private SessionState( + IsolationLevel level, + @Nullable String product, + @Nullable ZoneId timeZone, + Duration lockWaitTimeout, + boolean lockWaitTimeoutSupported + ) { + this.level = level; + this.product = product; + this.timeZone = timeZone; + this.lockWaitTimeout = lockWaitTimeout; + this.lockWaitTimeoutSupported = lockWaitTimeoutSupported; + } + + SessionState lockWaitTimeout(Duration timeout) { + return new SessionState(level, product, timeZone, timeout, true); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SessionState)) { + return false; + } + + SessionState that = (SessionState) o; + + return lockWaitTimeoutSupported == that.lockWaitTimeoutSupported && + level.equals(that.level) && + Objects.equals(product, that.product) && + Objects.equals(timeZone, that.timeZone) && + lockWaitTimeout.equals(that.lockWaitTimeout); + } + + @Override + public int hashCode() { + int result = level.hashCode(); + result = 31 * result + (product != null ? product.hashCode() : 0); + result = 31 * result + (timeZone != null ? timeZone.hashCode() : 0); + result = 31 * result + lockWaitTimeout.hashCode(); + return 31 * result + (lockWaitTimeoutSupported ? 1 : 0); + } + + @Override + public String toString() { + return "SessionState{level=" + level + + ", product='" + product + + "', timeZone=" + timeZone + + ", lockWaitTimeout=" + lockWaitTimeout + + ", lockWaitTimeoutSupported=" + lockWaitTimeoutSupported + + '}'; + } + } +} + +/** + * An implementation of {@link FluxExchangeable} that considers login to the database. + *

+ * Not like other {@link FluxExchangeable}s, it is started by a server-side message, which should be an implementation + * of {@link HandshakeRequest}. + */ +final class HandshakeExchangeable extends FluxExchangeable { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(HandshakeExchangeable.class); + + private static final Map ATTRIBUTES = Collections.emptyMap(); + + private static final String CLI_SPECIFIC = "HY000"; + + private static final int HANDSHAKE_VERSION = 10; + + private final Sinks.Many requests = Sinks.many().unicast() + .onBackpressureBuffer(Queues.one().get()); + + private final Client client; + + private final SslMode sslMode; + + private final String database; + + private final String user; + + @Nullable + private final CharSequence password; + + private final Set compressions; + + private final int zstdCompressionLevel; + + private boolean handshake = true; + + private MySqlAuthProvider authProvider; + + private byte[] salt; + + private boolean sslCompleted; + + HandshakeExchangeable(Client client, SslMode sslMode, String database, String user, + @Nullable CharSequence password, Set compressions, + int zstdCompressionLevel) { + this.client = client; + this.sslMode = sslMode; + this.database = database; + this.user = user; + this.password = password; + this.compressions = compressions; + this.zstdCompressionLevel = zstdCompressionLevel; + this.sslCompleted = sslMode == SslMode.TUNNEL; + } + + @Override + public void subscribe(CoreSubscriber actual) { + requests.asFlux().subscribe(actual); + } + + @Override + public void accept(ServerMessage message, SynchronousSink sink) { + if (message instanceof ErrorMessage) { + sink.error(((ErrorMessage) message).toException()); + return; + } + + // Ensures it will be initialized only once. + if (handshake) { + handshake = false; + if (message instanceof HandshakeRequest) { + HandshakeRequest request = (HandshakeRequest) message; + Capability capability = initHandshake(request); + + if (capability.isSslEnabled()) { + emitNext(SslRequest.from(capability, client.getContext().getClientCollation().getId()), sink); + } else { + emitNext(createHandshakeResponse(capability), sink); + } + } else { + sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + + message.getClass().getSimpleName() + "' in init phase")); + } + + return; + } + + if (message instanceof OkMessage) { + logger.trace("Connection (id {}) login success", client.getContext().getConnectionId()); + client.loginSuccess(); + sink.complete(); + } else if (message instanceof SyntheticSslResponseMessage) { + sslCompleted = true; + emitNext(createHandshakeResponse(client.getContext().getCapability()), sink); + } else if (message instanceof AuthMoreDataMessage) { + AuthMoreDataMessage msg = (AuthMoreDataMessage) message; + + if (msg.isFailed()) { + if (logger.isDebugEnabled()) { + logger.debug("Connection (id {}) fast authentication failed, use full authentication", + client.getContext().getConnectionId()); + } + + emitNext(createAuthResponse("full authentication"), sink); + } + // Otherwise success, wait until OK message or Error message. + } else if (message instanceof ChangeAuthMessage) { + ChangeAuthMessage msg = (ChangeAuthMessage) message; + + authProvider = MySqlAuthProvider.build(msg.getAuthType()); + salt = msg.getSalt(); + emitNext(createAuthResponse("change authentication"), sink); + } else { + sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + + message.getClass().getSimpleName() + "' in login phase")); + } + } + + @Override + public void dispose() { + // No particular error condition handling for complete signal. + this.requests.tryEmitComplete(); + } + + private void emitNext(SubsequenceClientMessage message, SynchronousSink sink) { + Sinks.EmitResult result = requests.tryEmitNext(message); + + if (result != Sinks.EmitResult.OK) { + sink.error(new IllegalStateException("Fail to emit a login request due to " + result)); + } + } + + private AuthResponse createAuthResponse(String phase) { + MySqlAuthProvider authProvider = getAndNextProvider(); + + if (authProvider.isSslNecessary() && !sslCompleted) { + throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), phase), CLI_SPECIFIC); + } + + return new AuthResponse(authProvider.authentication(password, salt, client.getContext().getClientCollation())); + } + + private Capability clientCapability(Capability serverCapability) { + Capability.Builder builder = serverCapability.mutate(); + + builder.disableSessionTrack(); + builder.disableDatabasePinned(); + builder.disableIgnoreAmbiguitySpace(); + builder.disableInteractiveTimeout(); + + if (sslMode == SslMode.TUNNEL) { + // Tunnel does not use MySQL SSL protocol, disable it. + builder.disableSsl(); + } else if (!serverCapability.isSslEnabled()) { + // Server unsupported SSL. + if (sslMode.requireSsl()) { + // Before handshake, Client.context does not be initialized + throw new R2dbcPermissionDeniedException("Server does not support SSL but mode '" + sslMode + + "' requires SSL", CLI_SPECIFIC); + } else if (sslMode.startSsl()) { + // SSL has start yet, and client can disable SSL, disable now. + client.sslUnsupported(); + } + } else { + // The server supports SSL, but the user does not want to use SSL, disable it. + if (!sslMode.startSsl()) { + builder.disableSsl(); + } + } + + if (isZstdAllowed(serverCapability)) { + if (isZstdSupported()) { + builder.disableZlibCompression(); + } else { + logger.warn("Server supports zstd, but zstd-jni dependency is missing"); + + if (isZlibAllowed(serverCapability)) { + builder.disableZstdCompression(); + } else if (compressions.contains(CompressionAlgorithm.UNCOMPRESSED)) { + builder.disableCompression(); + } else { + throw new R2dbcNonTransientResourceException( + "Environment does not support a compression algorithm in " + compressions + + ", config does not allow uncompressed mode", CLI_SPECIFIC); + } + } + } else if (isZlibAllowed(serverCapability)) { + builder.disableZstdCompression(); + } else if (compressions.contains(CompressionAlgorithm.UNCOMPRESSED)) { + builder.disableCompression(); + } else { + throw new R2dbcPermissionDeniedException( + "Environment does not support a compression algorithm in " + compressions + + ", config does not allow uncompressed mode", CLI_SPECIFIC); + } + + if (database.isEmpty()) { + builder.disableConnectWithDatabase(); + } + + if (client.getContext().getLocalInfilePath() == null) { + builder.disableLoadDataLocalInfile(); + } + + if (ATTRIBUTES.isEmpty()) { + builder.disableConnectAttributes(); + } + + return builder.build(); + } + + private Capability initHandshake(HandshakeRequest message) { + HandshakeHeader header = message.getHeader(); + int handshakeVersion = header.getProtocolVersion(); + ServerVersion serverVersion = header.getServerVersion(); + + if (handshakeVersion < HANDSHAKE_VERSION) { + logger.warn("MySQL use handshake V{}, server version is {}, maybe most features are unavailable", + handshakeVersion, serverVersion); + } + + Capability capability = clientCapability(message.getServerCapability()); + + // No need initialize server statuses because it has initialized by read filter. + this.client.getContext().initHandshake(header.getConnectionId(), serverVersion, capability); + this.authProvider = MySqlAuthProvider.build(message.getAuthType()); + this.salt = message.getSalt(); + + return capability; + } + + private MySqlAuthProvider getAndNextProvider() { + MySqlAuthProvider authProvider = this.authProvider; + this.authProvider = authProvider.next(); + return authProvider; + } + + private HandshakeResponse createHandshakeResponse(Capability capability) { + MySqlAuthProvider authProvider = getAndNextProvider(); + + if (authProvider.isSslNecessary() && !sslCompleted) { + throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), "handshake"), + CLI_SPECIFIC); + } + + byte[] authorization = authProvider.authentication(password, salt, client.getContext().getClientCollation()); + String authType = authProvider.getType(); + + if (MySqlAuthProvider.NO_AUTH_PROVIDER.equals(authType)) { + // Authentication type is not matter because of it has no authentication type. + // Server need send a Change Authentication Message after handshake response. + authType = MySqlAuthProvider.CACHING_SHA2_PASSWORD; + } + + return HandshakeResponse.from(capability, client.getContext().getClientCollation().getId(), user, authorization, + authType, database, ATTRIBUTES, zstdCompressionLevel); + } + + private boolean isZstdAllowed(Capability capability) { + return capability.isZstdCompression() && compressions.contains(CompressionAlgorithm.ZSTD); + } + + private boolean isZlibAllowed(Capability capability) { + return capability.isZlibCompression() && compressions.contains(CompressionAlgorithm.ZLIB); + } + + private static String authFails(String authType, String phase) { + return "Authentication type '" + authType + "' must require SSL in " + phase + " phase"; + } + + private static boolean isZstdSupported() { + try { + ClassLoader loader = AccessController.doPrivileged((PrivilegedAction) () -> { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + return cl == null ? ClassLoader.getSystemClassLoader() : cl; + }); + Class.forName("com.github.luben.zstd.Zstd", false, loader); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java similarity index 71% rename from src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java index eab13e88c..bba48841c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java @@ -16,13 +16,20 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlRow; +import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.codec.CodecContext; import io.asyncer.r2dbc.mysql.codec.Codecs; +import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Nullability; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; +import java.lang.reflect.ParameterizedType; import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; @@ -37,7 +44,7 @@ * * @see MySqlStatement#returnGeneratedValues(String...) reading last inserted ID. */ -final class InsertSyntheticRow implements Row, RowMetadata, ColumnMetadata { +final class InsertSyntheticRow implements MySqlRow, MySqlRowMetadata, MySqlColumnMetadata { private final Codecs codecs; @@ -45,15 +52,11 @@ final class InsertSyntheticRow implements Row, RowMetadata, ColumnMetadata { private final long lastInsertId; - private final ColumnNameSet nameSet; - InsertSyntheticRow(Codecs codecs, String keyName, long lastInsertId) { this.codecs = requireNonNull(codecs, "codecs must not be null"); this.keyName = requireNonNull(keyName, "keyName must not be null"); // lastInsertId may be negative if key is BIGINT UNSIGNED and value overflow than signed int64. this.lastInsertId = lastInsertId; - // Singleton name must be sorted. - this.nameSet = ColumnNameSet.of(keyName); } @Override @@ -96,19 +99,19 @@ public boolean contains(String name) { } @Override - public RowMetadata getMetadata() { + public MySqlRowMetadata getMetadata() { return this; } @Override - public ColumnMetadata getColumnMetadata(int index) { + public MySqlColumnMetadata getColumnMetadata(int index) { assertValidIndex(index); return this; } @Override - public ColumnMetadata getColumnMetadata(String name) { + public MySqlColumnMetadata getColumnMetadata(String name) { requireNonNull(name, "name must not be null"); assertValidName(name); @@ -116,7 +119,7 @@ public ColumnMetadata getColumnMetadata(String name) { } @Override - public List getColumnMetadatas() { + public List getColumnMetadatas() { return Collections.singletonList(this); } @@ -125,6 +128,11 @@ public MySqlType getType() { return lastInsertId < 0 ? MySqlType.BIGINT_UNSIGNED : MySqlType.BIGINT; } + @Override + public CharCollation getCharCollation(CodecContext context) { + return context.getClientCollation(); + } + @Override public String getName() { return keyName; @@ -140,14 +148,26 @@ public Nullability getNullability() { return Nullability.NON_NULL; } - private void assertValidName(String name) { - if (!contains0(name)) { - throw new NoSuchElementException("Column name '" + name + "' does not exist in " + this.nameSet); - } + @Override + public T get(int index, ParameterizedType type) { + throw new IllegalArgumentException(String.format("Cannot decode %s with last inserted ID %s", type, + lastInsertId < 0 ? Long.toUnsignedString(lastInsertId) : lastInsertId)); } - private boolean contains0(String name) { - return nameSet.contains(name); + @Override + public T get(String name, ParameterizedType type) { + throw new IllegalArgumentException(String.format("Cannot decode %s with last inserted ID %s", type, + lastInsertId < 0 ? Long.toUnsignedString(lastInsertId) : lastInsertId)); + } + + private boolean contains0(final String name) { + return keyName.equalsIgnoreCase(name); + } + + private void assertValidName(final String name) { + if (!contains0(name)) { + throw new NoSuchElementException("Column name '" + name + "' does not exist in {" + name + '}'); + } } private T get0(Class type) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java similarity index 87% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java index 6d74cf4d0..a71c31986 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java @@ -16,6 +16,8 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlBatch; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import reactor.core.publisher.Flux; @@ -28,20 +30,17 @@ * An implementation of {@link MySqlBatch} for executing a collection of statements in a batch against the * MySQL database. */ -final class MySqlBatchingBatch extends MySqlBatch { +final class MySqlBatchingBatch implements MySqlBatch { private final Client client; private final Codecs codecs; - private final ConnectionContext context; - private final StringJoiner queries = new StringJoiner(";"); - MySqlBatchingBatch(Client client, Codecs codecs, ConnectionContext context) { + MySqlBatchingBatch(Client client, Codecs codecs) { this.client = requireNonNull(client, "client must not be null"); this.codecs = requireNonNull(codecs, "codecs must not be null"); - this.context = requireNonNull(context, "context must not be null"); } @Override @@ -63,7 +62,7 @@ public MySqlBatch add(String sql) { @Override public Flux execute() { return QueryFlow.execute(client, getSql()) - .map(messages -> MySqlResult.toResult(false, codecs, context, null, messages)); + .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, null, messages)); } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java similarity index 60% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java index af40495f5..61cb1d0b8 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java @@ -16,32 +16,32 @@ package io.asyncer.r2dbc.mysql; -import io.r2dbc.spi.ConnectionMetadata; -import org.jetbrains.annotations.Nullable; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; +import io.asyncer.r2dbc.mysql.api.MySqlConnectionMetadata; +import io.asyncer.r2dbc.mysql.client.Client; /** * Connection metadata for a connection connected to MySQL database. */ -public final class MySqlConnectionMetadata implements ConnectionMetadata { - - private final String version; +final class MySqlClientConnectionMetadata implements MySqlConnectionMetadata { - private final String product; + private final Client client; - MySqlConnectionMetadata(String version, @Nullable String product) { - this.version = requireNonNull(version, "version must not be null"); - this.product = product == null ? "Unknown" : product; + MySqlClientConnectionMetadata(Client client) { + this.client = client; } @Override public String getDatabaseVersion() { - return version; + return client.getContext().getServerVersion().toString(); + } + + @Override + public boolean isMariaDb() { + return client.getContext().isMariaDb(); } @Override public String getDatabaseProductName() { - return product; + return client.getContext().getProduct(); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java similarity index 83% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java index 4bc9aaca4..8f8d2e9e0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java @@ -16,11 +16,14 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlNativeTypeMetadata; import io.asyncer.r2dbc.mysql.codec.CodecContext; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.message.server.DefinitionMetadataMessage; import io.r2dbc.spi.Nullability; +import org.jetbrains.annotations.VisibleForTesting; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; @@ -48,28 +51,29 @@ final class MySqlColumnDescriptor implements MySqlColumnMetadata { private final int collationId; - private MySqlColumnDescriptor(int index, short typeId, String name, ColumnDefinition definition, + @VisibleForTesting + MySqlColumnDescriptor(int index, short typeId, String name, int definitions, long size, int decimals, int collationId) { require(index >= 0, "index must not be a negative integer"); require(size >= 0, "size must not be a negative integer"); require(decimals >= 0, "decimals must not be a negative integer"); requireNonNull(name, "name must not be null"); - require(collationId > 0, "collationId must be a positive integer"); - requireNonNull(definition, "definition must not be null"); + + MySqlTypeMetadata typeMetadata = new MySqlTypeMetadata(typeId, definitions, collationId); this.index = index; - this.typeMetadata = new MySqlTypeMetadata(typeId, definition); - this.type = MySqlType.of(typeId, definition); + this.typeMetadata = typeMetadata; + this.type = MySqlType.of(typeMetadata); this.name = name; - this.nullability = definition.isNotNull() ? Nullability.NON_NULL : Nullability.NULLABLE; + this.nullability = typeMetadata.isNotNull() ? Nullability.NON_NULL : Nullability.NULLABLE; this.size = size; this.decimals = decimals; this.collationId = collationId; } static MySqlColumnDescriptor create(int index, DefinitionMetadataMessage message) { - ColumnDefinition definition = message.getDefinition(); - return new MySqlColumnDescriptor(index, message.getTypeId(), message.getColumn(), definition, + int definitions = message.getDefinitions(); + return new MySqlColumnDescriptor(index, message.getTypeId(), message.getColumn(), definitions, message.getSize(), message.getDecimals(), message.getCollationId()); } @@ -88,7 +92,7 @@ public String getName() { } @Override - public MySqlTypeMetadata getNativeTypeMetadata() { + public MySqlNativeTypeMetadata getNativeTypeMetadata() { return typeMetadata; } @@ -99,14 +103,13 @@ public Nullability getNullability() { @Override public Integer getPrecision() { + // FIXME: NEW_DECIMAL and DECIMAL are "exact" fixed-point number. + // So the `size` have to subtract: + // 1. if signed, 1 byte for the sign + // 2. if decimals > 0, 1 byte for the dot return (int) size; } - @Override - public long getNativePrecision() { - return size; - } - @Override public Integer getScale() { // 0x00 means it is an integer or a static string. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java similarity index 57% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 0eec8645c..39fb91eb6 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -16,12 +16,18 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; import io.asyncer.r2dbc.mysql.extension.Extension; +import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.resolver.AddressResolverGroup; import org.jetbrains.annotations.Nullable; import org.reactivestreams.Publisher; +import reactor.netty.internal.util.Metrics; +import reactor.netty.resources.LoopResources; +import reactor.netty.tcp.TcpResources; import javax.net.ssl.HostnameVerifier; import java.net.Socket; @@ -30,18 +36,22 @@ import java.time.Duration; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; import java.util.List; import java.util.Objects; import java.util.ServiceLoader; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_STRINGS; /** - * MySQL configuration of connection. + * A configuration of MySQL connection. */ public final class MySqlConnectionConfiguration { @@ -71,8 +81,11 @@ public final class MySqlConnectionConfiguration { @Nullable private final Duration connectTimeout; - @Nullable - private final ZoneId serverZoneId; + private final boolean preserveInstants; + + private final String connectionTimeZone; + + private final boolean forceConnectionTimeZoneToSession; private final ZeroDateOption zeroDateOption; @@ -88,6 +101,14 @@ public final class MySqlConnectionConfiguration { @Nullable private final Predicate preferPrepareStatement; + private final List sessionVariables; + + @Nullable + private final Duration lockWaitTimeout; + + @Nullable + private final Duration statementTimeout; + @Nullable private final Path loadLocalInfilePath; @@ -97,21 +118,42 @@ public final class MySqlConnectionConfiguration { private final int prepareCacheSize; + private final Set compressionAlgorithms; + + private final int zstdCompressionLevel; + + private final LoopResources loopResources; + private final Extensions extensions; @Nullable private final Publisher passwordPublisher; + @Nullable + private final AddressResolverGroup resolver; + + private final boolean metrics; + + private final boolean tinyInt1isBit; + private MySqlConnectionConfiguration( - boolean isHost, String domain, int port, MySqlSslConfiguration ssl, - boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout, - ZeroDateOption zeroDateOption, @Nullable ZoneId serverZoneId, - String user, @Nullable CharSequence password, @Nullable String database, - boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement, - @Nullable Path loadLocalInfilePath, int localInfileBufferSize, - int queryCacheSize, int prepareCacheSize, Extensions extensions, - @Nullable Publisher passwordPublisher - ) { + boolean isHost, String domain, int port, MySqlSslConfiguration ssl, + boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout, + ZeroDateOption zeroDateOption, + boolean preserveInstants, + String connectionTimeZone, + boolean forceConnectionTimeZoneToSession, + String user, @Nullable CharSequence password, @Nullable String database, + boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement, + List sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout, + @Nullable Path loadLocalInfilePath, int localInfileBufferSize, + int queryCacheSize, int prepareCacheSize, + Set compressionAlgorithms, int zstdCompressionLevel, + @Nullable LoopResources loopResources, + Extensions extensions, @Nullable Publisher passwordPublisher, + @Nullable AddressResolverGroup resolver, + boolean metrics, + boolean tinyInt1isBit) { this.isHost = isHost; this.domain = domain; this.port = port; @@ -119,19 +161,30 @@ private MySqlConnectionConfiguration( this.tcpNoDelay = tcpNoDelay; this.connectTimeout = connectTimeout; this.ssl = ssl; - this.serverZoneId = serverZoneId; + this.preserveInstants = preserveInstants; + this.connectionTimeZone = requireNonNull(connectionTimeZone, "connectionTimeZone must not be null"); + this.forceConnectionTimeZoneToSession = forceConnectionTimeZoneToSession; this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null"); this.user = requireNonNull(user, "user must not be null"); this.password = password; this.database = database == null || database.isEmpty() ? "" : database; this.createDatabaseIfNotExist = createDatabaseIfNotExist; this.preferPrepareStatement = preferPrepareStatement; + this.sessionVariables = sessionVariables; + this.lockWaitTimeout = lockWaitTimeout; + this.statementTimeout = statementTimeout; this.loadLocalInfilePath = loadLocalInfilePath; this.localInfileBufferSize = localInfileBufferSize; this.queryCacheSize = queryCacheSize; this.prepareCacheSize = prepareCacheSize; + this.compressionAlgorithms = compressionAlgorithms; + this.zstdCompressionLevel = zstdCompressionLevel; + this.loopResources = loopResources == null ? TcpResources.get() : loopResources; this.extensions = extensions; this.passwordPublisher = passwordPublisher; + this.resolver = resolver; + this.metrics = metrics; + this.tinyInt1isBit = tinyInt1isBit; } /** @@ -176,9 +229,16 @@ ZeroDateOption getZeroDateOption() { return zeroDateOption; } - @Nullable - ZoneId getServerZoneId() { - return serverZoneId; + boolean isPreserveInstants() { + return preserveInstants; + } + + String getConnectionTimeZone() { + return connectionTimeZone; + } + + boolean isForceConnectionTimeZoneToSession() { + return forceConnectionTimeZoneToSession; } String getUser() { @@ -203,6 +263,20 @@ Predicate getPreferPrepareStatement() { return preferPrepareStatement; } + List getSessionVariables() { + return sessionVariables; + } + + @Nullable + Duration getLockWaitTimeout() { + return lockWaitTimeout; + } + + @Nullable + Duration getStatementTimeout() { + return statementTimeout; + } + @Nullable Path getLoadLocalInfilePath() { return loadLocalInfilePath; @@ -220,6 +294,18 @@ int getPrepareCacheSize() { return prepareCacheSize; } + Set getCompressionAlgorithms() { + return compressionAlgorithms; + } + + int getZstdCompressionLevel() { + return zstdCompressionLevel; + } + + LoopResources getLoopResources() { + return loopResources; + } + Extensions getExtensions() { return extensions; } @@ -229,6 +315,19 @@ Publisher getPasswordPublisher() { return passwordPublisher; } + @Nullable + AddressResolverGroup getResolver() { + return resolver; + } + + boolean isMetrics() { + return metrics; + } + + boolean isTinyInt1isBit() { + return tinyInt1isBit; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -245,54 +344,81 @@ public boolean equals(Object o) { tcpKeepAlive == that.tcpKeepAlive && tcpNoDelay == that.tcpNoDelay && Objects.equals(connectTimeout, that.connectTimeout) && - Objects.equals(serverZoneId, that.serverZoneId) && + preserveInstants == that.preserveInstants && + Objects.equals(connectionTimeZone, that.connectionTimeZone) && + forceConnectionTimeZoneToSession == that.forceConnectionTimeZoneToSession && zeroDateOption == that.zeroDateOption && user.equals(that.user) && Objects.equals(password, that.password) && database.equals(that.database) && createDatabaseIfNotExist == that.createDatabaseIfNotExist && Objects.equals(preferPrepareStatement, that.preferPrepareStatement) && + sessionVariables.equals(that.sessionVariables) && + Objects.equals(lockWaitTimeout, that.lockWaitTimeout) && + Objects.equals(statementTimeout, that.statementTimeout) && Objects.equals(loadLocalInfilePath, that.loadLocalInfilePath) && localInfileBufferSize == that.localInfileBufferSize && queryCacheSize == that.queryCacheSize && prepareCacheSize == that.prepareCacheSize && + compressionAlgorithms.equals(that.compressionAlgorithms) && + zstdCompressionLevel == that.zstdCompressionLevel && + Objects.equals(loopResources, that.loopResources) && extensions.equals(that.extensions) && - Objects.equals(passwordPublisher, that.passwordPublisher); + Objects.equals(passwordPublisher, that.passwordPublisher) && + Objects.equals(resolver, that.resolver) && + metrics == that.metrics && + tinyInt1isBit == that.tinyInt1isBit; } @Override public int hashCode() { return Objects.hash(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, connectTimeout, - serverZoneId, zeroDateOption, user, password, database, createDatabaseIfNotExist, - preferPrepareStatement, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, - prepareCacheSize, extensions, passwordPublisher); + preserveInstants, connectionTimeZone, forceConnectionTimeZoneToSession, + zeroDateOption, user, password, database, createDatabaseIfNotExist, + preferPrepareStatement, + sessionVariables, + lockWaitTimeout, + statementTimeout, + loadLocalInfilePath, localInfileBufferSize, + queryCacheSize, prepareCacheSize, + compressionAlgorithms, zstdCompressionLevel, + loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit); } @Override public String toString() { - if (isHost) { - return "MySqlConnectionConfiguration{host='" + domain + "', port=" + port + ", ssl=" + ssl + - ", tcpNoDelay=" + tcpNoDelay + ", tcpKeepAlive=" + tcpKeepAlive + - ", connectTimeout=" + connectTimeout + ", serverZoneId=" + serverZoneId + - ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + + return "MySqlConnectionConfiguration{" + + (isHost ? "host='" + domain + "', port=" + port + ", ssl=" + ssl + + ", tcpNoDelay=" + tcpNoDelay + ", tcpKeepAlive=" + tcpKeepAlive : + "unixSocket='" + domain + "'") + + buildCommonToStringPart() + + '}'; + } + + private String buildCommonToStringPart() { + return ", connectTimeout=" + connectTimeout + + ", preserveInstants=" + preserveInstants + + ", connectionTimeZone=" + connectionTimeZone + + ", forceConnectionTimeZoneToSession=" + forceConnectionTimeZoneToSession + + ", zeroDateOption=" + zeroDateOption + + ", user='" + user + "', password=" + password + ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + + ", sessionVariables=" + sessionVariables + + ", lockWaitTimeout=" + lockWaitTimeout + + ", statementTimeout=" + statementTimeout + ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + - ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize + - ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; - } - - return "MySqlConnectionConfiguration{unixSocket='" + domain + - "', connectTimeout=" + connectTimeout + ", serverZoneId=" + serverZoneId + - ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + - ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + - ", preferPrepareStatement=" + preferPrepareStatement + - ", loadLocalInfilePath=" + loadLocalInfilePath + - ", localInfileBufferSize=" + localInfileBufferSize + - ", queryCacheSize=" + queryCacheSize + - ", prepareCacheSize=" + prepareCacheSize + ", extensions=" + extensions + - ", passwordPublisher=" + passwordPublisher + '}'; + ", queryCacheSize=" + queryCacheSize + + ", prepareCacheSize=" + prepareCacheSize + + ", compressionAlgorithms=" + compressionAlgorithms + + ", zstdCompressionLevel=" + zstdCompressionLevel + + ", loopResources=" + loopResources + + ", extensions=" + extensions + + ", passwordPublisher=" + passwordPublisher + + ", resolver=" + resolver + + ", metrics=" + metrics + + ", tinyInt1isBit=" + tinyInt1isBit; } /** @@ -321,8 +447,11 @@ public static final class Builder { private ZeroDateOption zeroDateOption = ZeroDateOption.USE_NULL; - @Nullable - private ZoneId serverZoneId; + private boolean preserveInstants = true; + + private String connectionTimeZone = "LOCAL"; + + private boolean forceConnectionTimeZoneToSession; @Nullable private SslMode sslMode; @@ -354,6 +483,14 @@ public static final class Builder { @Nullable private Predicate preferPrepareStatement; + @Nullable + private Duration lockWaitTimeout; + + @Nullable + private Duration statementTimeout; + + private List sessionVariables = Collections.emptyList(); + @Nullable private Path loadLocalInfilePath; @@ -363,6 +500,14 @@ public static final class Builder { private int prepareCacheSize = 256; + private Set compressionAlgorithms = + Collections.singleton(CompressionAlgorithm.UNCOMPRESSED); + + private int zstdCompressionLevel = 3; + + @Nullable + private LoopResources loopResources; + private boolean autodetectExtensions = true; private final List extensions = new ArrayList<>(); @@ -370,6 +515,13 @@ public static final class Builder { @Nullable private Publisher passwordPublisher; + @Nullable + private AddressResolverGroup resolver; + + private boolean metrics; + + private boolean tinyInt1isBit = true; + /** * Builds an immutable {@link MySqlConnectionConfiguration} with current options. * @@ -392,14 +544,23 @@ public MySqlConnectionConfiguration build() { MySqlSslConfiguration ssl = MySqlSslConfiguration.create(sslMode, tlsVersion, sslHostnameVerifier, sslCa, sslKey, sslKeyPassword, sslCert, sslContextBuilderCustomizer); return new MySqlConnectionConfiguration(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, - connectTimeout, zeroDateOption, serverZoneId, user, password, database, - createDatabaseIfNotExist, preferPrepareStatement, loadLocalInfilePath, + connectTimeout, zeroDateOption, + preserveInstants, + connectionTimeZone, + forceConnectionTimeZoneToSession, + user, password, database, + createDatabaseIfNotExist, preferPrepareStatement, + sessionVariables, + lockWaitTimeout, + statementTimeout, + loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, - Extensions.from(extensions, autodetectExtensions), passwordPublisher); + compressionAlgorithms, zstdCompressionLevel, loopResources, + Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit); } /** - * Configure the database. Default no database. + * Configures the database. Default no database. * * @param database the database, or {@code null} if no database want to be login. * @return this {@link Builder}. @@ -411,7 +572,7 @@ public Builder database(@Nullable String database) { } /** - * Configure to create the database given in the configuration if it does not yet exist. Default to + * Configures to create the database given in the configuration if it does not yet exist. Default to * {@code false}. * * @param enabled to discover and register extensions. @@ -424,7 +585,7 @@ public Builder createDatabaseIfNotExist(boolean enabled) { } /** - * Configure the Unix Domain Socket to connect to. + * Configures the Unix Domain Socket to connect to. * * @param unixSocket the socket file path. * @return this {@link Builder}. @@ -438,7 +599,7 @@ public Builder unixSocket(String unixSocket) { } /** - * Configure the host. + * Configures the host. * * @param host the host. * @return this {@link Builder}. @@ -452,7 +613,7 @@ public Builder host(String host) { } /** - * Configure the password, MySQL allows to login without password. + * Configures the password. Default login without password. *

* Note: for memory security, should not use intern {@link String} for password. * @@ -466,7 +627,7 @@ public Builder password(@Nullable CharSequence password) { } /** - * Configure the port. Defaults to {@code 3306}. + * Configures the port. Defaults to {@code 3306}. * * @param port the port. * @return this {@link Builder}. @@ -481,9 +642,9 @@ public Builder port(int port) { } /** - * Configure the connection timeout. Default no timeout. + * Configures the connection timeout. Default no timeout. * - * @param connectTimeout the connection timeout, or {@code null} if has no timeout. + * @param connectTimeout the connection timeout, or {@code null} if no timeout. * @return this {@link Builder}. * @since 0.8.1 */ @@ -493,7 +654,7 @@ public Builder connectTimeout(@Nullable Duration connectTimeout) { } /** - * Set the user for login the database. + * Configures the user for login the database. * * @param user the user. * @return this {@link Builder}. @@ -518,21 +679,68 @@ public Builder username(String user) { } /** - * Enforce the time zone of server. Default to query server time zone in initialization (no - * enforce). + * Configures the time zone conversion. Default to {@code true} means enable conversion between JVM + * and {@link #connectionTimeZone(String)}. + *

+ * Note: disable it will ignore the time zone of connection, and use the JVM local time zone. * - * @param serverZoneId the {@link ZoneId}, or {@code null} if query in initialization. - * @return this {@link Builder}. + * @param enabled {@code true} to preserve instants, or {@code false} to disable conversion. + * @return {@link Builder this} + * @since 1.1.2 + */ + public Builder preserveInstants(boolean enabled) { + this.preserveInstants = enabled; + return this; + } + + /** + * Configures the time zone of connection. Default to {@code LOCAL} means use JVM local time zone. + * {@code "SERVER"} means querying the server-side timezone during initialization. + * + * @param connectionTimeZone {@code "LOCAL"}, {@code "SERVER"}, or a valid ID of {@code ZoneId}. + * @return {@link Builder this} + * @throws IllegalArgumentException if {@code connectionTimeZone} is {@code null} or empty. + * @since 1.1.2 + */ + public Builder connectionTimeZone(String connectionTimeZone) { + requireNonEmpty(connectionTimeZone, "connectionTimeZone must not be empty"); + + this.connectionTimeZone = connectionTimeZone; + return this; + } + + /** + * Configures to force the connection time zone to session time zone. Default to {@code false}. Used + * only if the {@link #connectionTimeZone(String)} is not {@code "SERVER"}. + *

+ * Note: alter the time zone of session will affect the results of MySQL date/time functions, e.g. + * {@code NOW([n])}, {@code CURRENT_TIME([n])}, {@code CURRENT_DATE()}, etc. Please use with caution. + * + * @param enabled {@code true} to force the connection time zone to session time zone. + * @return {@link Builder this} + * @since 1.1.2 + */ + public Builder forceConnectionTimeZoneToSession(boolean enabled) { + this.forceConnectionTimeZoneToSession = enabled; + return this; + } + + /** + * Configures the time zone of server. Since 1.1.2, default to use JVM local time zone. + * + * @param serverZoneId the {@link ZoneId}, or {@code null} if query server during initialization. + * @return {@link Builder this} * @since 0.8.2 + * @deprecated since 1.1.2, use {@link #connectionTimeZone(String)} instead. */ + @Deprecated public Builder serverZoneId(@Nullable ZoneId serverZoneId) { - this.serverZoneId = serverZoneId; - return this; + return connectionTimeZone(serverZoneId == null ? "SERVER" : serverZoneId.getId()); } /** - * Configure the {@link ZeroDateOption}. It is a behavior option when this driver receives a value of - * zero-date. + * Configures the {@link ZeroDateOption}. Default to {@link ZeroDateOption#USE_NULL}. It is a + * behavior option when this driver receives a value of zero-date. * * @param zeroDate the {@link ZeroDateOption}. * @return this {@link Builder}. @@ -545,7 +753,7 @@ public Builder zeroDateOption(ZeroDateOption zeroDate) { } /** - * Configure ssl mode. See also {@link SslMode}. + * Configures ssl mode. See also {@link SslMode}. * * @param sslMode the SSL mode to use. * @return this {@link Builder}. @@ -558,7 +766,7 @@ public Builder sslMode(SslMode sslMode) { } /** - * Configure TLS versions, see {@link io.asyncer.r2dbc.mysql.constant.TlsVersions}. + * Configures TLS versions, see {@link io.asyncer.r2dbc.mysql.constant.TlsVersions TlsVersions}. * * @param tlsVersion TLS versions. * @return this {@link Builder}. @@ -581,7 +789,7 @@ public Builder tlsVersion(String... tlsVersion) { } /** - * Configure SSL {@link HostnameVerifier}, it is available only set {@link #sslMode(SslMode)} as + * Configures SSL {@link HostnameVerifier}, it is available only set {@link #sslMode(SslMode)} as * {@link SslMode#VERIFY_IDENTITY}. It is useful when server was using special Certificates or need * special verification. *

@@ -599,7 +807,7 @@ public Builder sslHostnameVerifier(HostnameVerifier sslHostnameVerifier) { } /** - * Configure SSL root certification for server certificate validation. It is only available if the + * Configures SSL root certification for server certificate validation. It is only available if the * {@link #sslMode(SslMode)} is configured for verify server certification. *

* Default is {@code null}, which means that the default algorithm is used for the trust manager. @@ -614,7 +822,7 @@ public Builder sslCa(@Nullable String sslCa) { } /** - * Configure client SSL certificate for client authentication. + * Configures client SSL certificate for client authentication. *

* The {@link #sslCert} and {@link #sslKey} must be both non-{@code null} or both {@code null}. * @@ -628,7 +836,7 @@ public Builder sslCert(@Nullable String sslCert) { } /** - * Configure client SSL key for client authentication. + * Configures client SSL key for client authentication. *

* The {@link #sslCert} and {@link #sslKey} must be both non-{@code null} or both {@code null}. * @@ -642,7 +850,7 @@ public Builder sslKey(@Nullable String sslKey) { } /** - * Configure the password of SSL key file for client certificate authentication. + * Configures the password of SSL key file for client certificate authentication. *

* It will be used only if {@link #sslKey} and {@link #sslCert} non-null. * @@ -657,7 +865,7 @@ public Builder sslKeyPassword(@Nullable CharSequence sslKeyPassword) { } /** - * Configure a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL + * Configures a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL * connection attempt to allow for just-in-time configuration updates. The {@link Function} gets * called with the prepared {@link SslContextBuilder} that has all configuration options applied. The * customizer may return the same builder or return a new builder instance to be used to build the SSL @@ -677,7 +885,7 @@ public Builder sslContextBuilderCustomizer( } /** - * Configure TCP KeepAlive. + * Configures TCP KeepAlive. * * @param enabled whether to enable TCP KeepAlive * @return this {@link Builder} @@ -690,7 +898,7 @@ public Builder tcpKeepAlive(boolean enabled) { } /** - * Configure TCP NoDelay. + * Configures TCP NoDelay. * * @param enabled whether to enable TCP NoDelay * @return this {@link Builder} @@ -703,7 +911,7 @@ public Builder tcpNoDelay(boolean enabled) { } /** - * Configure the protocol of parametrized statements to the text protocol. + * Configures the protocol of parameterized statements to the text protocol. *

* The text protocol is default protocol that's using client-preparing. See also MySQL * documentations. @@ -717,7 +925,7 @@ public Builder useClientPrepareStatement() { } /** - * Configure the protocol of parametrized statements to the binary protocol. + * Configures the protocol of parameterized statements to the binary protocol. *

* The binary protocol is compact protocol that's using server-preparing. See also MySQL * documentations. @@ -730,7 +938,7 @@ public Builder useServerPrepareStatement() { } /** - * Configure the protocol of parametrized statements and prepare-preferred simple statements to the + * Configures the protocol of parameterized statements and prepare-preferred simple statements to the * binary protocol. *

* The {@code preferPrepareStatement} configures whether to prefer prepare execution on a @@ -753,6 +961,47 @@ public Builder useServerPrepareStatement(Predicate preferPrepareStatemen return this; } + /** + * Configures the session variables, used to set session variables immediately after login. Default no + * session variables to set. It should be a list of key-value pairs. e.g. + * {@code ["sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'", "time_zone=00:00"]}. + * + * @param sessionVariables the session variables to set. + * @return {@link Builder this} + * @throws IllegalArgumentException if {@code sessionVariables} is {@code null}. + * @since 1.1.2 + */ + public Builder sessionVariables(String... sessionVariables) { + requireNonNull(sessionVariables, "sessionVariables must not be null"); + + this.sessionVariables = InternalArrays.toImmutableList(sessionVariables); + return this; + } + + /** + * Configures the lock wait timeout. Default to use the server-side default value. + * + * @param lockWaitTimeout the lock wait timeout, or {@code null} to use the server-side default value. + * @return {@link Builder this} + * @since 1.1.3 + */ + public Builder lockWaitTimeout(@Nullable Duration lockWaitTimeout) { + this.lockWaitTimeout = lockWaitTimeout; + return this; + } + + /** + * Configures the statement timeout. Default to use the server-side default value. + * + * @param statementTimeout the statement timeout, or {@code null} to use the server-side default value. + * @return {@link Builder this} + * @since 1.1.3 + */ + public Builder statementTimeout(@Nullable Duration statementTimeout) { + this.statementTimeout = statementTimeout; + return this; + } + /** * Configures to allow the {@code LOAD DATA LOCAL INFILE} statement in the given {@code path} or * disallow the statement. Default to {@code null} which means not allow the statement. @@ -804,7 +1053,7 @@ public Builder queryCacheSize(int queryCacheSize) { /** * Configures the maximum size of the server-preparing cache. Usually it should be power of two. * Default to {@code 256}. Driver will use unbounded cache if size is less than {@code 0}. It is used - * only if using server-preparing parametrized statements, i.e. the {@link #useServerPrepareStatement} + * only if using server-preparing parameterized statements, i.e. the {@link #useServerPrepareStatement} * is set. *

* Notice: the cache is using EC model (the PACELC theorem) for ensure consistency. Consistency is @@ -822,6 +1071,78 @@ public Builder prepareCacheSize(int prepareCacheSize) { return this; } + /** + * Configures the compression algorithms. Default to [{@link CompressionAlgorithm#UNCOMPRESSED}]. + *

+ * It will auto choose an algorithm that's contained in the list and supported by the server, + * preferring zstd, then zlib. If the list does not contain {@link CompressionAlgorithm#UNCOMPRESSED} + * and the server does not support any algorithm in the list, an exception will be thrown when + * connecting. + *

+ * Note: zstd requires a dependency {@code com.github.luben:zstd-jni}. + * + * @param compressionAlgorithms the list of compression algorithms. + * @return {@link Builder this}. + * @throws IllegalArgumentException if {@code compressionAlgorithms} is {@code null} or empty. + * @since 1.1.2 + */ + public Builder compressionAlgorithms(CompressionAlgorithm... compressionAlgorithms) { + requireNonNull(compressionAlgorithms, "compressionAlgorithms must not be null"); + require(compressionAlgorithms.length != 0, "compressionAlgorithms must not be empty"); + + if (compressionAlgorithms.length == 1) { + requireNonNull(compressionAlgorithms[0], "compressionAlgorithms must not contain null"); + this.compressionAlgorithms = Collections.singleton(compressionAlgorithms[0]); + } else { + Set algorithms = EnumSet.noneOf(CompressionAlgorithm.class); + + for (CompressionAlgorithm algorithm : compressionAlgorithms) { + requireNonNull(algorithm, "compressionAlgorithms must not contain null"); + algorithms.add(algorithm); + } + + this.compressionAlgorithms = algorithms; + } + + return this; + } + + /** + * Configures the zstd compression level. Default to {@code 3}. + *

+ * It is only used if zstd is chosen for the connection. + *

+ * Note: MySQL protocol does not allow to set the zlib compression level of the server, only zstd is + * configurable. + * + * @param level the compression level. + * @return {@link Builder this}. + * @throws IllegalArgumentException if {@code level} is not between 1 and 22. + * @see + * MySQL Connection Options --zstd-compression-level + * @since 1.1.2 + */ + public Builder zstdCompressionLevel(int level) { + require(level >= 1 && level <= 22, "level must be between 1 and 22"); + + this.zstdCompressionLevel = level; + return this; + } + + /** + * Configures the {@link LoopResources} for the driver. Default to + * {@link TcpResources#get() global tcp resources}. + * + * @param loopResources the {@link LoopResources}. + * @return this {@link Builder}. + * @throws IllegalArgumentException if {@code loopResources} is {@code null}. + * @since 1.1.2 + */ + public Builder loopResources(LoopResources loopResources) { + this.loopResources = requireNonNull(loopResources, "loopResources must not be null"); + return this; + } + /** * Configures whether to use {@link ServiceLoader} to discover and register extensions. Defaults to * {@code true}. @@ -863,6 +1184,56 @@ public Builder passwordPublisher(Publisher passwordPublisher) { return this; } + /** + * Sets the {@link AddressResolverGroup} for resolving host addresses. + *

+ * This can be used to customize the DNS resolution mechanism, which is particularly useful in environments + * with specific DNS configuration needs or where a custom DNS resolver is required. + * + * @param resolver the resolver group to use for host address resolution. + * @return this {@link Builder}. + * @since 1.2.0 + */ + public Builder resolver(AddressResolverGroup resolver) { + this.resolver = resolver; + return this; + } + + /** + * Option to enable metrics to be collected and registered in Micrometer's globalRegistry + * with {@link reactor.netty.tcp.TcpClient#metrics(boolean)}. Defaults to {@code false}. + *

+ * Note: It is required to add {@code io.micrometer.micrometer-core} dependency to classpath. + * + * @param enabled enable metrics for {@link reactor.netty.tcp.TcpClient}. + * @return this {@link Builder} + * @throws IllegalArgumentException if {@code io.micrometer:micrometer-core} is not on the classpath. + * @since 1.3.2 + */ + public Builder metrics(boolean enabled) { + require(!enabled || Metrics.isMicrometerAvailable(), + "dependency `io.micrometer:micrometer-core` must be added to classpath if metrics enabled" + ); + this.metrics = enabled; + return this; + } + + /** + * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type. + * When enabled, TINYINT(1) columns will be treated as BIT. Defaults to {@code true}. + *

+ * Note: Only signed TINYINT(1) columns can be treated as BIT or Boolean. + * Ref: https://bugs.mysql.com/bug.php?id=100309 + * + * @param tinyInt1isBit {@code true} to treat TINYINT(1) as BIT + * @return this {@link Builder} + * @since 1.4.0 + */ + public Builder tinyInt1isBit(boolean tinyInt1isBit) { + this.tinyInt1isBit = tinyInt1isBit; + return this; + } + private SslMode requireSslMode() { SslMode sslMode = this.sslMode; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java new file mode 100644 index 000000000..094674f2a --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -0,0 +1,225 @@ +/* + * Copyright 2023 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.cache.Caches; +import io.asyncer.r2dbc.mysql.cache.QueryCache; +import io.asyncer.r2dbc.mysql.client.Client; +import io.asyncer.r2dbc.mysql.internal.util.StringUtils; +import io.netty.channel.unix.DomainSocketAddress; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import org.jetbrains.annotations.Nullable; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.time.ZoneId; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; + +/** + * An implementation of {@link ConnectionFactory} for creating connections to a MySQL database. + */ +public final class MySqlConnectionFactory implements ConnectionFactory { + + private final MySqlConnectionConfiguration configuration; + private final LazyQueryCache queryCache; + + private MySqlConnectionFactory(MySqlConnectionConfiguration configuration) { + this.configuration = configuration; + this.queryCache = new LazyQueryCache(configuration.getQueryCacheSize()); + } + + @Override + public Mono create() { + MySqlSslConfiguration ssl; + SocketAddress address; + + if (configuration.isHost()) { + ssl = configuration.getSsl(); + address = InetSocketAddress.createUnresolved(configuration.getDomain(), + configuration.getPort()); + } else { + ssl = MySqlSslConfiguration.disabled(); + address = new DomainSocketAddress(configuration.getDomain()); + } + + String user = configuration.getUser(); + CharSequence password = configuration.getPassword(); + Publisher passwordPublisher = configuration.getPasswordPublisher(); + + if (Objects.nonNull(passwordPublisher)) { + return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( + configuration, ssl, + queryCache, + address, + user, + token + )); + } + + return getMySqlConnection( + configuration, ssl, + queryCache, + address, + user, + password + ); + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return MySqlConnectionFactoryMetadata.INSTANCE; + } + + /** + * Creates a {@link MySqlConnectionFactory} with a {@link MySqlConnectionConfiguration}. + * + * @param configuration the {@link MySqlConnectionConfiguration}. + * @return configured {@link MySqlConnectionFactory}. + */ + public static MySqlConnectionFactory from(MySqlConnectionConfiguration configuration) { + requireNonNull(configuration, "configuration must not be null"); + return new MySqlConnectionFactory(configuration); + } + + /** + * Gets an initialized {@link MySqlConnection} from authentication credential and configurations. + *

+ * It contains following steps: + *

  1. Create connection context
  2. + *
  3. Connect to MySQL server with TCP or Unix Domain Socket
  4. + *
  5. Handshake/login and init handshake states
  6. + *
  7. Init session states
+ * + * @param configuration the connection configuration. + * @param ssl the SSL configuration. + * @param queryCache lazy-init query cache, it is shared among all connections from the same factory. + * @param address TCP or Unix Domain Socket address. + * @param user the user of the authentication. + * @param password the password of the authentication. + * @return a {@link MySqlConnection}. + */ + private static Mono getMySqlConnection( + final MySqlConnectionConfiguration configuration, + final MySqlSslConfiguration ssl, + final LazyQueryCache queryCache, + final SocketAddress address, + final String user, + @Nullable final CharSequence password + ) { + return Mono.fromSupplier(() -> { + ZoneId connectionTimeZone = retrieveZoneId(configuration.getConnectionTimeZone()); + return new ConnectionContext( + configuration.getZeroDateOption(), + configuration.getLoadLocalInfilePath(), + configuration.getLocalInfileBufferSize(), + configuration.isTinyInt1isBit(), + configuration.isPreserveInstants(), + connectionTimeZone + ); + }).flatMap(context -> Client.connect( + ssl, + address, + configuration.isTcpKeepAlive(), + configuration.isTcpNoDelay(), + context, + configuration.getConnectTimeout(), + configuration.getLoopResources(), + configuration.getResolver(), + configuration.isMetrics() + )).flatMap(client -> { + // Lazy init database after handshake/login + boolean deferDatabase = configuration.isCreateDatabaseIfNotExist(); + String database = configuration.getDatabase(); + String loginDb = deferDatabase ? "" : database; + String sessionDb = deferDatabase ? database : ""; + + return InitFlow.initHandshake( + client, + ssl.getSslMode(), + loginDb, + user, + password, + configuration.getCompressionAlgorithms(), + configuration.getZstdCompressionLevel() + ).then(InitFlow.initSession( + client, + sessionDb, + configuration.getPrepareCacheSize(), + configuration.getSessionVariables(), + configuration.isForceConnectionTimeZoneToSession(), + configuration.getLockWaitTimeout(), + configuration.getStatementTimeout(), + configuration.getExtensions() + )).map(codecs -> new MySqlSimpleConnection( + client, + codecs, + queryCache.get(), + configuration.getPreferPrepareStatement() + )).onErrorResume(e -> client.forceClose().then(Mono.error(e))); + }); + } + + @Nullable + private static ZoneId retrieveZoneId(String timeZone) { + if ("LOCAL".equalsIgnoreCase(timeZone)) { + return ZoneId.systemDefault().normalized(); + } else if ("SERVER".equalsIgnoreCase(timeZone)) { + return null; + } + + return StringUtils.parseZoneId(timeZone); + } + + private static final class LazyQueryCache implements Supplier { + + private final int capacity; + + private final ReentrantLock lock = new ReentrantLock(); + + @Nullable + private volatile QueryCache cache; + + private LazyQueryCache(int capacity) { + this.capacity = capacity; + } + + @Override + public QueryCache get() { + QueryCache cache = this.cache; + if (cache == null) { + lock.lock(); + try { + if ((cache = this.cache) == null) { + this.cache = cache = Caches.createQueryCache(capacity); + } + return cache; + } finally { + lock.unlock(); + } + } + return cache; + } + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java new file mode 100644 index 000000000..5905c56ca --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -0,0 +1,630 @@ +/* + * Copyright 2023 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; +import io.asyncer.r2dbc.mysql.constant.SslMode; +import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.resolver.AddressResolverGroup; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import io.r2dbc.spi.ConnectionFactoryProvider; +import io.r2dbc.spi.Option; +import org.reactivestreams.Publisher; +import reactor.netty.resources.LoopResources; + +import javax.net.ssl.HostnameVerifier; +import java.time.Duration; +import java.time.ZoneId; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; +import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_STRINGS; +import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT; +import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; +import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; +import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; +import static io.r2dbc.spi.ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT; +import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; +import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; +import static io.r2dbc.spi.ConnectionFactoryOptions.SSL; +import static io.r2dbc.spi.ConnectionFactoryOptions.STATEMENT_TIMEOUT; +import static io.r2dbc.spi.ConnectionFactoryOptions.USER; + +/** + * An implementation of {@link ConnectionFactoryProvider} for creating {@link MySqlConnectionFactory}s. + */ +public final class MySqlConnectionFactoryProvider implements ConnectionFactoryProvider { + + /** + * The name of the driver used for discovery, should not be changed. + */ + public static final String MYSQL_DRIVER = "mysql"; + + /** + * Option to set the Unix Domain Socket. + * + * @since 0.8.1 + */ + public static final Option UNIX_SOCKET = Option.valueOf("unixSocket"); + + /** + * Option to set the time zone conversion. Default to {@code true} means enable conversion between JVM + * and {@link #CONNECTION_TIME_ZONE}. + *

+ * Note: disable it will ignore the time zone of connection, and use the JVM local time zone. + * + * @since 1.1.2 + */ + public static final Option PRESERVE_INSTANTS = Option.valueOf("preserveInstants"); + + /** + * Option to set the time zone of connection. Default to {@code LOCAL} means use JVM local time zone. + * It should be {@code "LOCAL"}, {@code "SERVER"}, or a valid ID of {@code ZoneId}. {@code "SERVER"} means + * querying the server-side timezone during initialization. + * + * @since 1.1.2 + */ + public static final Option CONNECTION_TIME_ZONE = Option.valueOf("connectionTimeZone"); + + /** + * Option to force the time zone of connection to session time zone. Default to {@code false}. + *

+ * Note: alter the time zone of session will affect the results of MySQL date/time functions, e.g. + * {@code NOW([n])}, {@code CURRENT_TIME([n])}, {@code CURRENT_DATE()}, etc. Please use with caution. + * + * @since 1.1.2 + */ + public static final Option FORCE_CONNECTION_TIME_ZONE_TO_SESSION = + Option.valueOf("forceConnectionTimeZoneToSession"); + + /** + * Option to set {@link ZoneId} of server. If it is set, driver will ignore the real time zone of + * server-side. + * + * @since 0.8.2 + * @deprecated since 1.1.2, use {@link #CONNECTION_TIME_ZONE} instead. + */ + @Deprecated + public static final Option SERVER_ZONE_ID = Option.valueOf("serverZoneId"); + + /** + * Option to configure handling when MySQL server returning "zero date" (aka. "0000-00-00 00:00:00") + * + * @since 0.8.1 + */ + public static final Option ZERO_DATE = Option.valueOf("zeroDate"); + + /** + * Option to {@link SslMode}. + * + * @since 0.8.1 + */ + public static final Option SSL_MODE = Option.valueOf("sslMode"); + + /** + * Option to configure {@link HostnameVerifier}. It is available only if the {@link #SSL_MODE} set to + * {@link SslMode#VERIFY_IDENTITY}. It can be an implementation class name of {@link HostnameVerifier} + * with a public no-args constructor. + * + * @since 0.8.2 + */ + public static final Option SSL_HOSTNAME_VERIFIER = + Option.valueOf("sslHostnameVerifier"); + + /** + * Option to TLS versions for SslContext protocols, see also {@code TlsVersions}. Usually sorted from + * higher to lower. It can be a {@code Collection}. It can be a {@link String}, protocols will be + * split by {@code ,}. e.g. "TLSv1.2,TLSv1.1,TLSv1". + * + * @since 0.8.1 + */ + public static final Option TLS_VERSION = Option.valueOf("tlsVersion"); + + /** + * Option to set a PEM file of server SSL CA. It will be used to verify server certificates. And it will + * be used only if {@link #SSL_MODE} set to {@link SslMode#VERIFY_CA} or higher level. + * + * @since 0.8.1 + */ + public static final Option SSL_CA = Option.valueOf("sslCa"); + + /** + * Option to set a PEM file of client SSL key. + * + * @since 0.8.1 + */ + public static final Option SSL_KEY = Option.valueOf("sslKey"); + + /** + * Option to set a PEM file password of client SSL key. It will be used only if {@link #SSL_KEY} and + * {@link #SSL_CERT} set. + * + * @since 0.8.1 + */ + public static final Option SSL_KEY_PASSWORD = Option.sensitiveValueOf("sslKeyPassword"); + + /** + * Option to set a PEM file of client SSL cert. + * + * @since 0.8.1 + */ + public static final Option SSL_CERT = Option.valueOf("sslCert"); + + /** + * Option to custom {@link SslContextBuilder}. It can be an implementation class name of {@link Function} + * with a public no-args constructor. + * + * @since 0.8.2 + */ + public static final Option> + SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer"); + + /** + * Enable/Disable TCP KeepAlive. + * + * @since 0.8.2 + */ + public static final Option TCP_KEEP_ALIVE = Option.valueOf("tcpKeepAlive"); + + /** + * Enable/Disable TCP NoDelay. + * + * @since 0.8.2 + */ + public static final Option TCP_NO_DELAY = Option.valueOf("tcpNoDelay"); + + /** + * Enable/Disable database creation if not exist. + * + * @since 1.0.6 + */ + public static final Option CREATE_DATABASE_IF_NOT_EXIST = + Option.valueOf("createDatabaseIfNotExist"); + + /** + * Enable server preparing for parameterized statements and prefer server preparing simple statements. + *

+ * The value can be a {@link Boolean}. If it is {@code true}, driver will use server preparing for + * parameterized statements and text query for simple statements. If it is {@code false}, driver will use + * client preparing for parameterized statements and text query for simple statements. + *

+ * The value can be a {@link Predicate}{@code <}{@link String}{@code >}. If it is set, driver will server + * preparing for parameterized statements, it configures whether to prefer prepare execution on a + * statement-by-statement basis (simple statements). The {@link Predicate}{@code <}{@link String}{@code >} + * accepts the simple SQL query string and returns a {@code boolean} flag indicating preference. + *

+ * The value can be a {@link String}. If it is set, driver will try to convert it to {@link Boolean} or an + * instance of {@link Predicate}{@code <}{@link String}{@code >} which use reflection with a public + * no-args constructor. + * + * @since 0.8.1 + */ + public static final Option USE_SERVER_PREPARE_STATEMENT = + Option.valueOf("useServerPrepareStatement"); + + /** + * Option to set session variables. It should be a list of key-value pairs. e.g. + * {@code ["sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'", "time_zone=00:00"]}. + * + * @since 1.1.2 + */ + public static final Option SESSION_VARIABLES = Option.valueOf("sessionVariables"); + + /** + * Option to set the allowed local infile path. + * + * @since 1.1.0 + */ + public static final Option ALLOW_LOAD_LOCAL_INFILE_IN_PATH = + Option.valueOf("allowLoadLocalInfileInPath"); + + /** + * Option to set the buffer size for local infile. Default to {@code 8192}. + * + * @since 1.1.2 + */ + public static final Option LOCAL_INFILE_BUFFER_SIZE = + Option.valueOf("localInfileBufferSize"); + + /** + * Option to set compression algorithms. Default to [{@link CompressionAlgorithm#UNCOMPRESSED}]. + *

+ * It will auto choose an algorithm that's contained in the list and supported by the server, preferring + * zstd, then zlib. If the list does not contain {@link CompressionAlgorithm#UNCOMPRESSED} and the server + * does not support any algorithm in the list, an exception will be thrown when connecting. + *

+ * Note: zstd requires a dependency {@code com.github.luben:zstd-jni}. + * + * @since 1.1.2 + */ + public static final Option COMPRESSION_ALGORITHMS = + Option.valueOf("compressionAlgorithms"); + + /** + * Option to set the zstd compression level. Default to {@code 3}. + *

+ * It is only used if zstd is chosen for the connection. + *

+ * Note: MySQL protocol does not allow to set the zlib compression level of the server, only zstd is + * configurable. + * + * @since 1.1.2 + */ + public static final Option ZSTD_COMPRESSION_LEVEL = + Option.valueOf("zstdCompressionLevel"); + + /** + * Option to set the {@link LoopResources} for the connection. Default to + * {@link reactor.netty.tcp.TcpResources#get() global tcp Resources} + * + * @since 1.1.2 + */ + public static final Option LOOP_RESOURCES = Option.valueOf("loopResources"); + + /** + * Option to set the maximum size of the {@link Query} parsing cache. Default to {@code 256}. + * + * @since 0.8.3 + */ + public static final Option PREPARE_CACHE_SIZE = Option.valueOf("prepareCacheSize"); + + /** + * Option to set the maximum size of the server-preparing cache. Default to {@code 0}. + * + * @since 0.8.3 + */ + public static final Option QUERY_CACHE_SIZE = Option.valueOf("queryCacheSize"); + + /** + * Enable/Disable auto-detect driver extensions. + * + * @since 0.8.2 + */ + public static final Option AUTODETECT_EXTENSIONS = Option.valueOf("autodetectExtensions"); + + /** + * Password Publisher function can be used to retrieve password before creating a connection. This can be + * used with Amazon RDS Aurora IAM authentication, wherein it requires token to be generated. The token is + * valid for 15 minutes, and this token will be used as password. + */ + public static final Option> PASSWORD_PUBLISHER = Option.valueOf("passwordPublisher"); + + /** + * Option to set the {@link AddressResolverGroup} for resolving host addresses. + *

+ * This can be used to customize the DNS resolution mechanism, which is particularly useful in environments + * with specific DNS configuration needs or where a custom DNS resolver is required. + *

+ * + * @since 1.2.0 + */ + public static final Option> RESOLVER = Option.valueOf("resolver"); + + /** + * Option to enable metrics to be collected and registered in Micrometer's globalRegistry + * with {@link reactor.netty.tcp.TcpClient#metrics(boolean)}. Defaults to {@code false}. + *

+ * Note: It is required to add {@code io.micrometer.micrometer-core} dependency to classpath. + * + * @since 1.3.2 + */ + public static final Option METRICS = Option.valueOf("metrics"); + + /** + * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type. + * When enabled, TINYINT(1) columns will be treated as BIT. Defaults to {@code true}. + *

+ * Note: Only signed TINYINT(1) columns can be treated as BIT or Boolean. + * Ref: https://bugs.mysql.com/bug.php?id=100309 + * + * @since 1.4.0 + */ + public static final Option TINY_INT_1_IS_BIT = Option.valueOf("tinyInt1isBit"); + + @Override + public ConnectionFactory create(ConnectionFactoryOptions options) { + requireNonNull(options, "connectionFactoryOptions must not be null"); + + return MySqlConnectionFactory.from(setup(options)); + } + + @Override + public boolean supports(ConnectionFactoryOptions options) { + requireNonNull(options, "connectionFactoryOptions must not be null"); + return MYSQL_DRIVER.equals(options.getValue(DRIVER)); + } + + @Override + public String getDriver() { + return MYSQL_DRIVER; + } + + /** + * Visible for unit tests. + * + * @param options the {@link ConnectionFactoryOptions} for setup {@link MySqlConnectionConfiguration}. + * @return completed {@link MySqlConnectionConfiguration}. + */ + static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { + OptionMapper mapper = new OptionMapper(options); + MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder(); + + mapper.requires(USER).asString() + .to(builder::user); + mapper.optional(PASSWORD).asPassword() + .to(builder::password); + mapper.optional(UNIX_SOCKET).asString() + .to(builder::unixSocket) + .otherwise(() -> setupHost(builder, mapper)); + mapper.optional(PRESERVE_INSTANTS).asBoolean() + .to(builder::preserveInstants); + mapper.optional(CONNECTION_TIME_ZONE).asString() + .to(builder::connectionTimeZone) + .otherwise(() -> mapper.optional(SERVER_ZONE_ID) + .as(ZoneId.class, id -> ZoneId.of(id, ZoneId.SHORT_IDS)) + .to(builder::serverZoneId)); + mapper.optional(FORCE_CONNECTION_TIME_ZONE_TO_SESSION).asBoolean() + .to(builder::forceConnectionTimeZoneToSession); + mapper.optional(TCP_KEEP_ALIVE).asBoolean() + .to(builder::tcpKeepAlive); + mapper.optional(TCP_NO_DELAY).asBoolean() + .to(builder::tcpNoDelay); + mapper.optional(ZERO_DATE) + .as(ZeroDateOption.class, id -> ZeroDateOption.valueOf(id.toUpperCase())) + .to(builder::zeroDateOption); + mapper.optional(USE_SERVER_PREPARE_STATEMENT).prepare(builder::useClientPrepareStatement, + builder::useServerPrepareStatement, builder::useServerPrepareStatement); + mapper.optional(ALLOW_LOAD_LOCAL_INFILE_IN_PATH).asString() + .to(builder::allowLoadLocalInfileInPath); + mapper.optional(LOCAL_INFILE_BUFFER_SIZE).asInt() + .to(builder::localInfileBufferSize); + mapper.optional(QUERY_CACHE_SIZE).asInt() + .to(builder::queryCacheSize); + mapper.optional(PREPARE_CACHE_SIZE).asInt() + .to(builder::prepareCacheSize); + mapper.optional(AUTODETECT_EXTENSIONS).asBoolean() + .to(builder::autodetectExtensions); + mapper.optional(CONNECT_TIMEOUT).as(Duration.class, Duration::parse) + .to(builder::connectTimeout); + mapper.optional(DATABASE).asString() + .to(builder::database); + mapper.optional(CREATE_DATABASE_IF_NOT_EXIST).asBoolean() + .to(builder::createDatabaseIfNotExist); + mapper.optional(COMPRESSION_ALGORITHMS).asArray( + CompressionAlgorithm[].class, + it -> CompressionAlgorithm.valueOf(it.toUpperCase()), + it -> it.split(","), + CompressionAlgorithm[]::new + ).to(builder::compressionAlgorithms); + mapper.optional(ZSTD_COMPRESSION_LEVEL).asInt() + .to(builder::zstdCompressionLevel); + mapper.optional(LOOP_RESOURCES).as(LoopResources.class) + .to(builder::loopResources); + mapper.optional(PASSWORD_PUBLISHER).as(Publisher.class) + .to(builder::passwordPublisher); + mapper.optional(RESOLVER).as(AddressResolverGroup.class) + .to(builder::resolver); + mapper.optional(SESSION_VARIABLES).asArray( + String[].class, + Function.identity(), + MySqlConnectionFactoryProvider::splitVariables, + String[]::new + ).to(builder::sessionVariables); + mapper.optional(LOCK_WAIT_TIMEOUT).as(Duration.class, Duration::parse) + .to(builder::lockWaitTimeout); + mapper.optional(STATEMENT_TIMEOUT).as(Duration.class, Duration::parse) + .to(builder::statementTimeout); + mapper.optional(METRICS).asBoolean() + .to(builder::metrics); + mapper.optional(TINY_INT_1_IS_BIT).asBoolean() + .to(builder::tinyInt1isBit); + + return builder.build(); + } + + /** + * Set builder of {@link MySqlConnectionConfiguration} for hostname-based address with SSL + * configurations. + * + * @param builder the builder of {@link MySqlConnectionConfiguration}. + * @param mapper the {@link OptionMapper} of {@code options}. + */ + private static void setupHost(MySqlConnectionConfiguration.Builder builder, OptionMapper mapper) { + mapper.requires(HOST).asString() + .to(builder::host); + mapper.optional(PORT).asInt() + .to(builder::port); + mapper.optional(SSL).asBoolean() + .to(isSsl -> builder.sslMode(isSsl ? SslMode.REQUIRED : SslMode.DISABLED)); + mapper.optional(SSL_MODE).as(SslMode.class, id -> SslMode.valueOf(id.toUpperCase())) + .to(builder::sslMode); + mapper.optional(TLS_VERSION) + .asArray(String[].class, Function.identity(), it -> it.split(","), String[]::new) + .to(builder::tlsVersion); + mapper.optional(SSL_HOSTNAME_VERIFIER).as(HostnameVerifier.class) + .to(builder::sslHostnameVerifier); + mapper.optional(SSL_CERT).asString() + .to(builder::sslCert); + mapper.optional(SSL_KEY).asString() + .to(builder::sslKey); + mapper.optional(SSL_KEY_PASSWORD).asPassword() + .to(builder::sslKeyPassword); + mapper.optional(SSL_CONTEXT_BUILDER_CUSTOMIZER).as(Function.class) + .to(builder::sslContextBuilderCustomizer); + mapper.optional(SSL_CA).asString() + .to(builder::sslCa); + } + + /** + * Splits session variables from user input. e.g. {@code sql_mode='ANSI_QUOTE,STRICT',c=d;e=f} will be + * split into {@code ["sql_mode='ANSI_QUOTE,STRICT'", "c=d", "e=f"]}. + *

+ * It supports escaping characters with backslash, quoted values with single or double quotes, and nested + * brackets. Priorities are: backslash in quoted > single quote = double quote > bracket, backslash + * will not be a valid escape character if it is not in a quoted value. + *

+ * Note that it does not strictly check syntax validity, so it will not throw syntax exceptions. + * + * @param sessionVariables the session variables from user input. + * @return the split list + * @throws IllegalArgumentException if {@code sessionVariables} is {@code null}. + */ + private static String[] splitVariables(String sessionVariables) { + requireNonNull(sessionVariables, "sessionVariables must not be null"); + + if (sessionVariables.isEmpty()) { + return EMPTY_STRINGS; + } + + // 1: bracket, 2: single quote, 3: double quote, 4: backtick + ArrayDeque stack = new ArrayDeque<>(); + int index = 0; + int len = sessionVariables.length(); + List variables = new ArrayList<>(); + + for (int i = 0; i < len; ++i) { + switch (sessionVariables.charAt(i)) { + case '\\': + if (i + 1 < len) { + if (stack.isEmpty()) { + break; + } + + switch (stack.peekLast()) { + case 2: + case 3: + // All valid escape characters + switch (sessionVariables.charAt(i + 1)) { + case '\'': + case '"': + case '\\': + case 'n': + case 'r': + case 't': + case 'b': + case 'f': + ++i; + break; + } + break; + default: + // Backtick does not support escape characters + break; + } + } + break; + case ';': + case ',': + if (stack.isEmpty()) { + variables.add(sessionVariables.substring(index, i).trim()); + index = i + 1; + } + break; + case '(': + if (stack.isEmpty()) { + stack.addLast(1); + break; + } + + switch (stack.peekLast()) { + case 2: + case 3: + case 4: + break; + default: + stack.addLast(1); + break; + } + break; + case ')': + if (stack.isEmpty()) { + // Invalid bracket, ignore + break; + } + + if (stack.peekLast() == 1) { + stack.pollLast(); + } + break; + case '\'': + if (stack.isEmpty()) { + stack.addLast(2); + break; + } + + switch (stack.peekLast()) { + case 2: + stack.pollLast(); + break; + case 3: + case 4: + break; + default: + stack.addLast(2); + break; + } + break; + case '"': + if (stack.isEmpty()) { + stack.addLast(3); + break; + } + + switch (stack.peekLast()) { + case 3: + stack.pollLast(); + break; + case 2: + case 4: + break; + default: + stack.addLast(3); + break; + } + break; + case '`': + if (stack.isEmpty()) { + stack.addLast(4); + break; + } + + switch (stack.peekLast()) { + case 4: + stack.pollLast(); + break; + case 2: + case 3: + break; + default: + stack.addLast(4); + break; + } + break; + } + } + + variables.add(sessionVariables.substring(index).trim()); + + return variables.toArray(new String[0]); + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java similarity index 67% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java index 04cc12eff..05add4758 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java @@ -16,11 +16,12 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlRow; +import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata; +import io.asyncer.r2dbc.mysql.codec.CodecContext; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; -import org.jetbrains.annotations.Nullable; import java.lang.reflect.ParameterizedType; @@ -29,11 +30,11 @@ /** * An implementation of {@link Row} for MySQL database. */ -public final class MySqlRow implements Row { +final class MySqlDataRow implements MySqlRow { private final FieldValue[] fields; - private final MySqlRowMetadata rowMetadata; + private final MySqlRowDescriptor rowMetadata; private final Codecs codecs; @@ -42,10 +43,13 @@ public final class MySqlRow implements Row { */ private final boolean binary; - private final ConnectionContext context; + /** + * It can be retained because it is provided by the executed connection instead of the current connection. + */ + private final CodecContext context; - MySqlRow(FieldValue[] fields, MySqlRowMetadata rowMetadata, Codecs codecs, boolean binary, - ConnectionContext context) { + MySqlDataRow(FieldValue[] fields, MySqlRowDescriptor rowMetadata, Codecs codecs, boolean binary, + CodecContext context) { this.fields = requireNonNull(fields, "fields must not be null"); this.rowMetadata = requireNonNull(rowMetadata, "rowMetadata must not be null"); this.codecs = requireNonNull(codecs, "codecs must not be null"); @@ -69,16 +73,7 @@ public T get(String name, Class type) { return codecs.decode(fields[info.getIndex()], info, type, binary, context); } - /** - * Returns the value for a column in this row. The value can be a parameterized type. - * - * @param index the index of the column starting at {@code 0}. - * @param type the parameterized type of item to return. - * @param the type of the item being returned. - * @return the value for a column in this row. Value can be {@code null}. - * @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}. - */ - @Nullable + @Override public T get(int index, ParameterizedType type) { requireNonNull(type, "type must not be null"); @@ -86,16 +81,7 @@ public T get(int index, ParameterizedType type) { return codecs.decode(fields[index], info, type, binary, context); } - /** - * Returns the value for a column in this row. The value can be a parameterized type. - * - * @param name the name of the column. - * @param type the parameterized type of item to return. - * @param the type of the item being returned. - * @return the value for a column in this row. Value can be {@code null}. - * @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}. - */ - @Nullable + @Override public T get(String name, ParameterizedType type) { requireNonNull(type, "type must not be null"); @@ -107,7 +93,7 @@ public T get(String name, ParameterizedType type) { * {@inheritDoc} */ @Override - public RowMetadata getMetadata() { + public MySqlRowMetadata getMetadata() { return rowMetadata; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java similarity index 93% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java index 08df22a4c..25088dff4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java @@ -43,7 +43,7 @@ default boolean isNull() { * Binary protocol encoding. See MySQL protocol documentations, if don't want to support the binary * protocol, please receive an exception. *

- * Note: not like the text protocol, it make a sense for state-less. + * Note: not like the text protocol, it makes a sense for state-less. *

* Binary protocol maybe need to add a var-integer length before encoded content. So if makes it like * {@code Mono publishBinary (Xxx binaryWriter)}, and if supports multiple times writing like a @@ -75,9 +75,9 @@ default boolean isNull() { Mono publishText(ParameterWriter writer); /** - * Get the {@link MySqlType} of this parameter data. + * Gets the {@link MySqlType} of this parameter data. *

- * If don't want to support the binary protocol, just throw an exception please. + * If it does not want to support the binary protocol, just throw an exception please. * * @return the MySQL type. */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java similarity index 53% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java index 37f26ac7b..a86d6a9ab 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java @@ -16,57 +16,37 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.message.server.DefinitionMetadataMessage; -import io.r2dbc.spi.RowMetadata; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.NoSuchElementException; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** - * An implementation of {@link RowMetadata} for MySQL database text/binary results. - * - * @see MySqlNames column name searching rules. + * An implementation of {@link MySqlRowMetadata} for MySQL database text/binary results. */ -final class MySqlRowMetadata implements RowMetadata { +final class MySqlRowDescriptor implements MySqlRowMetadata { private final MySqlColumnDescriptor[] originMetadata; - private final MySqlColumnDescriptor[] sortedMetadata; - - private final ColumnNameSet nameSet; - - private MySqlRowMetadata(MySqlColumnDescriptor[] metadata) { - int size = metadata.length; - - switch (size) { - case 0: - throw new IllegalArgumentException("Least 1 column metadata"); - case 1: - String name = metadata[0].getName(); - - this.originMetadata = metadata; - this.sortedMetadata = metadata; - this.nameSet = ColumnNameSet.of(name); - - break; - default: - MySqlColumnDescriptor[] sortedMetadata = new MySqlColumnDescriptor[size]; - System.arraycopy(metadata, 0, sortedMetadata, 0, size); - Arrays.sort(sortedMetadata, ColumnNameSet.NAME_COMPARATOR); - - String[] originNames = getNames(metadata); - String[] sortedNames = getNames(sortedMetadata); + @Nullable + private Map indexMap; - this.originMetadata = metadata; - this.sortedMetadata = sortedMetadata; - this.nameSet = ColumnNameSet.of(originNames, sortedNames); - - break; - } + /** + * Visible for testing + */ + @VisibleForTesting + MySqlRowDescriptor(MySqlColumnDescriptor[] metadata) { + originMetadata = metadata; } @Override @@ -78,24 +58,43 @@ public MySqlColumnDescriptor getColumnMetadata(int index) { return originMetadata[index]; } + private static Map createIndexMap(MySqlColumnDescriptor[] metadata) { + final int size = metadata.length; + final Map map = new HashMap<>(size); + + for (int i = 0; i < size; ++i) { + map.putIfAbsent(metadata[i].getName().toLowerCase(Locale.ROOT), i); + } + + return map; + } + + private int find(final String name) { + Map indexMap = this.indexMap; + if (null == indexMap) { + indexMap = this.indexMap = createIndexMap(originMetadata); + } + return indexMap.getOrDefault(name.toLowerCase(Locale.ROOT), -1); + } + @Override public MySqlColumnDescriptor getColumnMetadata(String name) { requireNonNull(name, "name must not be null"); - int index = nameSet.findIndex(name); + final int index = find(name); if (index < 0) { throw new NoSuchElementException("Column name '" + name + "' does not exist"); } - return sortedMetadata[index]; + return originMetadata[index]; } @Override public boolean contains(String name) { requireNonNull(name, "name must not be null"); - return nameSet.contains(name); + return find(name) >= 0; } @Override @@ -105,15 +104,14 @@ public List getColumnMetadatas() { @Override public String toString() { - return "MySqlRowMetadata{metadata=" + Arrays.toString(originMetadata) + ", sortedNames=" + - Arrays.toString(nameSet.getSortedNames()) + '}'; + return "MySqlRowDescriptor{metadata=" + Arrays.toString(originMetadata) + '}'; } MySqlColumnDescriptor[] unwrap() { return originMetadata; } - static MySqlRowMetadata create(DefinitionMetadataMessage[] columns) { + static MySqlRowDescriptor create(DefinitionMetadataMessage[] columns) { int size = columns.length; MySqlColumnDescriptor[] metadata = new MySqlColumnDescriptor[size]; @@ -121,17 +119,6 @@ static MySqlRowMetadata create(DefinitionMetadataMessage[] columns) { metadata[i] = MySqlColumnDescriptor.create(i, columns[i]); } - return new MySqlRowMetadata(metadata); - } - - private static String[] getNames(MySqlColumnDescriptor[] metadata) { - int size = metadata.length; - String[] names = new String[size]; - - for (int i = 0; i < size; ++i) { - names[i] = metadata[i].getName(); - } - - return names; + return new MySqlRowDescriptor(metadata); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java similarity index 81% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java index 749086572..3aafb1a3e 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java @@ -16,6 +16,9 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlRow; +import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils; import io.asyncer.r2dbc.mysql.internal.util.OperatorUtils; @@ -49,16 +52,16 @@ import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** - * An implementation of {@link Result} representing the results of a query against the MySQL database. + * An implementation of {@link MySqlResult} representing the results of a query against the MySQL database. *

- * A {@link Segment} provided by this implementation may be both {@link UpdateCount} and {@link RowSegment}, - * see also {@link MySqlOkSegment}. + * A {@link Segment} provided by this implementation may be both {@link UpdateCount} and {@link RowSegment}, see also + * {@link MySqlOkSegment}. */ -public final class MySqlResult implements Result { +final class MySqlSegmentResult implements MySqlResult { private final Flux segments; - private MySqlResult(Flux segments) { + private MySqlSegmentResult(Flux segments) { this.segments = segments; } @@ -81,7 +84,7 @@ public Flux map(BiFunction f) { return segments.handle((segment, sink) -> { if (segment instanceof RowSegment) { - Row row = ((RowSegment) segment).row(); + MySqlRow row = ((RowSegment) segment).row(); try { sink.next(f.apply(row, row.getMetadata())); @@ -116,10 +119,10 @@ public Flux map(Function f) { } @Override - public MySqlResult filter(Predicate filter) { + public MySqlResult filter(Predicate filter) { requireNonNull(filter, "filter must not be null"); - return new MySqlResult(segments.filter(segment -> { + return new MySqlSegmentResult(segments.filter(segment -> { if (filter.test(segment)) { return true; } @@ -133,7 +136,7 @@ public MySqlResult filter(Predicate filter) { } @Override - public Flux flatMap(Function> f) { + public Flux flatMap(Function> f) { requireNonNull(f, "mapping function must not be null"); return segments.flatMap(segment -> { @@ -154,15 +157,15 @@ public Flux flatMap(Function> f }); } - static MySqlResult toResult(boolean binary, Codecs codecs, ConnectionContext context, - @Nullable String syntheticKeyName, Flux messages) { + static MySqlResult toResult(boolean binary, Client client, Codecs codecs, + @Nullable String syntheticKeyName, Flux messages) { + requireNonNull(client, "client must not be null"); requireNonNull(codecs, "codecs must not be null"); - requireNonNull(context, "context must not be null"); requireNonNull(messages, "messages must not be null"); - return new MySqlResult(OperatorUtils.discardOnCancel(messages) + return new MySqlSegmentResult(OperatorUtils.discardOnCancel(messages) .doOnDiscard(ReferenceCounted.class, ReferenceCounted::release) - .handle(new MySqlSegments(binary, codecs, context, syntheticKeyName))); + .handle(new MySqlSegments(binary, client, codecs, syntheticKeyName))); } private static final class MySqlMessage implements Message { @@ -200,14 +203,14 @@ private static final class MySqlRowSegment extends AbstractReferenceCounted impl private final FieldValue[] fields; - private MySqlRowSegment(FieldValue[] fields, MySqlRowMetadata metadata, Codecs codecs, boolean binary, + private MySqlRowSegment(FieldValue[] fields, MySqlRowDescriptor metadata, Codecs codecs, boolean binary, ConnectionContext context) { - this.row = new MySqlRow(fields, metadata, codecs, binary, context); + this.row = new MySqlDataRow(fields, metadata, codecs, binary, context); this.fields = fields; } @Override - public Row row() { + public MySqlRow row() { return row; } @@ -226,11 +229,14 @@ protected void deallocate() { } } + @SuppressWarnings("checkstyle:FinalClass") private static class MySqlUpdateCount implements UpdateCount { - protected final long rows; + private final long rows; - private MySqlUpdateCount(long rows) { this.rows = rows; } + private MySqlUpdateCount(long rows) { + this.rows = rows; + } @Override public long value() { @@ -255,7 +261,7 @@ private MySqlOkSegment(long rows, long lastInsertId, Codecs codecs, String keyNa } @Override - public Row row() { + public MySqlRow row() { return new InsertSyntheticRow(codecs, keyName, lastInsertId); } } @@ -264,22 +270,21 @@ private static final class MySqlSegments implements BiConsumer sink) { // Updated rows can be identified either by OK or rows in case of RETURNING rowCount.getAndIncrement(); - MySqlRowMetadata metadata = this.rowMetadata; + MySqlRowDescriptor metadata = this.rowMetadata; if (metadata == null) { ReferenceCountUtil.safeRelease(message); - sink.error(new IllegalStateException("No MySqlRowMetadata available")); + sink.error(new IllegalStateException("No metadata available")); return; } @@ -305,7 +310,7 @@ public void accept(ServerMessage message, SynchronousSink sink) { ReferenceCountUtil.safeRelease(message); } - sink.next(new MySqlRowSegment(fields, metadata, codecs, binary, context)); + sink.next(new MySqlRowSegment(fields, metadata, codecs, binary, client.getContext())); } else if (message instanceof SyntheticMetadataMessage) { DefinitionMetadataMessage[] metadataMessages = ((SyntheticMetadataMessage) message).unwrap(); @@ -313,11 +318,11 @@ public void accept(ServerMessage message, SynchronousSink sink) { return; } - this.rowMetadata = MySqlRowMetadata.create(metadataMessages); + this.rowMetadata = MySqlRowDescriptor.create(metadataMessages); } else if (message instanceof OkMessage) { OkMessage msg = (OkMessage) message; - if (MySqlStatementSupport.supportReturning(context) && msg.isEndOfRows()) { + if (MySqlStatementSupport.supportReturning(client.getContext()) && msg.isEndOfRows()) { sink.next(new MySqlUpdateCount(rowCount.getAndSet(0))); } else { long rows = msg.getAffectedRows(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java new file mode 100644 index 000000000..8f8665a60 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java @@ -0,0 +1,320 @@ +/* + * Copyright 2023 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.api.MySqlBatch; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.api.MySqlConnectionMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; +import io.asyncer.r2dbc.mysql.cache.QueryCache; +import io.asyncer.r2dbc.mysql.client.Client; +import io.asyncer.r2dbc.mysql.codec.Codecs; +import io.asyncer.r2dbc.mysql.internal.util.StringUtils; +import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; +import io.asyncer.r2dbc.mysql.message.server.ErrorMessage; +import io.asyncer.r2dbc.mysql.message.server.ServerMessage; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.TransactionDefinition; +import io.r2dbc.spi.ValidationDepth; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.function.Function; +import java.util.function.Predicate; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty; +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; + +/** + * An implementation of {@link MySqlConnection} for connecting to the MySQL database. + */ +final class MySqlSimpleConnection implements MySqlConnection { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(MySqlSimpleConnection.class); + + private static final String PING_MARKER = "/* ping */"; + + private static final Function VALIDATE = message -> { + if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { + return true; + } + + if (message instanceof ErrorMessage) { + ErrorMessage msg = (ErrorMessage) message; + logger.debug("Remote validate failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), msg.getMessage()); + } else { + ReferenceCountUtil.safeRelease(message); + } + + return false; + }; + + private final Client client; + + private final Codecs codecs; + + private final MySqlConnectionMetadata metadata; + + private final QueryCache queryCache; + + @Nullable + private final Predicate prepare; + + // TODO: Check it when executing + private final boolean batchSupported; + + MySqlSimpleConnection(Client client, Codecs codecs, QueryCache queryCache, @Nullable Predicate prepare) { + ConnectionContext context = client.getContext(); + + this.client = client; + this.codecs = codecs; + this.metadata = new MySqlClientConnectionMetadata(client); + this.queryCache = queryCache; + this.prepare = prepare; + this.batchSupported = context.getCapability().isMultiStatementsAllowed(); + + if (this.batchSupported) { + logger.debug("Batch is supported by server"); + } else { + logger.warn("The MySQL server does not support batch, fallback to executing one-by-one"); + } + } + + @Override + public Mono beginTransaction() { + return beginTransaction(MySqlTransactionDefinition.empty()); + } + + @Override + public Mono beginTransaction(TransactionDefinition definition) { + return Mono.defer(() -> QueryFlow.beginTransaction(client, batchSupported, definition)); + } + + @Override + public Mono close() { + Mono closer = client.close(); + + if (logger.isDebugEnabled()) { + return closer.doOnSubscribe(s -> logger.debug("Connection closing")) + .doOnSuccess(ignored -> logger.debug("Connection close succeed")); + } + + return closer; + } + + @Override + public Mono commitTransaction() { + return Mono.defer(() -> QueryFlow.doneTransaction(client, true, batchSupported)); + } + + @Override + public MySqlBatch createBatch() { + return batchSupported ? new MySqlBatchingBatch(client, codecs) : new MySqlSyntheticBatch(client, codecs); + } + + @Override + public Mono createSavepoint(String name) { + requireNonEmpty(name, "Savepoint name must not be empty"); + + return QueryFlow.createSavepoint(client, name, batchSupported); + } + + @Override + public MySqlStatement createStatement(String sql) { + requireNonNull(sql, "sql must not be null"); + + if (sql.startsWith(PING_MARKER)) { + return new PingStatement(client, codecs); + } + + Query query = queryCache.get(sql); + + if (query.isSimple()) { + if (prepare != null && prepare.test(sql)) { + logger.debug("Create a simple statement provided by prepare query"); + return new PrepareSimpleStatement(client, codecs, sql); + } + + logger.debug("Create a simple statement provided by text query"); + + return new TextSimpleStatement(client, codecs, sql); + } + + if (prepare == null) { + logger.debug("Create a parameterized statement provided by text query"); + return new TextParameterizedStatement(client, codecs, query); + } + + logger.debug("Create a parameterized statement provided by prepare query"); + + return new PrepareParameterizedStatement(client, codecs, query); + } + + @Override + public Mono postAllocate() { + return Mono.empty(); + } + + @Override + public Mono preRelease() { + // Rollback if the connection is in transaction. + return rollbackTransaction(); + } + + @Override + public Mono releaseSavepoint(String name) { + requireNonEmpty(name, "Savepoint name must not be empty"); + + return QueryFlow.executeVoid(client, "RELEASE SAVEPOINT " + StringUtils.quoteIdentifier(name)); + } + + @Override + public Mono rollbackTransaction() { + return Mono.defer(() -> QueryFlow.doneTransaction(client, false, batchSupported)); + } + + @Override + public Mono rollbackTransactionToSavepoint(String name) { + requireNonEmpty(name, "Savepoint name must not be empty"); + + return QueryFlow.executeVoid(client, "ROLLBACK TO SAVEPOINT " + StringUtils.quoteIdentifier(name)); + } + + @Override + public MySqlConnectionMetadata getMetadata() { + return metadata; + } + + /** + * MySQL does not have a way to query the isolation level of the current transaction, only inferred from past + * statements, so driver can not make sure the result is right. + *

+ * See MySQL Bug 53341 + *

+ * {@inheritDoc} + */ + @Override + public IsolationLevel getTransactionIsolationLevel() { + return client.getContext().getCurrentIsolationLevel(); + } + + @Override + public Mono setTransactionIsolationLevel(IsolationLevel isolationLevel) { + requireNonNull(isolationLevel, "isolationLevel must not be null"); + + // Set subsequent transaction isolation level. + return QueryFlow.executeVoid(client, + "SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql()) + .doOnSuccess(ignored -> { + ConnectionContext context = client.getContext(); + + context.setSessionIsolationLevel(isolationLevel); + if (!context.isInTransaction()) { + context.setCurrentIsolationLevel(isolationLevel); + } + }); + } + + @Override + public Mono validate(ValidationDepth depth) { + requireNonNull(depth, "depth must not be null"); + + if (depth == ValidationDepth.LOCAL) { + return Mono.fromSupplier(client::isConnected); + } + + return Mono.defer(() -> { + if (!client.isConnected()) { + return Mono.just(false); + } + + return QueryFlow.ping(client) + .map(VALIDATE) + .last() + .onErrorResume(e -> { + // `last` maybe emit a NoSuchElementException, exchange maybe emit exception by Netty. + // But should NEVER emit any exception, so logging exception and emit false. + logger.debug("Remote validate failed", e); + return Mono.just(false); + }); + }); + } + + @Override + public boolean isAutoCommit() { + return client.getContext().isAutoCommit(); + } + + @Override + public Mono setAutoCommit(boolean autoCommit) { + return Mono.defer(() -> QueryFlow.executeVoid(client, "SET autocommit=" + (autoCommit ? 1 : 0))); + } + + @Override + public Mono setLockWaitTimeout(Duration timeout) { + requireNonNull(timeout, "timeout must not be null"); + + if (client.getContext().isLockWaitTimeoutSupported()) { + return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(timeout)) + .doOnSuccess(ignored -> client.getContext().setAllLockWaitTimeout(timeout)); + } + + logger.warn("Lock wait timeout is not supported by server, setLockWaitTimeout operation is ignored"); + return Mono.empty(); + + } + + @Override + public Mono setStatementTimeout(Duration timeout) { + requireNonNull(timeout, "timeout must not be null"); + + ConnectionContext context = client.getContext(); + + // mariadb: https://mariadb.com/kb/en/aborting-statements/ + // mysql: https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/ + // ref: https://github.com/mariadb-corporation/mariadb-connector-r2dbc + if (context.isStatementTimeoutSupported()) { + String variable = StringUtils.statementTimeoutVariable(timeout, context.isMariaDb()); + return QueryFlow.setSessionVariable(client, variable); + } + + return Mono.error( + new R2dbcNonTransientResourceException( + "Statement timeout is not supported by server version " + context.getServerVersion(), + "HY000", + -1 + ) + ); + } + + /** + * Visible only for testing. + * + * @return current connection context + */ + @TestOnly + ConnectionContext context() { + return client.getContext(); + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java similarity index 99% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java index 2a4b1c0fa..d76662f40 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java @@ -29,7 +29,7 @@ import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_STRINGS; /** - * MySQL configuration of SSL. + * A configuration of MySQL SSL connection. */ public final class MySqlSslConfiguration { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java similarity index 87% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java index 696626ba0..5b40500ee 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java @@ -16,6 +16,8 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import org.jetbrains.annotations.Nullable; @@ -31,19 +33,20 @@ abstract class MySqlStatementSupport implements MySqlStatement { private static final String LAST_INSERT_ID = "LAST_INSERT_ID"; - protected final ConnectionContext context; + protected final Client client; @Nullable private String[] generatedColumns = null; - MySqlStatementSupport(ConnectionContext context) { - this.context = requireNonNull(context, "context must not be null"); + MySqlStatementSupport(Client client) { + this.client = requireNonNull(client, "client must not be null"); } @Override public final MySqlStatement returnGeneratedValues(String... columns) { requireNonNull(columns, "columns must not be null"); + ConnectionContext context = client.getContext(); int len = columns.length; if (len == 0) { @@ -70,7 +73,7 @@ final String syntheticKeyName() { String[] columns = this.generatedColumns; // MariaDB should use `RETURNING` clause instead. - if (columns == null || supportReturning(this.context)) { + if (columns == null || supportReturning(this.client.getContext())) { return null; } @@ -84,7 +87,7 @@ final String syntheticKeyName() { final String returningIdentifiers() { String[] columns = this.generatedColumns; - if (columns == null || !supportReturning(context)) { + if (columns == null || !supportReturning(this.client.getContext())) { return ""; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java similarity index 82% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java index 87325591e..73640a82e 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java @@ -16,6 +16,8 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlBatch; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import reactor.core.publisher.Flux; @@ -29,20 +31,17 @@ * An implementation of {@link MySqlBatch} for executing a collection of statements in one-by-one against the * MySQL database. */ -final class MySqlSyntheticBatch extends MySqlBatch { +final class MySqlSyntheticBatch implements MySqlBatch { private final Client client; private final Codecs codecs; - private final ConnectionContext context; - private final List statements = new ArrayList<>(); - MySqlSyntheticBatch(Client client, Codecs codecs, ConnectionContext context) { + MySqlSyntheticBatch(Client client, Codecs codecs) { this.client = requireNonNull(client, "client must not be null"); this.codecs = requireNonNull(codecs, "codecs must not be null"); - this.context = requireNonNull(context, "context must not be null"); } @Override @@ -54,7 +53,7 @@ public MySqlBatch add(String sql) { @Override public Flux execute() { return QueryFlow.execute(client, statements) - .map(messages -> MySqlResult.toResult(false, codecs, context, null, messages)); + .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, null, messages)); } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java similarity index 81% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java index fab2bcc15..f5c9af4ed 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java @@ -33,42 +33,44 @@ * and 1073741824. * * @since 0.9.0 + * @deprecated since 1.1.3, use {@link io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition} instead. */ +@Deprecated public final class MySqlTransactionDefinition implements TransactionDefinition { /** - * Use {@code WITH CONSISTENT SNAPSHOT} syntax, all MySQL-compatible servers should support this syntax. + * Use {@code WITH CONSISTENT SNAPSHOT} property. + *

* The option starts a consistent read for storage engines such as InnoDB and XtraDB that can do so, the * same as if a {@code START TRANSACTION} followed by a {@code SELECT ...} from any InnoDB table was * issued. - *

- * NOTICE: This option and {@link #READ_ONLY} cannot be enabled at the same definition. */ - public static final Option WITH_CONSISTENT_SNAPSHOT = Option.valueOf("withConsistentSnapshot"); + public static final Option WITH_CONSISTENT_SNAPSHOT = + io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT; /** - * Use {@code START TRANSACTION WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar syntax. - * Only available when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}. + * Use {@code WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar property. Only available + * when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}. *

- * NOTICE: This is an extended syntax for special servers. Before using it, check whether the server - * supports the syntax. + * Note: This is an extended syntax based on specific distributions. Please check whether the server + * supports this property before using it. */ - public static final Option CONSISTENT_SNAPSHOT_ENGINE = - Option.valueOf("consistentSnapshotEngine"); + public static final Option CONSISTENT_SNAPSHOT_ENGINE = + io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE; /** - * Use {@code START TRANSACTION WITH CONSISTENT SNAPSHOT FROM SESSION [session_id]} for Percona/MySQL or - * similar syntax. Only available when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}. + * Use {@code WITH CONSISTENT SNAPSHOT FROM SESSION [session_id]} for Percona/MySQL or similar property. + * Only available when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}. *

- * The {@code session_id} is the session identifier reported in the {@code Id} column of the process list. - * Reported by {@code SHOW COLUMNS FROM performance_schema.processlist}, it should be an unsigned 64-bit - * integer. Use {@code SHOW PROCESSLIST} to find session identifier of the process list. + * The {@code session_id} is received by {@code SHOW COLUMNS FROM performance_schema.processlist}, it + * should be an unsigned 64-bit integer. Use {@code SHOW PROCESSLIST} to find session identifier of the + * process list. *

- * NOTICE: This is an extended syntax for special servers. Before using it, check whether the server - * supports the syntax. + * Note: This is an extended syntax based on specific distributions. Please check whether the server + * supports this property before using it. */ public static final Option CONSISTENT_SNAPSHOT_FROM_SESSION = - Option.valueOf("consistentSnapshotFromSession"); + io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_FROM_SESSION; private static final MySqlTransactionDefinition EMPTY = new MySqlTransactionDefinition(Collections.emptyMap()); @@ -187,7 +189,7 @@ public Builder withConsistentSnapshot(@Nullable Boolean withConsistentSnapshot) * @return this builder. */ public Builder consistentSnapshotEngine(@Nullable ConsistentSnapshotEngine snapshotEngine) { - return option(CONSISTENT_SNAPSHOT_ENGINE, snapshotEngine); + return option(CONSISTENT_SNAPSHOT_ENGINE, snapshotEngine == null ? null : snapshotEngine.asSql()); } /** @@ -200,7 +202,7 @@ public Builder consistentSnapshotFromSession(@Nullable Long sessionId) { return option(CONSISTENT_SNAPSHOT_FROM_SESSION, sessionId); } - private Builder option(Option key, @Nullable T value) { + private Builder option(Option key, @Nullable Object value) { if (value == null) { this.options.remove(key); } else { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java similarity index 52% rename from src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java index 2ab077c0c..9217367fa 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java @@ -16,12 +16,13 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlNativeTypeMetadata; import io.asyncer.r2dbc.mysql.collation.CharCollation; /** - * A flag bitmap considers column definitions. + * An implementation of {@link MySqlNativeTypeMetadata}. */ -public final class ColumnDefinition { +final class MySqlTypeMetadata implements MySqlNativeTypeMetadata { private static final short NOT_NULL = 1; @@ -48,75 +49,61 @@ public final class ColumnDefinition { private static final short ALL_USED = NOT_NULL | UNSIGNED | BINARY | ENUM | SET; + private final int typeId; + /** - * The original bitmap of {@link ColumnDefinition this}. + * The original bitmap of definitions. *

* MySQL uses 32-bits definition flags, but only returns the lower 16-bits. */ - private final short bitmap; + private final short definitions; /** - * collation id(or charset number) + * The character collation id of the column. *

* collationId > 0 when protocol version == 4.1, 0 otherwise. */ private final int collationId; - private ColumnDefinition(short bitmap, int collationId) { - this.bitmap = bitmap; + MySqlTypeMetadata(int typeId, int definitions, int collationId) { + this.typeId = typeId; + this.definitions = (short) (definitions & ALL_USED); this.collationId = collationId; } - /** - * Checks if value is not null. - * - * @return if value is not null. - */ + @Override + public int getTypeId() { + return typeId; + } + + @Override public boolean isNotNull() { - return (bitmap & NOT_NULL) != 0; + return (definitions & NOT_NULL) != 0; } - /** - * Checks if value is an unsigned number. e.g. INT UNSIGNED, BIGINT UNSIGNED. - *

- * Note: IEEE-754 floating types (e.g. DOUBLE/FLOAT) do not supports it in MySQL 8.0+. When creating a - * column as an unsigned floating type, the server may report a warning. - * - * @return if value is an unsigned number. - */ + @Override public boolean isUnsigned() { - return (bitmap & UNSIGNED) != 0; + return (definitions & UNSIGNED) != 0; } - /** - * Checks if value is binary data. - * - * @return if value is binary data. - */ + @Override public boolean isBinary() { // Utilize collationId to ascertain whether it is binary or not. // This is necessary since the union of JSON columns, varchar binary, and char binary // results in a bitmap with the BINARY flag set. // see: https://github.com/asyncer-io/r2dbc-mysql/issues/91 - return collationId == 0 & (bitmap & BINARY) != 0 | collationId == CharCollation.BINARY_ID; + // FIXME: use collationId to check, definitions is not reliable even in protocol version < 4.1 + return (collationId == 0 && (definitions & BINARY) != 0) || collationId == CharCollation.BINARY_ID; } - /** - * Checks if value type is enum. - * - * @return if value is an enum. - */ + @Override public boolean isEnum() { - return (bitmap & ENUM) != 0; + return (definitions & ENUM) != 0; } - /** - * Checks if value type is set. - * - * @return if value is a set. - */ + @Override public boolean isSet() { - return (bitmap & SET) != 0; + return (definitions & SET) != 0; } @Override @@ -124,45 +111,26 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (!(o instanceof ColumnDefinition)) { + if (!(o instanceof MySqlTypeMetadata)) { return false; } - ColumnDefinition that = (ColumnDefinition) o; + MySqlTypeMetadata that = (MySqlTypeMetadata) o; - return bitmap == that.bitmap & collationId == that.collationId; + return typeId == that.typeId && definitions == that.definitions && collationId == that.collationId; } @Override public int hashCode() { - return bitmap; + int result = 31 * typeId + (int) definitions; + return 31 * result + collationId; } @Override public String toString() { - return "ColumnDefinition<0x" + Integer.toHexString(bitmap) + ", 0x" + Integer.toHexString(collationId)+ '>'; - } - - /** - * Creates a {@link ColumnDefinition} with column definitions bitmap. It will unset all unknown or useless - * flags. - * - * @param definitions the column definitions bitmap. - * @return the {@link ColumnDefinition} without unknown or useless flags. - */ - public static ColumnDefinition of(int definitions) { - return new ColumnDefinition((short) (definitions & ALL_USED), 0); - } - - /** - * Creates a {@link ColumnDefinition} with column definitions bitmap. It will unset all unknown or useless - * flags. - * - * @param definitions the column definitions bitmap. - * @param collationId the collation id. - * @return the {@link ColumnDefinition} without unknown or useless flags. - */ - public static ColumnDefinition of(int definitions, int collationId) { - return new ColumnDefinition((short) (definitions & ALL_USED), collationId); + return "MySqlTypeMetadata{typeId=" + typeId + + ", definitions=0x" + Integer.toHexString(definitions) + + ", collationId=" + collationId + + '}'; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java similarity index 81% rename from src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java index 218cc11df..f75a913f1 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java @@ -23,6 +23,7 @@ import java.util.Collection; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.IntFunction; import java.util.function.Predicate; /** @@ -55,7 +56,9 @@ final class Source { @Nullable private final T value; - private Source(@Nullable T value) { this.value = value; } + private Source(@Nullable T value) { + this.value = value; + } Otherwise to(Consumer consumer) { if (value == null) { @@ -105,21 +108,39 @@ Source as(Class type, Function mapping) { throw new IllegalArgumentException(toMessage(value, type.getTypeName())); } - Source asStrings() { + Source asArray(Class arrayType, Function mapper, + Function splitter, IntFunction generator) { if (value == null) { return nilSource(); } - if (value instanceof String[]) { - return new Source<>((String[]) value); + if (arrayType.isInstance(value)) { + return new Source<>(arrayType.cast(value)); + } else if (value instanceof String[]) { + return new Source<>(mapArray((String[]) value, mapper, generator)); } else if (value instanceof String) { - return new Source<>(((String) value).split(",")); + String[] strings = splitter.apply((String) value); + + if (arrayType.isInstance(strings)) { + return new Source<>(arrayType.cast(strings)); + } + + return new Source<>(mapArray(strings, mapper, generator)); } else if (value instanceof Collection) { - return new Source<>(((Collection) value).stream() - .map(String.class::cast).toArray(String[]::new)); + @SuppressWarnings("unchecked") + Class type = (Class) arrayType.getComponentType(); + R[] array = ((Collection) value).stream().map(e -> { + if (type.isInstance(e)) { + return type.cast(e); + } else { + return mapper.apply(e.toString()); + } + }).toArray(generator); + + return new Source<>(array); } - throw new IllegalArgumentException(toMessage(value, "String[]")); + throw new IllegalArgumentException(toMessage(value, arrayType.getTypeName())); } Source asBoolean() { @@ -236,6 +257,16 @@ private static Source nilSource() { private static String toMessage(Object value, String type) { return "Cannot convert value " + value + " to " + type; } + + private static O[] mapArray(String[] input, Function mapper, IntFunction generator) { + O[] output = generator.apply(input.length); + + for (int i = 0; i < input.length; i++) { + output[i] = mapper.apply(input[i]); + } + + return output; + } } enum Otherwise { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java index 62ac43e29..590762bc4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java @@ -25,7 +25,7 @@ import java.nio.ByteBuffer; /** - * A writer for {@link MySqlParameter}s of parametrized statements with text-based protocol. + * A writer for {@link MySqlParameter}s of parameterized statements with text-based protocol. */ public abstract class ParameterWriter extends Writer { @@ -38,7 +38,7 @@ public abstract class ParameterWriter extends Writer { /** * Writes a value of {@code int} to current parameter. If current mode is string mode, it will write as a - * string like {@code write(String.valueOf(value))}. If it write as a numeric, nothing else can be written + * string like {@code write(String.valueOf(value))}. If write as a numeric, nothing else can be written * before or after this. * * @param value the value of {@code int}. @@ -68,7 +68,7 @@ public abstract class ParameterWriter extends Writer { /** * Writes a value of {@link BigInteger} to current parameter. If current mode is string mode, it will - * write as a string like {@code write(value.toString())}. If it write as a numeric, nothing else can be + * write as a string like {@code write(value.toString())}. If write as a numeric, nothing else can be * written before or after this. * * @param value the value of {@link BigInteger}. @@ -79,7 +79,7 @@ public abstract class ParameterWriter extends Writer { /** * Writes a value of {@code float} to current parameter. If current mode is string mode, it will write as - * a string like {@code write(String.valueOf(value))}. If it write as a numeric, nothing else can be + * a string like {@code write(String.valueOf(value))}. If write as a numeric, nothing else can be * written before or after this. * * @param value the value of {@code float}. @@ -89,7 +89,7 @@ public abstract class ParameterWriter extends Writer { /** * Writes a value of {@code double} to current parameter. If current mode is string mode, it will write as - * a string like {@code write(String.valueOf(value))}. If it write as a numeric, nothing else can be + * a string like {@code write(String.valueOf(value))}. If write as a numeric, nothing else can be * written before or after this. * * @param value the value of {@code double}. @@ -99,7 +99,7 @@ public abstract class ParameterWriter extends Writer { /** * Writes a value of {@link BigDecimal} to current parameter. If current mode is string mode, it will - * write as a string like {@code write(value.toString())}. If it write as a numeric, nothing else can be + * write as a string like {@code write(value.toString())}. If write as a numeric, nothing else can be * written before or after this. * * @param value the value of {@link BigDecimal}. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java similarity index 88% rename from src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java index ffe203077..2b1d64ed5 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java @@ -16,6 +16,8 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import reactor.core.publisher.Flux; @@ -32,14 +34,12 @@ import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** - * Base class considers parametrized {@link MySqlStatement} with parameter markers. + * Base class considers parameterized {@link MySqlStatement} with parameter markers. *

- * MySQL uses indexed parameters which are marked by {@literal ?} without naming. Implementations should uses + * MySQL uses indexed parameters which are marked by {@literal ?} without naming. Implementations should use * {@link Query} to supports named parameters. */ -abstract class ParametrizedStatementSupport extends MySqlStatementSupport { - - protected final Client client; +abstract class ParameterizedStatementSupport extends MySqlStatementSupport { protected final Codecs codecs; @@ -49,13 +49,12 @@ abstract class ParametrizedStatementSupport extends MySqlStatementSupport { private final AtomicBoolean executed = new AtomicBoolean(); - ParametrizedStatementSupport(Client client, Codecs codecs, Query query, ConnectionContext context) { - super(context); + ParameterizedStatementSupport(Client client, Codecs codecs, Query query) { + super(client); requireNonNull(query, "query must not be null"); require(query.getParameters() > 0, "parameters must be a positive integer"); - this.client = requireNonNull(client, "client must not be null"); this.codecs = requireNonNull(codecs, "codecs must not be null"); this.query = query; this.bindings = new Bindings(query.getParameters()); @@ -73,7 +72,7 @@ public final MySqlStatement add() { public final MySqlStatement bind(int index, Object value) { requireNonNull(value, "value must not be null"); - addBinding(index, codecs.encode(value, context)); + addBinding(index, codecs.encode(value, client.getContext())); return this; } @@ -82,7 +81,7 @@ public final MySqlStatement bind(String name, Object value) { requireNonNull(name, "name must not be null"); requireNonNull(value, "value must not be null"); - addBinding(getIndexes(name), codecs.encode(value, context)); + addBinding(getIndexes(name), codecs.encode(value, client.getContext())); return this; } @@ -106,7 +105,7 @@ public final MySqlStatement bindNull(String name, Class type) { } @Override - public final Flux execute() { + public final Flux execute() { if (bindings.bindings.isEmpty()) { throw new IllegalStateException("No parameters bound for current statement"); } @@ -114,14 +113,14 @@ public final Flux execute() { return Flux.defer(() -> { if (!executed.compareAndSet(false, true)) { - return Flux.error(new IllegalStateException("Parametrized statement was already executed")); + return Flux.error(new IllegalStateException("Parameterized statement was already executed")); } return execute(bindings.bindings); }); } - abstract protected Flux execute(List bindings); + protected abstract Flux execute(List bindings); /** * Get parameter index(es) by parameter name. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java similarity index 78% rename from src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java index d11717a34..9cdc43c95 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java @@ -16,24 +16,25 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * An implementation of {@link MySqlStatement} considers the lightweight ping syntax. */ final class PingStatement implements MySqlStatement { - private final MySqlConnection connection; + private final Client client; private final Codecs codecs; - private final ConnectionContext context; - - PingStatement(MySqlConnection connection, Codecs codecs, ConnectionContext context) { - this.connection = connection; + PingStatement(Client client, Codecs codecs) { + this.client = client; this.codecs = codecs; - this.context = context; } @Override @@ -63,7 +64,12 @@ public MySqlStatement bindNull(String name, Class type) { @Override public Flux execute() { - return Flux.just(MySqlResult.toResult(false, codecs, context, null, - connection.doPingInternal())); + return Flux.from(Mono.fromSupplier(() -> MySqlSegmentResult.toResult( + false, + client, + codecs, + null, + QueryFlow.ping(client) + ))); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java similarity index 68% rename from src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java index 3a946f3ea..44edd9509 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java @@ -16,7 +16,8 @@ package io.asyncer.r2dbc.mysql; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; @@ -27,27 +28,23 @@ import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; /** - * An implementation of {@link ParametrizedStatementSupport} based on MySQL prepare query. + * An implementation of {@link ParameterizedStatementSupport} based on MySQL prepare query. */ -final class PrepareParametrizedStatement extends ParametrizedStatementSupport { - - private final PrepareCache prepareCache; +final class PrepareParameterizedStatement extends ParameterizedStatementSupport { private int fetchSize = 0; - PrepareParametrizedStatement(Client client, Codecs codecs, Query query, ConnectionContext context, - PrepareCache prepareCache) { - super(client, codecs, query, context); - this.prepareCache = prepareCache; + PrepareParameterizedStatement(Client client, Codecs codecs, Query query) { + super(client, codecs, query); } @Override public Flux execute(List bindings) { return Flux.defer(() -> QueryFlow.execute(client, StringUtils.extendReturning(query.getFormattedSql(), returningIdentifiers()), - bindings, fetchSize, prepareCache + bindings, fetchSize )) - .map(messages -> MySqlResult.toResult(true, codecs, context, syntheticKeyName(), messages)); + .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages)); } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java similarity index 77% rename from src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java index 2284b991e..29a2b8232 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java @@ -16,7 +16,8 @@ package io.asyncer.r2dbc.mysql; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; @@ -34,21 +35,17 @@ final class PrepareSimpleStatement extends SimpleStatementSupport { private static final List BINDINGS = Collections.singletonList(new Binding(0)); - private final PrepareCache prepareCache; - private int fetchSize = 0; - PrepareSimpleStatement(Client client, Codecs codecs, ConnectionContext context, String sql, - PrepareCache prepareCache) { - super(client, codecs, context, sql); - this.prepareCache = prepareCache; + PrepareSimpleStatement(Client client, Codecs codecs, String sql) { + super(client, codecs, sql); } @Override public Flux execute() { return Flux.defer(() -> QueryFlow.execute(client, - StringUtils.extendReturning(sql, returningIdentifiers()), BINDINGS, fetchSize, prepareCache)) - .map(messages -> MySqlResult.toResult(true, codecs, context, syntheticKeyName(), messages)); + StringUtils.extendReturning(sql, returningIdentifiers()), BINDINGS, fetchSize)) + .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages)); } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Query.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Query.java similarity index 99% rename from src/main/java/io/asyncer/r2dbc/mysql/Query.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Query.java index 1fe94ee54..49c063e61 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/Query.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Query.java @@ -167,7 +167,7 @@ public static Query parse(String sql) { } Map nameKeyedParams = new HashMap<>(); - // Used by singleton map, if SQL does not contains named-parameter, it will always be empty. + // Used by singleton map, if SQL does not contain named-parameter, it will always be empty. String anyName = ""; // The last parameter end index (whatever named or not) of sql. int lastParamEnd = 0; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java similarity index 69% rename from src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 26721b911..3bae1dcc3 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -16,18 +16,14 @@ package io.asyncer.r2dbc.mysql; -import io.asyncer.r2dbc.mysql.authentication.MySqlAuthProvider; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; +import io.asyncer.r2dbc.mysql.api.MySqlBatch; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.client.FluxExchangeable; import io.asyncer.r2dbc.mysql.constant.ServerStatuses; -import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; -import io.asyncer.r2dbc.mysql.message.client.AuthResponse; import io.asyncer.r2dbc.mysql.message.client.ClientMessage; -import io.asyncer.r2dbc.mysql.message.client.HandshakeResponse; import io.asyncer.r2dbc.mysql.message.client.LocalInfileResponse; -import io.asyncer.r2dbc.mysql.message.client.SubsequenceClientMessage; import io.asyncer.r2dbc.mysql.message.client.PingMessage; import io.asyncer.r2dbc.mysql.message.client.PrepareQueryMessage; import io.asyncer.r2dbc.mysql.message.client.PreparedCloseMessage; @@ -35,28 +31,21 @@ import io.asyncer.r2dbc.mysql.message.client.PreparedFetchMessage; import io.asyncer.r2dbc.mysql.message.client.PreparedResetMessage; import io.asyncer.r2dbc.mysql.message.client.PreparedTextQueryMessage; -import io.asyncer.r2dbc.mysql.message.client.SslRequest; import io.asyncer.r2dbc.mysql.message.client.TextQueryMessage; -import io.asyncer.r2dbc.mysql.message.server.AuthMoreDataMessage; -import io.asyncer.r2dbc.mysql.message.server.ChangeAuthMessage; import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; import io.asyncer.r2dbc.mysql.message.server.EofMessage; import io.asyncer.r2dbc.mysql.message.server.ErrorMessage; -import io.asyncer.r2dbc.mysql.message.server.HandshakeHeader; -import io.asyncer.r2dbc.mysql.message.server.HandshakeRequest; import io.asyncer.r2dbc.mysql.message.server.LocalInfileRequest; import io.asyncer.r2dbc.mysql.message.server.OkMessage; import io.asyncer.r2dbc.mysql.message.server.PreparedOkMessage; import io.asyncer.r2dbc.mysql.message.server.ServerMessage; import io.asyncer.r2dbc.mysql.message.server.ServerStatusMessage; import io.asyncer.r2dbc.mysql.message.server.SyntheticMetadataMessage; -import io.asyncer.r2dbc.mysql.message.server.SyntheticSslResponseMessage; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import io.r2dbc.spi.IsolationLevel; -import io.r2dbc.spi.R2dbcPermissionDeniedException; import io.r2dbc.spi.TransactionDefinition; import org.jetbrains.annotations.Nullable; import reactor.core.CoreSubscriber; @@ -70,18 +59,17 @@ import java.time.Duration; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Predicate; /** - * A message flow considers both of parametrized and text queries, such as {@link TextParametrizedStatement}, - * {@link PrepareParametrizedStatement}, {@link TextSimpleStatement}, {@link PrepareSimpleStatement} and + * A message flow considers both of parameterized and text queries, such as {@link TextParameterizedStatement}, + * {@link PrepareParameterizedStatement}, {@link TextSimpleStatement}, {@link PrepareSimpleStatement} and * {@link MySqlBatch}. */ final class QueryFlow { @@ -99,37 +87,47 @@ final class QueryFlow { } }; + private static final BiConsumer> PING = (message, sink) -> { + if (message instanceof ErrorMessage) { + sink.next(message); + sink.complete(); + } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { + sink.next(message); + sink.complete(); + } else { + ReferenceCountUtil.safeRelease(message); + } + }; + /** - * Execute multiple bindings of a server-preparing statement with one-by-one binary execution. The - * execution terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. If client - * receives a {@link ErrorMessage} will cancel subsequent {@link Binding}s. The exchange will be completed - * by {@link CompleteMessage} after receive the last result for the last binding. + * Execute multiple bindings of a server-preparing statement with one-by-one binary execution. The execution + * terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. If client receives a + * {@link ErrorMessage} will cancel subsequent {@link Binding}s. The exchange will be completed by + * {@link CompleteMessage} after receive the last result for the last binding. * * @param client the {@link Client} to exchange messages with. * @param sql the statement for exception tracing. * @param bindings the data of bindings. * @param fetchSize the size of fetching, if it less than or equal to {@literal 0} means fetch all rows. - * @param cache the cache of server-preparing result. * @return the messages received in response to this exchange. */ - static Flux> execute(Client client, String sql, List bindings, int fetchSize, - PrepareCache cache) { + static Flux> execute(Client client, String sql, List bindings, int fetchSize) { return Flux.defer(() -> { if (bindings.isEmpty()) { return Flux.empty(); } // Note: the prepared SQL may not be sent when the cache matches. - return client.exchange(new PrepareExchangeable(cache, sql, bindings.iterator(), fetchSize)) + return client.exchange(new PrepareExchangeable(client, sql, bindings.iterator(), fetchSize)) .windowUntil(RESULT_DONE); }); } /** - * Execute multiple bindings of a client-preparing statement with one-by-one text query. The execution - * terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} - * will emit an exception and cancel subsequent {@link Binding}s. This exchange will be completed by - * {@link CompleteMessage} after receive the last result for the last binding. + * Execute multiple bindings of a client-preparing statement with one-by-one text query. The execution terminates + * with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception + * and cancel subsequent {@link Binding}s. This exchange will be completed by {@link CompleteMessage} after receive + * the last result for the last binding. * * @param client the {@link Client} to exchange messages with. * @param query the {@link Query} for synthetic client-preparing statement. @@ -152,8 +150,8 @@ static Flux> execute( /** * Execute a simple compound query. Query execution terminates with the last {@link CompleteMessage} or a - * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed - * by {@link CompleteMessage} after receive the last result for the last binding. + * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed by + * {@link CompleteMessage} after receive the last result for the last binding. * * @param client the {@link Client} to exchange messages with. * @param sql the query to execute, can be contains multi-statements. @@ -165,9 +163,9 @@ static Flux> execute(Client client, String sql) { /** * Execute multiple simple compound queries with one-by-one. Query execution terminates with the last - * {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception and - * cancel subsequent statements' execution. The exchange will be completed by {@link CompleteMessage} - * after receive the last result for the last binding. + * {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception and cancel + * subsequent statements' execution. The exchange will be completed by {@link CompleteMessage} after receive the + * last result for the last binding. * * @param client the {@link Client} to exchange messages with. * @param statements bundled sql for execute. @@ -188,29 +186,9 @@ static Flux> execute(Client client, List statements) } /** - * Login a {@link Client} and receive the {@code client} after logon. It will emit an exception when - * client receives a {@link ErrorMessage}. - * - * @param client the {@link Client} to exchange messages with. - * @param sslMode the {@link SslMode} defines SSL capability and behavior. - * @param database the database that will be connected. - * @param user the user that will be login. - * @param password the password of the {@code user}. - * @param context the {@link ConnectionContext} for initialization. - * @return the messages received in response to the login exchange. - */ - static Mono login(Client client, SslMode sslMode, String database, String user, - @Nullable CharSequence password, ConnectionContext context) { - return client.exchange(new LoginExchangeable(client, sslMode, database, user, password, context)) - .onErrorResume(e -> client.forceClose().then(Mono.error(e))) - .then(Mono.just(client)); - } - - /** - * Execute a simple query and return a {@link Mono} for the complete signal or error. Query execution - * terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} - * will emit an exception. The exchange will be completed by {@link CompleteMessage} after receive the - * last result for the last binding. + * Execute a simple query and return a {@link Mono} for the complete signal or error. Query execution terminates + * with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. + * The exchange will be completed by {@link CompleteMessage} after receive the last result for the last binding. *

* Note: this method does not support {@code LOCAL INFILE} due to it should be used for excepted queries. * @@ -234,18 +212,16 @@ static Mono executeVoid(Client client, String sql) { } /** - * Begins a new transaction with a {@link TransactionDefinition}. It will change current transaction - * statuses of the {@link ConnectionState}. + * Begins a new transaction with a {@link TransactionDefinition}. It will change current transaction statuses of + * the {@link ConnectionContext}. * * @param client the {@link Client} to exchange messages with. - * @param state the connection state for checks and sets transaction statuses. * @param batchSupported if connection supports batch query. * @param definition the {@link TransactionDefinition}. * @return receives complete signal. */ - static Mono beginTransaction(Client client, ConnectionState state, boolean batchSupported, - TransactionDefinition definition) { - final StartTransactionState startState = new StartTransactionState(state, definition); + static Mono beginTransaction(Client client, boolean batchSupported, TransactionDefinition definition) { + final StartTransactionState startState = new StartTransactionState(client, definition); if (batchSupported) { return client.exchange(new TransactionBatchExchangeable(startState)).then(); @@ -255,18 +231,15 @@ static Mono beginTransaction(Client client, ConnectionState state, boolean } /** - * Commits or rollbacks current transaction. It will recover statuses of the {@link ConnectionState} in - * the initial connection state. + * Commits or rollbacks current transaction. It will recover statuses of the {@link ConnectionContext}. * * @param client the {@link Client} to exchange messages with. - * @param state the connection state for checks and resets transaction statuses. - * @param commit if commit, otherwise rollback. + * @param commit if it is commit, otherwise rollback. * @param batchSupported if connection supports batch query. * @return receives complete signal. */ - static Mono doneTransaction(Client client, ConnectionState state, boolean commit, - boolean batchSupported) { - final CommitRollbackState commitState = new CommitRollbackState(state, commit); + static Mono doneTransaction(Client client, boolean commit, boolean batchSupported) { + final CommitRollbackState commitState = new CommitRollbackState(client, commit); if (batchSupported) { return client.exchange(new TransactionBatchExchangeable(commitState)).then(); @@ -275,20 +248,95 @@ static Mono doneTransaction(Client client, ConnectionState state, boolean return client.exchange(new TransactionMultiExchangeable(commitState)).then(); } - static Mono createSavepoint(Client client, ConnectionState state, String name, - boolean batchSupported) { - final CreateSavepointState savepointState = new CreateSavepointState(state, name); + /** + * Creates a savepoint with a name. It will begin a new transaction before creating a savepoint if the connection is + * not in a transaction. + * + * @param client the {@link Client} to exchange messages with. + * @param name the name of the savepoint. + * @param batchSupported if connection supports batch query. + * @return a {@link Mono} receives complete signal. + */ + static Mono createSavepoint(Client client, String name, boolean batchSupported) { + final CreateSavepointState savepointState = new CreateSavepointState(client, name); if (batchSupported) { return client.exchange(new TransactionBatchExchangeable(savepointState)).then(); } return client.exchange(new TransactionMultiExchangeable(savepointState)).then(); } + /** + * Executes a ping command to the server. + * + * @param client the {@link Client} to exchange messages with. + * @return complete or error messages received in response to this exchange. + */ + static Flux ping(Client client) { + return client.exchange(PingMessage.INSTANCE, PING); + } + + /** + * Sets a session variable to the server. + * + * @param client the {@link Client} to exchange messages with. + * @param variable the session variable to set, e.g. {@code "sql_mode='ANSI'"}. + * @return a {@link Mono} receives complete signal. + */ + static Mono setSessionVariable(Client client, String variable) { + if (variable.isEmpty()) { + return Mono.empty(); + } else if (variable.startsWith("@")) { + return executeVoid(client, "SET " + variable); + } + + return executeVoid(client, "SET SESSION " + variable); + } + + /** + * Sets multiple session variables to the server. + * + * @param client the {@link Client} to exchange messages with. + * @param sessionVariables the session variables to set, e.g. {@code ["sql_mode='ANSI'", "time_zone='+09:00'"]}. + * @return a {@link Mono} receives complete signal. + */ + static Mono setSessionVariables(Client client, List sessionVariables) { + switch (sessionVariables.size()) { + case 0: + return Mono.empty(); + case 1: + return setSessionVariable(client, sessionVariables.get(0)); + default: { + StringBuilder query = new StringBuilder(sessionVariables.size() * 32 + 16).append("SET "); + boolean comma = false; + + for (String variable : sessionVariables) { + if (variable.isEmpty()) { + continue; + } + + if (comma) { + query.append(','); + } else { + comma = true; + } + + if (variable.startsWith("@")) { + query.append(variable); + } else { + query.append("SESSION ").append(variable); + } + } + + return executeVoid(client, query.toString()); + } + } + } + /** * Execute a simple query statement. Query execution terminates with the last {@link CompleteMessage} or a - * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed - * by {@link CompleteMessage} after receive the last result for the last binding. The exchange will be - * completed by {@link CompleteMessage} after receive the last result for the last binding. + * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed by + * {@link CompleteMessage} after receive the last result for the last binding. The exchange will be completed by + * {@link CompleteMessage} after receive the last result for the last binding. * * @param client the {@link Client} to exchange messages with. * @param sql the query to execute, can be contains multi-statements. @@ -298,7 +346,8 @@ private static Flux execute0(Client client, String sql) { return client.exchange(new SimpleQueryExchangeable(sql)); } - private QueryFlow() { } + private QueryFlow() { + } } /** @@ -327,7 +376,7 @@ public final void accept(ServerMessage message, SynchronousSink s QueryLogger.logLocalInfile(path); requests.emitNext( - new LocalInfileResponse(request.getEnvelopeId() + 1, path, sink), + new LocalInfileResponse(path, sink), Sinks.EmitFailureHandler.FAIL_FAST ); } else { @@ -339,9 +388,9 @@ public final void accept(ServerMessage message, SynchronousSink s } } - abstract protected void tryNextOrComplete(@Nullable SynchronousSink sink); + protected abstract void tryNextOrComplete(@Nullable SynchronousSink sink); - abstract protected String offendingSql(); + protected abstract String offendingSql(); } final class SimpleQueryExchangeable extends BaseFluxExchangeable { @@ -511,12 +560,12 @@ protected String offendingSql() { } /** - * An implementation of {@link FluxExchangeable} that considers server-preparing queries. Which contains a - * built-in state machine. + * An implementation of {@link FluxExchangeable} that considers server-preparing queries. Which contains a built-in + * state machine. *

- * It will reset a prepared statement if cache has matched it, otherwise it will prepare statement to a new - * statement ID and put the ID into the cache. If the statement ID does not exist in the cache after the last - * row sent, the ID will be closed. + * It will reset a prepared statement if cache has matched it, otherwise it will prepare statement to a new statement ID + * and put the ID into the cache. If the statement ID does not exist in the cache after the last row sent, the ID will + * be closed. */ final class PrepareExchangeable extends FluxExchangeable { @@ -533,7 +582,7 @@ final class PrepareExchangeable extends FluxExchangeable { private final Sinks.Many requests = Sinks.many().unicast() .onBackpressureBuffer(Queues.one().get()); - private final PrepareCache cache; + private final Client client; private final String sql; @@ -548,8 +597,8 @@ final class PrepareExchangeable extends FluxExchangeable { private boolean shouldClose; - PrepareExchangeable(PrepareCache cache, String sql, Iterator bindings, int fetchSize) { - this.cache = cache; + PrepareExchangeable(Client client, String sql, Iterator bindings, int fetchSize) { + this.client = client; this.sql = sql; this.bindings = bindings; this.fetchSize = fetchSize; @@ -561,7 +610,7 @@ public void subscribe(CoreSubscriber actual) { requests.asFlux().subscribe(actual); // After subscribe. - Integer statementId = cache.getIfPresent(sql); + Integer statementId = client.getContext().getPrepareCache().getIfPresent(sql); if (statementId == null) { logger.debug("Prepare cache mismatch, try to preparing"); this.shouldClose = true; @@ -702,7 +751,7 @@ private void putToCache(Integer statementId) { boolean putSucceed; try { - putSucceed = cache.putIfAbsent(sql, statementId, evictId -> { + putSucceed = client.getContext().getPrepareCache().putIfAbsent(sql, statementId, evictId -> { logger.debug("Prepare cache evicts statement {} when putting", evictId); Sinks.EmitResult result = requests.tryEmitNext(new PreparedCloseMessage(evictId)); @@ -798,247 +847,9 @@ private void onCompleteMessage(CompleteMessage message, SynchronousSink - * Not like other {@link FluxExchangeable}s, it is started by a server-side message, which should be an - * implementation of {@link HandshakeRequest}. - */ -final class LoginExchangeable extends FluxExchangeable { - - private static final InternalLogger logger = InternalLoggerFactory.getInstance(LoginExchangeable.class); - - private static final Map ATTRIBUTES = Collections.emptyMap(); - - private static final String CLI_SPECIFIC = "HY000"; - - private static final int HANDSHAKE_VERSION = 10; - - private final Sinks.Many requests = Sinks.many().unicast() - .onBackpressureBuffer(Queues.one().get()); - - private final Client client; - - private final SslMode sslMode; - - private final String database; - - private final String user; - - @Nullable - private final CharSequence password; - - private final ConnectionContext context; - - private boolean handshake = true; - - private MySqlAuthProvider authProvider; - - private byte[] salt; - - private boolean sslCompleted; - - private int lastEnvelopeId; - - LoginExchangeable(Client client, SslMode sslMode, String database, String user, - @Nullable CharSequence password, ConnectionContext context) { - this.client = client; - this.sslMode = sslMode; - this.database = database; - this.user = user; - this.password = password; - this.context = context; - this.sslCompleted = sslMode == SslMode.TUNNEL; - } - - @Override - public void subscribe(CoreSubscriber actual) { - requests.asFlux().subscribe(actual); - } - - @Override - public void accept(ServerMessage message, SynchronousSink sink) { - if (message instanceof ErrorMessage) { - sink.error(((ErrorMessage) message).toException()); - return; - } - - // Ensures it will be initialized only once. - if (handshake) { - handshake = false; - if (message instanceof HandshakeRequest) { - HandshakeRequest request = (HandshakeRequest) message; - Capability capability = initHandshake(request); - - lastEnvelopeId = request.getEnvelopeId() + 1; - - if (capability.isSslEnabled()) { - emitNext(SslRequest.from(lastEnvelopeId, capability, - context.getClientCollation().getId()), sink); - } else { - emitNext(createHandshakeResponse(lastEnvelopeId, capability), sink); - } - } else { - sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + - message.getClass().getSimpleName() + "' in init phase")); - } - - return; - } - - if (message instanceof OkMessage) { - client.loginSuccess(); - sink.complete(); - } else if (message instanceof SyntheticSslResponseMessage) { - sslCompleted = true; - emitNext(createHandshakeResponse(++lastEnvelopeId, context.getCapability()), sink); - } else if (message instanceof AuthMoreDataMessage) { - AuthMoreDataMessage msg = (AuthMoreDataMessage) message; - lastEnvelopeId = msg.getEnvelopeId() + 1; - - if (msg.isFailed()) { - if (logger.isDebugEnabled()) { - logger.debug("Connection (id {}) fast authentication failed, use full authentication", - context.getConnectionId()); - } - - emitNext(createAuthResponse(lastEnvelopeId, "full authentication"), sink); - } - // Otherwise success, wait until OK message or Error message. - } else if (message instanceof ChangeAuthMessage) { - ChangeAuthMessage msg = (ChangeAuthMessage) message; - lastEnvelopeId = msg.getEnvelopeId() + 1; - authProvider = MySqlAuthProvider.build(msg.getAuthType()); - salt = msg.getSalt(); - emitNext(createAuthResponse(lastEnvelopeId, "change authentication"), sink); - } else { - sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + - message.getClass().getSimpleName() + "' in login phase")); - } - } - - @Override - public void dispose() { - // No particular error condition handling for complete signal. - this.requests.tryEmitComplete(); - } - - private void emitNext(SubsequenceClientMessage message, SynchronousSink sink) { - Sinks.EmitResult result = requests.tryEmitNext(message); - - if (result != Sinks.EmitResult.OK) { - sink.error(new IllegalStateException("Fail to emit a login request due to " + result)); - } - } - - private AuthResponse createAuthResponse(int envelopeId, String phase) { - MySqlAuthProvider authProvider = getAndNextProvider(); - - if (authProvider.isSslNecessary() && !sslCompleted) { - throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), phase), CLI_SPECIFIC); - } - - return new AuthResponse(envelopeId, - authProvider.authentication(password, salt, context.getClientCollation())); - } - - private Capability clientCapability(Capability serverCapability) { - Capability.Builder builder = serverCapability.mutate(); - - builder.disableDatabasePinned(); - builder.disableCompression(); - builder.disableIgnoreAmbiguitySpace(); - builder.disableInteractiveTimeout(); - - if (sslMode == SslMode.TUNNEL) { - // Tunnel does not use MySQL SSL protocol, disable it. - builder.disableSsl(); - } else if (!serverCapability.isSslEnabled()) { - // Server unsupported SSL. - if (sslMode.requireSsl()) { - throw new R2dbcPermissionDeniedException("Server version '" + context.getServerVersion() + - "' does not support SSL but mode '" + sslMode + "' requires SSL", CLI_SPECIFIC); - } else if (sslMode.startSsl()) { - // SSL has start yet, and client can disable SSL, disable now. - client.sslUnsupported(); - } - } else { - // The server supports SSL, but the user does not want to use SSL, disable it. - if (!sslMode.startSsl()) { - builder.disableSsl(); - } - } - - if (database.isEmpty()) { - builder.disableConnectWithDatabase(); - } - - if (context.getLocalInfilePath() == null) { - builder.disableLoadDataLocalInfile(); - } - - if (ATTRIBUTES.isEmpty()) { - builder.disableConnectAttributes(); - } - - return builder.build(); - } - - private Capability initHandshake(HandshakeRequest message) { - HandshakeHeader header = message.getHeader(); - int handshakeVersion = header.getProtocolVersion(); - ServerVersion serverVersion = header.getServerVersion(); - - if (handshakeVersion < HANDSHAKE_VERSION) { - logger.warn("MySQL use handshake V{}, server version is {}, maybe most features are unavailable", - handshakeVersion, serverVersion); - } - - Capability capability = clientCapability(message.getServerCapability()); - - // No need initialize server statuses because it has initialized by read filter. - this.context.init(header.getConnectionId(), serverVersion, capability); - this.authProvider = MySqlAuthProvider.build(message.getAuthType()); - this.salt = message.getSalt(); - - return capability; - } - - private MySqlAuthProvider getAndNextProvider() { - MySqlAuthProvider authProvider = this.authProvider; - this.authProvider = authProvider.next(); - return authProvider; - } - - private HandshakeResponse createHandshakeResponse(int envelopeId, Capability capability) { - MySqlAuthProvider authProvider = getAndNextProvider(); - - if (authProvider.isSslNecessary() && !sslCompleted) { - throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), "handshake"), - CLI_SPECIFIC); - } - - byte[] authorization = authProvider.authentication(password, salt, context.getClientCollation()); - String authType = authProvider.getType(); - - if (MySqlAuthProvider.NO_AUTH_PROVIDER.equals(authType)) { - // Authentication type is not matter because of it has no authentication type. - // Server need send a Change Authentication Message after handshake response. - authType = MySqlAuthProvider.CACHING_SHA2_PASSWORD; - } - - return HandshakeResponse.from(envelopeId, capability, context.getClientCollation().getId(), - user, authorization, authType, database, ATTRIBUTES); - } - - private static String authFails(String authType, String phase) { - return "Authentication type '" + authType + "' must require SSL in " + phase + " phase"; - } -} - abstract class AbstractTransactionState { - final ConnectionState state; + final Client client; final List statements = new ArrayList<>(5); @@ -1050,8 +861,8 @@ abstract class AbstractTransactionState { @Nullable private String sql; - protected AbstractTransactionState(ConnectionState state) { - this.state = state; + protected AbstractTransactionState(Client client) { + this.client = client; } final void setSql(String sql) { @@ -1109,21 +920,24 @@ final class CommitRollbackState extends AbstractTransactionState { private final boolean commit; - CommitRollbackState(ConnectionState state, boolean commit) { - super(state); + CommitRollbackState(Client client, boolean commit) { + super(client); this.commit = commit; } @Override boolean cancelTasks() { - if (!state.isInTransaction()) { + ConnectionContext context = client.getContext(); + + if (!context.isInTransaction()) { tasks |= CANCEL; return true; } - if (state.isLockWaitTimeoutChanged()) { + if (context.isLockWaitTimeoutChanged()) { + // If server does not support lock wait timeout, the state will not be changed, so it is safe. tasks |= LOCK_WAIT_TIMEOUT; - statements.add("SET innodb_lock_wait_timeout=" + state.getSessionLockWaitTimeout()); + statements.add(StringUtils.lockWaitTimeoutStatement(context.getSessionLockWaitTimeout())); } tasks |= COMMIT_OR_ROLLBACK; @@ -1136,10 +950,10 @@ boolean cancelTasks() { protected boolean process(int task, SynchronousSink sink) { switch (task) { case LOCK_WAIT_TIMEOUT: - state.resetCurrentLockWaitTimeout(); + client.getContext().resetCurrentLockWaitTimeout(); return true; case COMMIT_OR_ROLLBACK: - state.resetIsolationLevel(); + client.getContext().resetCurrentIsolationLevel(); sink.complete(); return false; case CANCEL: @@ -1165,22 +979,28 @@ final class StartTransactionState extends AbstractTransactionState { private final TransactionDefinition definition; - StartTransactionState(ConnectionState state, TransactionDefinition definition) { - super(state); + StartTransactionState(Client client, TransactionDefinition definition) { + super(client); this.definition = definition; } @Override boolean cancelTasks() { - if (state.isInTransaction()) { + final ConnectionContext context = client.getContext(); + if (context.isInTransaction()) { tasks |= CANCEL; return true; } + final Duration timeout = definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT); if (timeout != null) { - final long lockWaitTimeout = timeout.getSeconds(); - tasks |= LOCK_WAIT_TIMEOUT; - statements.add("SET innodb_lock_wait_timeout=" + lockWaitTimeout); + if (context.isLockWaitTimeoutSupported()) { + tasks |= LOCK_WAIT_TIMEOUT; + statements.add(StringUtils.lockWaitTimeoutStatement(timeout)); + } else { + QueryFlow.logger.warn( + "Lock wait timeout is not supported by server, transaction definition lockWaitTimeout is ignored"); + } } final IsolationLevel isolationLevel = definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL); @@ -1202,48 +1022,54 @@ protected boolean process(int task, SynchronousSink sink) { case LOCK_WAIT_TIMEOUT: final Duration timeout = definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT); if (timeout != null) { - final long lockWaitTimeout = timeout.getSeconds(); - state.setCurrentLockWaitTimeout(lockWaitTimeout); + client.getContext().setCurrentLockWaitTimeout(timeout); } return true; case ISOLATION_LEVEL: - final IsolationLevel isolationLevel = - definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL); + final IsolationLevel isolationLevel = definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL); if (isolationLevel != null) { - state.setIsolationLevel(isolationLevel); + client.getContext().setCurrentIsolationLevel(isolationLevel); } return true; case START_TRANSACTION: case CANCEL: sink.complete(); return false; - } sink.error(new IllegalStateException("Undefined transaction task: " + task + ", remain: " + tasks)); return false; } - private static String buildStartTransaction(TransactionDefinition definition) { + /** + * Visible for testing. + * + * @param definition the transaction definition + * @return the {@code START TRANSACTION} statement + */ + static String buildStartTransaction(TransactionDefinition definition) { Boolean readOnly = definition.getAttribute(TransactionDefinition.READ_ONLY); Boolean snapshot = definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT); - if (readOnly == null && (snapshot == null || !snapshot)) { + if (readOnly == null && !Boolean.TRUE.equals(snapshot)) { return "BEGIN"; } StringBuilder builder = new StringBuilder(90).append("START TRANSACTION"); + boolean first = true; - if (snapshot != null && snapshot) { - ConsistentSnapshotEngine engine = - definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE); + if (Boolean.TRUE.equals(snapshot)) { + // Compatible for enum ConsistentSnapshotEngine. + Object eng = definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE); + String engine = eng == null ? null : eng.toString(); + first = false; builder.append(" WITH CONSISTENT "); if (engine == null) { builder.append("SNAPSHOT"); } else { - builder.append(engine.asSql()).append(" SNAPSHOT"); + builder.append(engine).append(" SNAPSHOT"); } Long sessionId = @@ -1255,6 +1081,10 @@ private static String buildStartTransaction(TransactionDefinition definition) { } if (readOnly != null) { + if (!first) { + builder.append(','); + } + if (readOnly) { builder.append(" READ ONLY"); } else { @@ -1274,14 +1104,14 @@ final class CreateSavepointState extends AbstractTransactionState { private final String name; - CreateSavepointState(final ConnectionState state, final String name) { - super(state); + CreateSavepointState(final Client client, final String name) { + super(client); this.name = name; } @Override boolean cancelTasks() { - if (!state.isInTransaction()) { + if (!client.getContext().isInTransaction()) { tasks |= START_TRANSACTION; statements.add("BEGIN"); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java similarity index 99% rename from src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java index 519a56ebe..6ffa079e0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java @@ -41,7 +41,7 @@ public final class ServerVersion implements Comparable { * Unresolved/origin version pattern, do NOT use it on {@link #hashCode()}, {@link #equals(Object)} or * {@link #compareTo(ServerVersion)}. */ - private transient final String origin; + private final transient String origin; private final int major; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java similarity index 89% rename from src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java index 56b34a926..78a6ec781 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; @@ -26,16 +27,13 @@ */ abstract class SimpleStatementSupport extends MySqlStatementSupport { - protected final Client client; - protected final Codecs codecs; protected final String sql; - SimpleStatementSupport(Client client, Codecs codecs, ConnectionContext context, String sql) { - super(context); + SimpleStatementSupport(Client client, Codecs codecs, String sql) { + super(client); - this.client = requireNonNull(client, "client must not be null"); this.codecs = requireNonNull(codecs, "codecs must not be null"); this.sql = requireNonNull(sql, "sql must not be null"); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParameterizedStatement.java similarity index 66% rename from src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParameterizedStatement.java index e0fd475c6..84249cb03 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParameterizedStatement.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import reactor.core.publisher.Flux; @@ -23,18 +24,17 @@ import java.util.List; /** - * An implementation of {@link ParametrizedStatementSupport} based on MySQL text query. + * An implementation of {@link ParameterizedStatementSupport} based on MySQL text query. */ -final class TextParametrizedStatement extends ParametrizedStatementSupport { +final class TextParameterizedStatement extends ParameterizedStatementSupport { - TextParametrizedStatement(Client client, Codecs codecs, Query query, ConnectionContext context) { - super(client, codecs, query, context); + TextParameterizedStatement(Client client, Codecs codecs, Query query) { + super(client, codecs, query); } @Override protected Flux execute(List bindings) { - return Flux.defer(() -> QueryFlow.execute(client, query, returningIdentifiers(), - bindings)) - .map(messages -> MySqlResult.toResult(false, codecs, context, syntheticKeyName(), messages)); + return Flux.defer(() -> QueryFlow.execute(client, query, returningIdentifiers(), bindings)) + .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, syntheticKeyName(), messages)); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java similarity index 81% rename from src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java index 04fd90001..f1e0f7083 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; @@ -26,8 +27,8 @@ */ final class TextSimpleStatement extends SimpleStatementSupport { - TextSimpleStatement(Client client, Codecs codecs, ConnectionContext context, String sql) { - super(client, codecs, context, sql); + TextSimpleStatement(Client client, Codecs codecs, String sql) { + super(client, codecs, sql); } @Override @@ -35,6 +36,6 @@ public Flux execute() { return Flux.defer(() -> QueryFlow.execute( client, StringUtils.extendReturning(sql, returningIdentifiers()) - ).map(messages -> MySqlResult.toResult(false, codecs, context, syntheticKeyName(), messages))); + ).map(messages -> MySqlSegmentResult.toResult(false, client, codecs, syntheticKeyName(), messages))); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlBatch.java new file mode 100644 index 000000000..d49ca9b1e --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlBatch.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.Batch; +import reactor.core.publisher.Flux; + +/** + * {@link Batch} for executing a collection of statements in a batch against a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlBatch extends Batch { + + /** + * {@inheritDoc} + * + * @param sql the statement to add + * @return {@link MySqlBatch this} + * @throws IllegalArgumentException if {@code sql} is {@code null} + */ + @Override + MySqlBatch add(String sql); + + /** + * {@inheritDoc} + * + * @return the {@link MySqlResult}s of executing the batch + */ + @Override + Flux execute(); +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/Lifecycle.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlColumnMetadata.java similarity index 66% rename from src/main/java/io/asyncer/r2dbc/mysql/client/Lifecycle.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlColumnMetadata.java index 2ad0f6efd..9bd71a36a 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/Lifecycle.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlColumnMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 asyncer.io projects + * Copyright 2024 asyncer.io projects * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,14 @@ * limitations under the License. */ -package io.asyncer.r2dbc.mysql.client; +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.ColumnMetadata; /** - * The lifecycle of connection. + * {@link ColumnMetadata} for column metadata returned from a MySQL database. + * + * @since 1.1.3 */ -enum Lifecycle { - -// CONNECTION, // Useless for signal - - COMMAND, - -// REPLICATION // Useless for r2dbc driver, just ignore +public interface MySqlColumnMetadata extends MySqlReadableMetadata, ColumnMetadata { } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnection.java new file mode 100644 index 000000000..e2b5b2244 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnection.java @@ -0,0 +1,203 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Lifecycle; +import io.r2dbc.spi.TransactionDefinition; +import io.r2dbc.spi.ValidationDepth; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * A {@link Connection} for connecting to a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlConnection extends Connection, Lifecycle { + + /** + * {@inheritDoc} + *

+ * Note: MySQL server will disable the auto-commit mode automatically when a transaction is started. + * + * @return a {@link Mono} that indicates that the transaction has begun + */ + @Override + Mono beginTransaction(); + + /** + * {@inheritDoc} + *

+ * Note: MySQL server will disable the auto-commit mode automatically when a transaction is started. + * + * @param definition the transaction definition, must not be {@code null} + * @return a {@link Mono} that indicates that the transaction has begun + * @throws IllegalArgumentException if {@code definition} is {@code null} + */ + @Override + Mono beginTransaction(TransactionDefinition definition); + + /** + * {@inheritDoc} + * + * @return a {@link Mono} that indicates that the connection has been closed + */ + @Override + Mono close(); + + /** + * {@inheritDoc} + * + * @return a {@link Mono} that indicates that the transaction has been committed + */ + @Override + Mono commitTransaction(); + + /** + * {@inheritDoc} + * + * @return a {@link MySqlBatch} that can be used to execute a batch of statements + */ + @Override + MySqlBatch createBatch(); + + /** + * {@inheritDoc} + * + * @param name the savepoint name, must not be {@code null} + * @return a {@link Mono} that indicates that the savepoint has been created + * @throws IllegalArgumentException if {@code name} is {@code null} + */ + @Override + Mono createSavepoint(String name); + + /** + * {@inheritDoc} + * + * @param sql the SQL to execute, must not be {@code null} + * @return a new {@link MySqlStatement} instance + * @throws IllegalArgumentException if {@code sql} is {@code null} + */ + @Override + MySqlStatement createStatement(String sql); + + /** + * {@inheritDoc} + * + * @return a {@link MySqlConnectionMetadata} that contains the connection metadata + */ + @Override + MySqlConnectionMetadata getMetadata(); + + /** + * {@inheritDoc} + * + * @param name the savepoint name, must not be {@code null} + * @return a {@link Mono} that indicates that the savepoint has been released + * @throws IllegalArgumentException if {@code name} is {@code null} + */ + @Override + Mono releaseSavepoint(String name); + + /** + * {@inheritDoc} + * + * @return a {@link Mono} that indicates that the transaction has been rolled back + */ + @Override + Mono rollbackTransaction(); + + /** + * {@inheritDoc} + * + * @param name the savepoint name, must not be {@code null} + * @return a {@link Mono} that indicates that the transaction has been rolled back to the savepoint + * @throws IllegalArgumentException if {@code name} is {@code null} + */ + @Override + Mono rollbackTransactionToSavepoint(String name); + + /** + * {@inheritDoc} + * + * @param autoCommit the auto-commit mode + * @return a {@link Mono} that indicates that the auto-commit mode has been set + */ + @Override + Mono setAutoCommit(boolean autoCommit); + + /** + * {@inheritDoc} + *

+ * Note: Currently, it should be used only for InnoDB storage engine. + * + * @param timeout the lock wait timeout, must not be {@code null} + * @return a {@link Mono} that indicates that the lock wait timeout has been set + * @throws IllegalArgumentException if {@code timeout} is {@code null} + */ + @Override + Mono setLockWaitTimeout(Duration timeout); + + /** + * {@inheritDoc} + * + * @param timeout the statement timeout, must not be {@code null} + * @return a {@link Mono} that indicates that the statement timeout has been set + * @throws IllegalArgumentException if {@code timeout} is {@code null} + */ + @Override + Mono setStatementTimeout(Duration timeout); + + /** + * {@inheritDoc} + * + * @param isolationLevel the isolation level, must not be {@code null} + * @return a {@link Mono} that indicates that the isolation level of the current session has been set + * @throws IllegalArgumentException if {@code isolationLevel} is {@code null} + */ + @Override + Mono setTransactionIsolationLevel(IsolationLevel isolationLevel); + + /** + * {@inheritDoc} + * + * @param depth the validation depth, must not be {@code null} + * @return a {@link Mono} that indicates that the connection has been validated + * @throws IllegalArgumentException if {@code depth} is {@code null} + */ + @Override + Mono validate(ValidationDepth depth); + + /** + * {@inheritDoc} + * + * @return a {@link Mono} that indicates that the connection is ready for usage + */ + @Override + Mono postAllocate(); + + /** + * {@inheritDoc} + * + * @return a {@link Mono} that indicates that the connection is ready for release + */ + @Override + Mono preRelease(); +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnectionMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnectionMetadata.java new file mode 100644 index 000000000..41adc5f7b --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnectionMetadata.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.ConnectionMetadata; + +/** + * {@link ConnectionMetadata} for a connection connected to a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlConnectionMetadata extends ConnectionMetadata { + + /** + * {@inheritDoc} + *

+ * Note: it should be the result of {@code SELECT @@version_comment} + * + * @return the product name of the database + */ + @Override + String getDatabaseProductName(); + + /** + * {@inheritDoc} + * + * @return the version received from the server, e.g. {@code 5.7.30}, {@code 5.5.5-10.4.13-MariaDB} + */ + @Override + String getDatabaseVersion(); + + /** + * Checks if the connection is in MariaDB mode. + * + * @return {@code true} if it is MariaDB + */ + boolean isMariaDb(); +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlNativeTypeMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlNativeTypeMetadata.java new file mode 100644 index 000000000..adb9533a3 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlNativeTypeMetadata.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +/** + * An interface for MySQL native type metadata. + * + * @see MySqlReadableMetadata#getNativeTypeMetadata() + * @since 1.1.3 + */ +public interface MySqlNativeTypeMetadata { + + /** + * Gets the native type identifier, e.g. {@code 3} for {@code INT}. + *

+ * Note: It can not check if the current type is unsigned or not, and some types will use the same + * identifier. e.g. {@code TEXT} and {@code BLOB} are using {@code 252}. + * + * @return the native type identifier + */ + int getTypeId(); + + /** + * Checks if the value is not null. + * + * @return if value is not null + */ + boolean isNotNull(); + + /** + * Checks if the value is an unsigned number. e.g. INT UNSIGNED, BIGINT UNSIGNED. + *

+ * Note: IEEE-754 floating types (e.g. DOUBLE/FLOAT) do not support it in MySQL 8.0+. When creating a + * column as an unsigned floating type, the server may report a warning. + * + * @return if value is an unsigned number + */ + boolean isUnsigned(); + + /** + * Checks if the value is binary data. + * + * @return if value is binary data + */ + boolean isBinary(); + + /** + * Checks if the value type is enum. + * + * @return if value is an enum + */ + boolean isEnum(); + + /** + * Checks if the value type is set. + * + * @return if value is a set + */ + boolean isSet(); +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameterMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameterMetadata.java new file mode 100644 index 000000000..a34d63f62 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameterMetadata.java @@ -0,0 +1,27 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.OutParameterMetadata; + +/** + * {@link OutParameterMetadata} for an {@code OUT} parameter metadata returned from a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlOutParameterMetadata extends MySqlReadableMetadata, OutParameterMetadata { +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameters.java similarity index 55% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameters.java index c980207cf..cd771dce1 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameters.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 asyncer.io projects + * Copyright 2024 asyncer.io projects * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +14,22 @@ * limitations under the License. */ -package io.asyncer.r2dbc.mysql; +package io.asyncer.r2dbc.mysql.api; -import io.r2dbc.spi.Batch; -import reactor.core.publisher.Flux; +import io.r2dbc.spi.OutParameters; /** - * Base class considers methods definition for implementations of {@link Batch}. + * {@link OutParameters} for a collection of {@code OUT} parameters returned from a MySQL database. + * + * @since 1.1.3 */ -public abstract class MySqlBatch implements Batch { - - @Override - abstract public MySqlBatch add(String sql); +public interface MySqlOutParameters extends MySqlReadable, OutParameters { + /** + * {@inheritDoc} + * + * @return the {@link MySqlOutParametersMetadata} for all {@code OUT} parameters + */ @Override - abstract public Flux execute(); + MySqlOutParametersMetadata getMetadata(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParametersMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParametersMetadata.java new file mode 100644 index 000000000..eee96606c --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParametersMetadata.java @@ -0,0 +1,59 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.OutParametersMetadata; + +import java.util.List; +import java.util.NoSuchElementException; + +/** + * {@link OutParametersMetadata} for {@code OUT} parameters metadata returned from a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlOutParametersMetadata extends OutParametersMetadata { + + /** + * {@inheritDoc} + * + * @param index the out parameter index starting at 0 + * @return the {@link MySqlOutParametersMetadata} for one out parameter + * @throws IndexOutOfBoundsException if {@code index} is out of range + */ + @Override + MySqlOutParameterMetadata getParameterMetadata(int index); + + /** + * {@inheritDoc} + * + * @param name the name of the out parameter. Parameter names are case-insensitive. + * @return the {@link MySqlOutParameterMetadata} for one out parameter + * @throws IllegalArgumentException if {@code name} is {@code null} + * @throws NoSuchElementException if there is no out parameter with the {@code name} + */ + @Override + MySqlOutParameterMetadata getParameterMetadata(String name); + + /** + * {@inheritDoc} + * + * @return the {@link MySqlOutParameterMetadata} for all out parameters + */ + @Override + List getParameterMetadatas(); +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlReadable.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlReadable.java new file mode 100644 index 000000000..e47c806f0 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlReadable.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.Readable; + +/** + * {@link Readable Readable data} for a row or a collection of {@code OUT} parameters that's against a MySQL + * database. + * + * @see MySqlOutParameters + * @see MySqlRow + * @since 1.1.3 + */ +public interface MySqlReadable extends Readable { +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlReadableMetadata.java similarity index 61% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlReadableMetadata.java index 162bf81e3..c8b69b08c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlReadableMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 asyncer.io projects + * Copyright 2024 asyncer.io projects * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,45 +14,55 @@ * limitations under the License. */ -package io.asyncer.r2dbc.mysql; +package io.asyncer.r2dbc.mysql.api; import io.asyncer.r2dbc.mysql.codec.CodecContext; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; -import io.r2dbc.spi.ColumnMetadata; +import io.r2dbc.spi.ReadableMetadata; /** - * An abstraction of {@link ColumnMetadata} considers MySQL + * {@link ReadableMetadata} for metadata of a column or an {@code OUT} parameter returned from a MySQL + * database. + * + * @since 1.1.3 */ -public interface MySqlColumnMetadata extends ColumnMetadata { +public interface MySqlReadableMetadata extends ReadableMetadata { /** * {@inheritDoc} + * + * @return the {@link MySqlType} descriptor. */ @Override MySqlType getType(); /** - * {@inheritDoc} - */ - @Override - MySqlTypeMetadata getNativeTypeMetadata(); - - /** - * Gets the {@link CharCollation} used for stringification type. It will not be a binary collation. + * Gets the {@link CharCollation} used for stringification type. If server-side collation is binary, it + * will return the default client collation of {@code context}. * - * @param context the codec context for load the default character collation on the server-side. + * @param context the codec context for load the default character collation. * @return the {@link CharCollation}. */ CharCollation getCharCollation(CodecContext context); /** - * Gets the field max size that's defined by the table, the original type is an unsigned int32. + * {@inheritDoc} * - * @return the field max size. + * @return the {@link MySqlNativeTypeMetadata}. */ - long getNativePrecision(); + @Override + default MySqlNativeTypeMetadata getNativeTypeMetadata() { + return null; + } + /** + * {@inheritDoc} + * + * @return the primary Java {@link Class type}. + * @see MySqlRow#get + * @see MySqlOutParameters#get + */ @Override default Class getJavaType() { return getType().getJavaType(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlResult.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlResult.java new file mode 100644 index 000000000..81fabfeea --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlResult.java @@ -0,0 +1,173 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.OutParameters; +import io.r2dbc.spi.Readable; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * A {@link Result} for results of a query against a MySQL database. + *

+ * Note: A query may return multiple {@link MySqlResult}s. + * + * @since 1.1.3 + */ +public interface MySqlResult extends Result { + + /** + * {@inheritDoc} + * + * @return a {@link Mono} emitting the number of rows updated, or empty if it is not an update result. + * @throws IllegalStateException if the result was consumed + */ + @Override + Mono getRowsUpdated(); + + /** + * {@inheritDoc} + * + * @param mappingFunction that maps a {@link Row} and {@link RowMetadata} to a value + * @param the type of the mapped value + * @return a {@link Flux} of mapped results + * @throws IllegalArgumentException if {@code mappingFunction} is {@code null} + * @throws IllegalStateException if the result was consumed + */ + @Override + Flux map(BiFunction mappingFunction); + + /** + * {@inheritDoc} + * + * @param mappingFunction that maps a {@link Readable} to a value + * @param the type of the mapped value + * @return a {@link Flux} of mapped results + * @throws IllegalArgumentException if {@code mappingFunction} is {@code null} + * @throws IllegalStateException if the result was consumed + * @see MySqlReadable + * @see MySqlRow + * @see MySqlOutParameters + */ + @Override + Flux map(Function mappingFunction); + + /** + * {@inheritDoc} + * + * @param filter to apply to each element to determine if it should be included + * @return a {@link MySqlResult} that will only emit results that match the {@code predicate} + * @throws IllegalArgumentException if {@code predicate} is {@code null} + * @throws IllegalStateException if the result was consumed + */ + @Override + MySqlResult filter(Predicate filter); + + /** + * {@inheritDoc} + * + * @param mappingFunction that maps a {@link Result.Segment} a to a {@link Publisher} + * @param the type of the mapped value + * @return a {@link Flux} of mapped results + * @throws IllegalArgumentException if {@code mappingFunction} is {@code null} + * @throws IllegalStateException if the result was consumed + */ + @Override + Flux flatMap(Function> mappingFunction); + + /** + * Marker interface for a MySQL result segment. Result segments represent the individual parts of a result + * from a query against a MySQL database. It is a sealed interface. + * + * @see RowSegment + * @see OutSegment + * @see UpdateCount + * @see Message + * @see OkSegment + */ + interface Segment extends Result.Segment { + } + + /** + * Row segment consisting of {@link Row row data}. + */ + interface RowSegment extends Segment, Result.RowSegment { + + /** + * Gets the {@link MySqlRow row data}. + * + * @return a {@link MySqlRow} of data + */ + @Override + MySqlRow row(); + } + + /** + * Out parameters segment consisting of {@link OutParameters readable data}. + */ + interface OutSegment extends Segment, Result.OutSegment { + + /** + * Retrieve all {@code OUT} parameters as a {@link MySqlRow}. + *

+ * In MySQL, {@code OUT} parameters are returned as a row. These rows will be preceded by a flag + * indicating that the following rows are {@code OUT} parameters. So, an {@link OutSegment} must + * can be retrieved as a {@link MySqlRow}, but not vice versa. + * + * @return a {@link MySqlRow} of all {@code OUT} parameters + */ + MySqlRow row(); + + /** + * Gets all {@link OutParameters OUT parameters}. + * + * @return a {@link OutParameters} of data + */ + @Override + MySqlOutParameters outParameters(); + } + + /** + * Update count segment consisting providing an {@link #value() affected rows count}. + */ + interface UpdateCount extends Segment, Result.UpdateCount { + } + + /** + * Message segment reported as result of the statement processing. + */ + interface Message extends Segment, Result.Message { + } + + /** + * Insert result segment consisting of a {@link #row() last inserted id} and + * {@link #value() affected rows count}, and only appears if the statement is an insert, the table has an + * auto-increment identifier column, and the statement is not using the {@code RETURNING} clause. + *

+ * Note: a {@link MySqlResult} will return only the last inserted id whatever how many rows are inserted. + */ + interface OkSegment extends RowSegment, UpdateCount { + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRow.java new file mode 100644 index 000000000..9aef2bb89 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRow.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.Row; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.ParameterizedType; +import java.util.NoSuchElementException; + +/** + * A {@link Row} for a data row of a {@link MySqlResult}. + * + * @since 1.1.3 + */ +public interface MySqlRow extends MySqlReadable, Row { + + /** + * Returns the {@link MySqlRowMetadata} for all columns in this row. + * + * @return the {@link MySqlRowMetadata} for all columns in this row + */ + @Override + MySqlRowMetadata getMetadata(); + + /** + * Returns the value which can be a generic type. + *

+ * UNSTABLE: it is not a standard of {@code r2dbc-spi}, so it may be changed in the future. + * + * @param index the index starting at {@code 0} + * @param type the parameterized type of item to return. + * @param the type of the item being returned. + * @return the value for a column in this row. Value can be {@code null}. + * @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}. + * @throws IndexOutOfBoundsException if {@code index} is out of range + * @throws UnsupportedOperationException if the row is containing last inserted ID + */ + @Nullable T get(int index, ParameterizedType type); + + /** + * Returns the value which can be a generic type. + *

+ * UNSTABLE: it is not a standard of {@code r2dbc-spi}, so it may be changed in the future. + * + * @param name the name of the column. + * @param type the parameterized type of item to return. + * @param the type of the item being returned. + * @return the value for a column in this row. Value can be {@code null}. + * @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}. + * @throws NoSuchElementException if {@code name} is not a known readable column or out parameter + * @throws UnsupportedOperationException if the row is containing last inserted ID + */ + @Nullable T get(String name, ParameterizedType type); +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRowMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRowMetadata.java new file mode 100644 index 000000000..c9a67c251 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRowMetadata.java @@ -0,0 +1,70 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.RowMetadata; + +import java.util.List; +import java.util.NoSuchElementException; + +/** + * {@link RowMetadata} for a row metadata returned from a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlRowMetadata extends RowMetadata { + + /** + * {@inheritDoc} + * + * @param index the column index starting at 0 + * @return the {@link MySqlRowMetadata} for one column in this row + * @throws IndexOutOfBoundsException if {@code index} is out of range + */ + @Override + MySqlColumnMetadata getColumnMetadata(int index); + + /** + * {@inheritDoc} + * + * @param name the name of the column. Column names are case-insensitive. When a get method contains + * several columns with same name, then the value of the first matching column will be + * returned + * @return the {@link MySqlColumnMetadata} for one column in this row + * @throws IllegalArgumentException if {@code name} is {@code null} + * @throws NoSuchElementException if there is no column with the {@code name} + */ + @Override + MySqlColumnMetadata getColumnMetadata(String name); + + /** + * {@inheritDoc} + * + * @return the {@link MySqlColumnMetadata} for all columns in this row + */ + @Override + List getColumnMetadatas(); + + /** + * {@inheritDoc} + * + * @param columnName the name of the column. Column names are case-insensitive. + * @return {@code true} if this object contains metadata for {@code columnName}; {@code false} otherwise. + */ + @Override + boolean contains(String columnName); +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java new file mode 100644 index 000000000..6bc57c99f --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java @@ -0,0 +1,129 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.Statement; +import reactor.core.publisher.Flux; + +import java.util.NoSuchElementException; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; + +/** + * A strongly typed abstraction of {@link Statement} for a SQL statement against a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlStatement extends Statement { + + /** + * {@inheritDoc} + * + * @return {@link MySqlStatement this} + * @throws IllegalStateException if the statement is parameterized and not all parameters are provided + */ + @Override + MySqlStatement add(); + + /** + * {@inheritDoc} + * + * @param index the index to bind to + * @param value the value to bind + * @return {@link MySqlStatement this} + * @throws IllegalArgumentException if {@code value} is {@code null} + * @throws IndexOutOfBoundsException if the parameter {@code index} is out of range + * @throws UnsupportedOperationException if the statement is not a parameterized statement + */ + @Override + MySqlStatement bind(int index, Object value); + + /** + * {@inheritDoc} + * + * @param name the name of identifier to bind to + * @param value the value to bind + * @return {@link MySqlStatement this} + * @throws IllegalArgumentException if {@code name} or {@code value} is {@code null} + * @throws NoSuchElementException if {@code name} is not a known name to bind + * @throws UnsupportedOperationException if the statement is not a parameterized statement + */ + @Override + MySqlStatement bind(String name, Object value); + + /** + * {@inheritDoc} + * + * @param index the index to bind to + * @param type the type of null value + * @return {@link MySqlStatement this} + * @throws IllegalArgumentException if {@code type} is {@code null} + * @throws IndexOutOfBoundsException if the parameter {@code index} is out of range + * @throws UnsupportedOperationException if the statement is not a parameterized statement + */ + @Override + MySqlStatement bindNull(int index, Class type); + + /** + * {@inheritDoc} + * + * @param name the name of identifier to bind to + * @param type the type of null value + * @return {@link MySqlStatement this} + * @throws IllegalArgumentException if {@code name} or {@code type} is {@code null} + * @throws NoSuchElementException if {@code name} is not a known name to bind + * @throws UnsupportedOperationException if the statement is not a parameterized statement + */ + @Override + MySqlStatement bindNull(String name, Class type); + + /** + * {@inheritDoc} + * + * @return a {@link Flux} representing {@link MySqlResult}s of the statement + * @throws IllegalStateException if the statement is parameterized and not all parameters are provided + */ + @Override + Flux execute(); + + /** + * {@inheritDoc} + * + * @param columns the names of the columns to return + * @return {@link MySqlStatement this} + * @throws IllegalArgumentException if {@code columns}, or any item is empty or {@code null} + */ + @Override + default MySqlStatement returnGeneratedValues(String... columns) { + requireNonNull(columns, "columns must not be null"); + return this; + } + + /** + * {@inheritDoc} + * + * @param rows the number of rows to fetch + * @return {@link MySqlStatement this} + * @throws IllegalArgumentException if fetch size is less than zero + */ + @Override + default MySqlStatement fetchSize(int rows) { + require(rows >= 0, "Fetch size must be greater or equal to zero"); + return this; + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinition.java new file mode 100644 index 000000000..636a50678 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinition.java @@ -0,0 +1,194 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Option; +import io.r2dbc.spi.TransactionDefinition; + +import java.time.Duration; + +/** + * {@link TransactionDefinition} for a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlTransactionDefinition extends TransactionDefinition { + + /** + * Use {@code WITH CONSISTENT SNAPSHOT} property. + *

+ * The option starts a consistent read for storage engines such as InnoDB and XtraDB that can do so, the + * same as if a {@code START TRANSACTION} followed by a {@code SELECT ...} from any InnoDB table was + * issued. + */ + Option WITH_CONSISTENT_SNAPSHOT = Option.valueOf("withConsistentSnapshot"); + + /** + * Use {@code WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar property. Only available + * when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}. + *

+ * Note: This is an extended syntax based on specific distributions. Please check whether the server + * supports this property before using it. + */ + Option CONSISTENT_SNAPSHOT_ENGINE = Option.valueOf("consistentSnapshotEngine"); + + /** + * Use {@code WITH CONSISTENT SNAPSHOT FROM SESSION [session_id]} for Percona/MySQL or similar property. + * Only available when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}. + *

+ * The {@code session_id} is received by {@code SHOW COLUMNS FROM performance_schema.processlist}, it + * should be an unsigned 64-bit integer. Use {@code SHOW PROCESSLIST} to find session identifier of the + * process list. + *

+ * Note: This is an extended syntax based on specific distributions. Please check whether the server + * supports this property before using it. + */ + Option CONSISTENT_SNAPSHOT_FROM_SESSION = Option.valueOf("consistentSnapshotFromSession"); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying + * {@link IsolationLevel}. + * + * @param isolationLevel the isolation level to use during the transaction. + * @return a new {@link MySqlTransactionDefinition} with the {@code isolationLevel}. + * @throws IllegalArgumentException if {@code isolationLevel} is {@code null}. + */ + MySqlTransactionDefinition isolationLevel(IsolationLevel isolationLevel); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and using the default + * isolation level. Removes transaction isolation level if configured already. + * + * @return a new {@link MySqlTransactionDefinition} without specified isolation level. + */ + MySqlTransactionDefinition withoutIsolationLevel(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and using read-only + * transaction semantics. Overrides transaction mutability if configured already. + * + * @return a new {@link MySqlTransactionDefinition} with read-only semantics. + */ + MySqlTransactionDefinition readOnly(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and using explicitly + * read-write transaction semantics. Overrides transaction mutability if configured already. + * + * @return a new {@link MySqlTransactionDefinition} with read-write semantics. + */ + MySqlTransactionDefinition readWrite(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and avoid to using + * explicitly mutability. Removes transaction mutability if configured already. + * + * @return a new {@link MySqlTransactionDefinition} without explicitly mutability. + */ + MySqlTransactionDefinition withoutMutability(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying a lock wait + * timeout. Overrides transaction lock wait timeout if configured already. + *

+ * Note: for now, it is only available in InnoDB or InnoDB-compatible engines. + * + * @param timeout the lock wait timeout. + * @return a new {@link MySqlTransactionDefinition} with the {@code timeout}. + * @throws IllegalArgumentException if {@code timeout} is {@code null}. + */ + MySqlTransactionDefinition lockWaitTimeout(Duration timeout); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to use the + * default lock wait timeout. Removes transaction lock wait timeout if configured already. + * + * @return a new {@link MySqlTransactionDefinition} without specified lock wait timeout. + */ + MySqlTransactionDefinition withoutLockWaitTimeout(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent snapshot. Overrides transaction consistency if configured already. + * + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + */ + MySqlTransactionDefinition consistent(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent engine snapshot. Overrides transaction consistency if configured already. + * + * @param engine the consistent snapshot engine, e.g. {@code ROCKSDB}. + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + * @throws IllegalArgumentException if {@code engine} is {@code null}. + */ + MySqlTransactionDefinition consistent(String engine); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent engine snapshot from session. Overrides transaction consistency if configured already. + * + * @param engine the consistent snapshot engine, e.g. {@code ROCKSDB}. + * @param sessionId the session id. + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + * @throws IllegalArgumentException if {@code engine} is {@code null}. + */ + MySqlTransactionDefinition consistent(String engine, long sessionId); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent snapshot from session. Overrides transaction consistency if configured already. + * + * @param sessionId the session id. + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + */ + MySqlTransactionDefinition consistentFromSession(long sessionId); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to without + * consistent snapshot. Removes transaction consistency if configured already. + * + * @return a new {@link MySqlTransactionDefinition} without consistent snapshot semantics. + */ + MySqlTransactionDefinition withoutConsistent(); + + /** + * Gets an empty {@link MySqlTransactionDefinition}. + * + * @return an empty {@link MySqlTransactionDefinition}. + */ + static MySqlTransactionDefinition empty() { + return SimpleTransactionDefinition.EMPTY; + } + + /** + * Creates a {@link MySqlTransactionDefinition} specifying transaction mutability. + * + * @param readWrite {@code true} for read-write, {@code false} to use a read-only transaction. + * @return a new {@link MySqlTransactionDefinition} using the specified transaction mutability. + */ + static MySqlTransactionDefinition mutability(boolean readWrite) { + return readWrite ? SimpleTransactionDefinition.EMPTY.readWrite() : + SimpleTransactionDefinition.EMPTY.readOnly(); + } + + static MySqlTransactionDefinition from(IsolationLevel isolationLevel) { + return SimpleTransactionDefinition.EMPTY.isolationLevel(isolationLevel); + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/SimpleTransactionDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/SimpleTransactionDefinition.java new file mode 100644 index 000000000..3c2d0f40d --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/SimpleTransactionDefinition.java @@ -0,0 +1,218 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Option; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; + +/** + * An implementation of {@link MySqlTransactionDefinition} for immutable transaction definition. + * + * @since 1.1.3 + */ +final class SimpleTransactionDefinition implements MySqlTransactionDefinition { + + static final SimpleTransactionDefinition EMPTY = new SimpleTransactionDefinition(Collections.emptyMap()); + + private final Map, Object> options; + + private SimpleTransactionDefinition(Map, Object> options) { + this.options = options; + } + + @SuppressWarnings("unchecked") + @Override + public T getAttribute(Option option) { + return (T) this.options.get(option); + } + + @Override + public MySqlTransactionDefinition isolationLevel(IsolationLevel isolationLevel) { + requireNonNull(isolationLevel, "isolationLevel must not be null"); + + return with(ISOLATION_LEVEL, isolationLevel); + } + + @Override + public MySqlTransactionDefinition withoutIsolationLevel() { + return without(ISOLATION_LEVEL); + } + + @Override + public MySqlTransactionDefinition readOnly() { + return with(READ_ONLY, true); + } + + @Override + public MySqlTransactionDefinition readWrite() { + return with(READ_ONLY, false); + } + + @Override + public MySqlTransactionDefinition withoutMutability() { + return without(READ_ONLY); + } + + @Override + public MySqlTransactionDefinition lockWaitTimeout(Duration timeout) { + requireNonNull(timeout, "timeout must not be null"); + + return with(LOCK_WAIT_TIMEOUT, timeout); + } + + @Override + public MySqlTransactionDefinition withoutLockWaitTimeout() { + return without(LOCK_WAIT_TIMEOUT); + } + + @Override + public MySqlTransactionDefinition consistent() { + return with(WITH_CONSISTENT_SNAPSHOT, true); + } + + @Override + public MySqlTransactionDefinition consistent(String engine) { + requireNonNull(engine, "engine must not be null"); + + return consistent0(CONSISTENT_SNAPSHOT_ENGINE, engine); + } + + @Override + public MySqlTransactionDefinition consistent(String engine, long sessionId) { + requireNonNull(engine, "engine must not be null"); + + Map, Object> options = new HashMap<>(this.options); + + options.put(WITH_CONSISTENT_SNAPSHOT, true); + options.put(CONSISTENT_SNAPSHOT_ENGINE, engine); + options.put(CONSISTENT_SNAPSHOT_FROM_SESSION, sessionId); + + return of(options); + } + + @Override + public MySqlTransactionDefinition consistentFromSession(long sessionId) { + return consistent0(CONSISTENT_SNAPSHOT_FROM_SESSION, sessionId); + } + + @Override + public MySqlTransactionDefinition withoutConsistent() { + if (this.options.isEmpty()) { + return this; + } + + Map, Object> options = new HashMap<>(this.options); + + options.remove(WITH_CONSISTENT_SNAPSHOT); + options.remove(CONSISTENT_SNAPSHOT_ENGINE); + options.remove(CONSISTENT_SNAPSHOT_FROM_SESSION); + + return of(options); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SimpleTransactionDefinition)) { + return false; + } + + SimpleTransactionDefinition that = (SimpleTransactionDefinition) o; + + return options.equals(that.options); + } + + @Override + public int hashCode() { + return options.hashCode(); + } + + @Override + public String toString() { + return "SimpleTransactionDefinition" + options; + } + + private MySqlTransactionDefinition with(Option option, T value) { + if (this.options.isEmpty()) { + return new SimpleTransactionDefinition(Collections.singletonMap(option, value)); + } + + if (value.equals(this.options.get(option))) { + return this; + } + + Map, Object> options = new HashMap<>(this.options); + + options.put(option, value); + + return of(options); + } + + private SimpleTransactionDefinition without(Option option) { + requireNonNull(option, "option must not be null"); + + if (!this.options.containsKey(option)) { + return this; + } + + if (this.options.size() == 1) { + return EMPTY; + } + + Map, Object> options = new HashMap<>(this.options); + + options.remove(option); + + return of(options); + } + + private MySqlTransactionDefinition consistent0(Option option, T value) { + if (Boolean.TRUE.equals(this.options.get(WITH_CONSISTENT_SNAPSHOT))) { + return with(option, value); + } + + Map, Object> options = new HashMap<>(this.options); + + options.put(WITH_CONSISTENT_SNAPSHOT, true); + options.put(option, value); + + return of(options); + } + + private static SimpleTransactionDefinition of(Map, Object> options) { + switch (options.size()) { + case 0: + return EMPTY; + case 1: { + Map.Entry, Object> e = options.entrySet().iterator().next(); + + return new SimpleTransactionDefinition(Collections.singletonMap(e.getKey(), e.getValue())); + } + default: + return new SimpleTransactionDefinition(options); + } + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/package-info.java new file mode 100644 index 000000000..65e67fa90 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * R2DBC driver API for MySQL. + */ + +@NotNullByDefault +package io.asyncer.r2dbc.mysql.api; + +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java similarity index 96% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java index 43d18fd32..2a9cd84e8 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java @@ -22,10 +22,10 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; /** - * An utility for general authentication hashing algorithm. + * A utility for general authentication hashing algorithm. */ final class AuthUtils { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java similarity index 97% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java index bf6919701..0d070dd00 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java @@ -19,7 +19,7 @@ import io.asyncer.r2dbc.mysql.collation.CharCollation; import org.jetbrains.annotations.Nullable; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java similarity index 96% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java index e00cf8292..52c92b969 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java @@ -21,7 +21,7 @@ import java.nio.CharBuffer; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java similarity index 98% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java index 41f640d0a..2ae157271 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java @@ -68,7 +68,7 @@ public interface MySqlAuthProvider { /** * Get the built-in authentication plugin provider through the specified {@code type}. * - * @param type the type name of a authentication plugin provider + * @param type the type name of an authentication plugin provider * @return the authentication plugin provider * @throws R2dbcPermissionDeniedException the {@code type} name not found */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java similarity index 96% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java index f2a17047c..cc90da4a2 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java @@ -21,7 +21,7 @@ import java.nio.CharBuffer; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java similarity index 96% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java index bf91b5921..6a88fdebd 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java @@ -21,7 +21,7 @@ import java.nio.CharBuffer; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java index cd5b412a3..92fa6bb83 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.authentication; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java similarity index 98% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java index 8fa9d38e8..27d5806fc 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java @@ -21,6 +21,9 @@ */ public final class Caches { + private Caches() { + } + /** * Create a new {@link QueryCache} by cache configuration. * diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java index 0a103f872..f695c24cd 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.cache; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java similarity index 81% rename from src/main/java/io/asyncer/r2dbc/mysql/client/Client.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java index 979748779..316d90999 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java @@ -22,12 +22,14 @@ import io.asyncer.r2dbc.mysql.message.server.ServerMessage; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; +import io.netty.resolver.AddressResolverGroup; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.jetbrains.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; +import reactor.netty.resources.LoopResources; import reactor.netty.tcp.TcpClient; import java.net.InetSocketAddress; @@ -45,21 +47,21 @@ public interface Client { InternalLogger logger = InternalLoggerFactory.getInstance(Client.class); /** - * Perform an exchange of a request message. Calling this method while a previous exchange is active will - * return a deferred handle and queue the request until the previous exchange terminates. + * Perform an exchange of a request message. Calling this method while a previous exchange is active will return a + * deferred handle and queue the request until the previous exchange terminates. * * @param request one and only one request message for get server responses - * @param handler response handler, {@link SynchronousSink#complete()} should be called after the last - * response frame is sent to complete the stream and prevent multiple subscribers from - * consuming previous, active response streams + * @param handler response handler, {@link SynchronousSink#complete()} should be called after the last response + * frame is sent to complete the stream and prevent multiple subscribers from consuming previous, + * active response streams * @param handling response type * @return A {@link Flux} of incoming messages that ends with the end of the frame */ Flux exchange(ClientMessage request, BiConsumer> handler); /** - * Perform an exchange of multi-request messages. Calling this method while a previous exchange is active - * will return a deferred handle and queue the request until the previous exchange terminates. + * Perform an exchange of multi-request messages. Calling this method while a previous exchange is active will + * return a deferred handle and queue the request until the previous exchange terminates. * * @param exchangeable request messages and response handler * @param handling response type @@ -90,6 +92,14 @@ public interface Client { */ ByteBufAllocator getByteBufAllocator(); + /** + * Returns the current {@link ConnectionContext}. It should not be retained long-term as it may change on reconnects + * or redirects. + * + * @return the {@link ConnectionContext} + */ + ConnectionContext getContext(); + /** * Checks if the connection is open. * @@ -116,17 +126,22 @@ public interface Client { * @param tcpNoDelay if enable the {@link ChannelOption#TCP_NODELAY} * @param context the connection context * @param connectTimeout connect timeout, or {@code null} if it has no timeout + * @param loopResources the loop resources to use + * @param metrics if enable the {@link TcpClient#metrics)} * @return A {@link Mono} that will emit a connected {@link Client}. * @throws IllegalArgumentException if {@code ssl}, {@code address} or {@code context} is {@code null}. * @throws ArithmeticException if {@code connectTimeout} milliseconds overflow as an int */ static Mono connect(MySqlSslConfiguration ssl, SocketAddress address, boolean tcpKeepAlive, - boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout) { + boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout, + LoopResources loopResources, @Nullable AddressResolverGroup resolver, boolean metrics) { requireNonNull(ssl, "ssl must not be null"); requireNonNull(address, "address must not be null"); requireNonNull(context, "context must not be null"); - TcpClient tcpClient = TcpClient.newConnection(); + TcpClient tcpClient = TcpClient.newConnection() + .runOn(loopResources) + .metrics(metrics); if (connectTimeout != null) { tcpClient = tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, @@ -138,6 +153,10 @@ static Mono connect(MySqlSslConfiguration ssl, SocketAddress address, bo tcpClient = tcpClient.option(ChannelOption.TCP_NODELAY, tcpNoDelay); } + if (resolver != null) { + tcpClient = tcpClient.resolver(resolver); + } + return tcpClient.remoteAddress(() -> address).connect() .map(conn -> new ReactorNettyClient(conn, ssl, context)); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java new file mode 100644 index 000000000..f71a85ba6 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java @@ -0,0 +1,243 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import io.asyncer.r2dbc.mysql.constant.Packets; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.jetbrains.annotations.Nullable; + +import java.net.SocketAddress; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A codec that compresses and decompresses packets. + *

    + *
  • Read: compression {@link ByteBuf} -> compression-framed {@link ByteBuf} -> + * decompressed {@link ByteBuf}
  • + *
  • Write: uncompressed-framed {@link ByteBuf} -> compression-framed {@link ByteBuf}
  • + *
+ */ +final class CompressionDuplexCodec extends ByteToMessageDecoder implements ChannelOutboundHandler { + + static final String NAME = "R2dbcMysqlCompressionDuplexCodec"; + + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(CompressionDuplexCodec.class); + + private static final int MIN_COMPRESS_LENGTH = 50; + + /** + * Compression packet sequence id, incremented independently of the normal sequence id. + */ + private final AtomicInteger sequenceId = new AtomicInteger(0); + + private final Compressor compressor; + + @Nullable + private ByteBuf writeCumulated; + + private final Cumulator writeCumulator = MERGE_CUMULATOR; + + private int frameLength = -1; + + CompressionDuplexCodec(Compressor compressor) { + this.compressor = compressor; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + if (msg instanceof ByteBuf) { + ByteBuf cumulated = this.writeCumulated == null ? ctx.alloc().buffer(0, 0) : + this.writeCumulated; + + this.writeCumulated = cumulated = writeCumulator.cumulate(ctx.alloc(), cumulated, (ByteBuf) msg); + + while (cumulated.readableBytes() >= Packets.MAX_PAYLOAD_SIZE) { + logger.trace("Accumulated to the maximum payload, compressing"); + + ByteBuf slice = cumulated.readSlice(Packets.MAX_PAYLOAD_SIZE); + ByteBuf compressed = compressor.compress(slice); + + if (compressed.readableBytes() >= slice.readableBytes()) { + logger.trace("Sending uncompressed due to compressed payload is larger than original"); + compressed.release(); + ctx.write(buildHeader(ctx, slice.readableBytes(), 0)); + ctx.write(slice.retain()); + } else { + logger.trace("Sending compressed payload"); + ctx.write(buildHeader(ctx, compressed.readableBytes(), Packets.MAX_PAYLOAD_SIZE)); + ctx.write(compressed); + } + } + + if (!cumulated.isReadable()) { + this.writeCumulated = null; + cumulated.release(); + } else { + logger.trace("Accumulated writing buffers, waiting for flush"); + } + } else { + ctx.write(msg, promise); + } + } + + private ByteBuf buildHeader(ChannelHandlerContext ctx, int compressedSize, int uncompressedSize) { + return ctx.alloc().ioBuffer(Packets.COMPRESS_HEADER_SIZE) + .writeMediumLE(compressedSize) + .writeByte(sequenceId.getAndIncrement()) + .writeMediumLE(uncompressedSize); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ByteBuf cumulated = this.writeCumulated; + + this.writeCumulated = null; + + if (cumulated == null) { + ctx.flush(); + return; + } + + int uncompressedSize = cumulated.readableBytes(); + + if (uncompressedSize < MIN_COMPRESS_LENGTH) { + logger.trace("flushing, payload is too small to compress, sending uncompressed"); + ctx.write(buildHeader(ctx, uncompressedSize, 0)); + ctx.writeAndFlush(cumulated); + } else { + try { + logger.trace("flushing, compressing payload"); + + ByteBuf compressed = compressor.compress(cumulated); + + if (compressed.readableBytes() >= uncompressedSize) { + logger.trace("Sending uncompressed due to compressed payload is larger than original"); + compressed.release(); + ctx.write(buildHeader(ctx, uncompressedSize, 0)); + ctx.writeAndFlush(cumulated.retain()); + } else { + logger.trace("Sending compressed payload"); + ctx.write(buildHeader(ctx, compressed.readableBytes(), uncompressedSize)); + ctx.writeAndFlush(compressed); + } + } finally { + cumulated.release(); + } + } + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + ByteBuf frame = decode(in); + + if (frame != null) { + out.add(frame); + } + } + + @Nullable + private ByteBuf decode(ByteBuf in) { + if (frameLength == -1) { + // New frame + if (in.readableBytes() < Packets.SIZE_FIELD_SIZE) { + return null; + } + + frameLength = in.getUnsignedMediumLE(in.readerIndex()) + Packets.COMPRESS_HEADER_SIZE; + } + + if (in.readableBytes() < frameLength) { + return null; + } + + in.skipBytes(Packets.SIZE_FIELD_SIZE); + + int sequenceId = in.readUnsignedByte(); + int uncompressedSize = in.readUnsignedMediumLE(); + ByteBuf frame = in.readRetainedSlice(frameLength - Packets.COMPRESS_HEADER_SIZE); + + logger.trace("Decoded frame with sequence id: {}, total size: {}, uncompressed size: {}", + sequenceId, frameLength, uncompressedSize); + this.frameLength = -1; + this.sequenceId.set(sequenceId + 1); + + if (uncompressedSize == 0) { + return frame; + } else { + try { + return compressor.decompress(frame, uncompressedSize); + } finally { + frame.release(); + } + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (PacketEvent.RESET_SEQUENCE == evt) { + logger.debug("Reset sequence id"); + this.sequenceId.set(0); + } + + ctx.fireUserEventTriggered(evt); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, + ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) { + this.compressor.dispose(); + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java new file mode 100644 index 000000000..ee7adbdf1 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import io.netty.buffer.ByteBuf; +import reactor.core.Disposable; + +/** + * An abstraction considers to compress and decompress data. + */ +interface Compressor extends Disposable { + + /** + * Compresses the given {@link ByteBuf}. It does not guarantee that the compressed data is smaller than + * the original. It will not change the reader index of the given {@link ByteBuf}. It may return early if + * the compressed data is not smaller than the original. + * + * @param buf the {@link ByteBuf} to compress + * @return the compressed {@link ByteBuf} + */ + ByteBuf compress(ByteBuf buf); + + /** + * Decompresses the given {@link ByteBuf}. + * + * @param buf the {@link ByteBuf} to decompress + * @param uncompressedSize the size of the uncompressed data + * @return the decompressed {@link ByteBuf} + */ + ByteBuf decompress(ByteBuf buf, int uncompressedSize); +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java similarity index 99% rename from src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java index 8b58a7547..39dc784c1 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java @@ -124,7 +124,7 @@ private static boolean matchIpv6(String ip, List sans) { String host = normaliseIpv6(ip); for (San san : sans) { - // IP must be case sensitive. + // IP must be case-sensitive. if (San.IP == san.getType() && host.equals(normaliseIpv6(san.getValue()))) { if (LOG_DEBUG) { logger.debug("Certificate for '{}' matched IPv6 '{}' of the Subject Alternative Names", @@ -231,7 +231,7 @@ private static boolean matchHost(String host, String pattern) { int remainderIndex = host.length() - postfixSize; if (remainderIndex <= asteriskIndex) { - // Asterisk must to match least one character. + // The asterisk must match at least one character. // In other words: groups.*.example.com can not match groups..example.com return false; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java similarity index 50% rename from src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java index 1641e480c..8fb3d47e1 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java @@ -17,9 +17,9 @@ package io.asyncer.r2dbc.mysql.client; import io.asyncer.r2dbc.mysql.ConnectionContext; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.asyncer.r2dbc.mysql.internal.util.OperatorUtils; import io.asyncer.r2dbc.mysql.message.client.ClientMessage; -import io.asyncer.r2dbc.mysql.message.client.SubsequenceClientMessage; import io.asyncer.r2dbc.mysql.message.client.PrepareQueryMessage; import io.asyncer.r2dbc.mysql.message.client.PreparedFetchMessage; import io.asyncer.r2dbc.mysql.message.client.SslRequest; @@ -34,51 +34,63 @@ import io.asyncer.r2dbc.mysql.message.server.SyntheticMetadataMessage; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; +import org.jetbrains.annotations.Nullable; import reactor.core.publisher.Flux; +import java.net.SocketAddress; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** - * Client/server messages encode/decode logic. + * A codec that encodes and decodes MySQL messages. + *
    + *
  • Read: {@link ByteBuf} -> framed {@link ByteBuf} -> {@link ServerMessage}
  • + *
  • Write: {@link ClientMessage} -> framed {@link ByteBuf} with last flush
  • + *
*/ -final class MessageDuplexCodec extends ChannelDuplexHandler { +final class MessageDuplexCodec extends ByteToMessageDecoder implements ChannelOutboundHandler { static final String NAME = "R2dbcMySqlMessageDuplexCodec"; private static final InternalLogger logger = InternalLoggerFactory.getInstance(MessageDuplexCodec.class); + private final AtomicInteger sequenceId = new AtomicInteger(0); + private DecodeContext decodeContext = DecodeContext.login(); + /** + * It can be retained because reconnect and redirect will re-create the {@link MessageDuplexCodec}. + */ private final ConnectionContext context; private final ServerMessageDecoder decoder = new ServerMessageDecoder(); + private int frameLength = -1; + MessageDuplexCodec(ConnectionContext context) { this.context = requireNonNull(context, "context must not be null"); } @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - if (msg instanceof ByteBuf) { + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + ByteBuf frame = decode(in); + + if (frame != null) { DecodeContext context = this.decodeContext; - ServerMessage message = this.decoder.decode((ByteBuf) msg, this.context, context); + ServerMessage message = this.decoder.decode(frame, this.context, context); if (message != null) { - handleDecoded(ctx, message); + handleDecoded(out, message); } - } else if (msg instanceof ServerMessage) { - ctx.fireChannelRead(msg); - } else { - if (logger.isWarnEnabled()) { - logger.warn("Unknown message type {} on reading", msg.getClass()); - } - ReferenceCountUtil.release(msg); } } @@ -86,22 +98,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { if (msg instanceof ClientMessage) { ByteBufAllocator allocator = ctx.alloc(); - Flux encoded; + ClientMessage message = (ClientMessage) msg; + Flux encoded = Flux.from(message.encode(allocator, this.context)); - if (msg instanceof SubsequenceClientMessage) { - SubsequenceClientMessage message = (SubsequenceClientMessage) msg; - - encoded = Flux.from(message.encode(allocator, this.context)); - int envelopeId = message.getEnvelopeId(); - - OperatorUtils.envelope(encoded, allocator, envelopeId, false) - .subscribe(new WriteSubscriber(ctx, promise)); - } else { - encoded = Flux.from(((ClientMessage) msg).encode(allocator, this.context)); - - OperatorUtils.envelope(encoded, allocator, 0, true) - .subscribe(new WriteSubscriber(ctx, promise)); - } + OperatorUtils.envelope(encoded, allocator, sequenceId, message.isCumulative()) + .subscribe(new WriteSubscriber(ctx, promise)); if (msg instanceof PrepareQueryMessage) { setDecodeContext(DecodeContext.prepareQuery()); @@ -118,13 +119,74 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) } } + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof PacketEvent) { + switch ((PacketEvent) evt) { + case RESET_SEQUENCE: + logger.trace("Reset sequence id"); + this.sequenceId.set(0); + break; + case USE_COMPRESSION: + logger.trace("Reset sequence id"); + this.sequenceId.set(0); + + if (context.getCapability().isZstdCompression()) { + enableZstdCompression(ctx); + } else if (context.getCapability().isZlibCompression()) { + enableZlibCompression(ctx); + } else { + logger.warn("Unexpected event compression triggered, no capability found"); + } + break; + default: + // Ignore unknown event + break; + } + } + + ctx.fireUserEventTriggered(evt); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + @Override public void channelInactive(ChannelHandlerContext ctx) { decoder.dispose(); ctx.fireChannelInactive(); } - private void handleDecoded(ChannelHandlerContext ctx, ServerMessage msg) { + @Nullable + private ByteBuf decode(ByteBuf in) { + if (frameLength == -1) { + // New frame + if (in.readableBytes() < Packets.SIZE_FIELD_SIZE) { + return null; + } + + frameLength = in.getUnsignedMediumLE(in.readerIndex()) + Packets.NORMAL_HEADER_SIZE; + } + + if (in.readableBytes() < frameLength) { + return null; + } + + in.skipBytes(Packets.SIZE_FIELD_SIZE); + + int sequenceId = in.readUnsignedByte(); + ByteBuf frame = in.readRetainedSlice(frameLength - Packets.NORMAL_HEADER_SIZE); + + logger.trace("Decoded frame with sequence id: {}, total size: {}", sequenceId, frameLength); + this.sequenceId.set(sequenceId + 1); + this.frameLength = -1; + + return frame; + } + + private void handleDecoded(List out, ServerMessage msg) { if (msg instanceof ServerStatusMessage) { this.context.setServerStatuses(((ServerStatusMessage) msg).getServerStatuses()); } @@ -159,7 +221,7 @@ private void handleDecoded(ChannelHandlerContext ctx, ServerMessage msg) { } // Generic handle. - ctx.fireChannelRead(msg); + out.add(msg); } private void setDecodeContext(DecodeContext context) { @@ -168,4 +230,59 @@ private void setDecodeContext(DecodeContext context) { logger.debug("Decode context change to {}", context); } } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, + ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) { + ctx.read(); + } + + private static void enableZstdCompression(ChannelHandlerContext ctx) { + CompressionDuplexCodec handler = new CompressionDuplexCodec( + new ZstdCompressor(3)); + + if (ctx.pipeline().get(CompressionDuplexCodec.NAME) != null) { + logger.warn("Unexpected event, compression already enabled"); + } else { + logger.debug("Compression zstd enabled for subsequent packets"); + ctx.pipeline().addBefore(NAME, CompressionDuplexCodec.NAME, handler); + } + } + + private static void enableZlibCompression(ChannelHandlerContext ctx) { + CompressionDuplexCodec handler = new CompressionDuplexCodec(new ZlibCompressor()); + + if (ctx.pipeline().get(CompressionDuplexCodec.NAME) != null) { + logger.warn("Unexpected event, compression already enabled"); + } else { + logger.debug("Compression zlib enabled for subsequent packets"); + ctx.pipeline().addBefore(NAME, CompressionDuplexCodec.NAME, handler); + } + } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java new file mode 100644 index 000000000..c8cd53906 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +/** + * A packet event considers how the handler should handle subsequent packets. + */ +enum PacketEvent { + + /** + * Sequence is reset, all sequence IDs should be reset to 0. + */ + RESET_SEQUENCE, + + /** + * Compression is enabled, the handler should decode the next packet as a compression packet. + *

+ * It should just reset the normal sequence ID to 0. + */ + USE_COMPRESSION, +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java similarity index 90% rename from src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index e1917aacb..5054f3631 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -95,9 +95,7 @@ final class ReactorNettyClient implements Client { this.context = context; // Note: encoder/decoder should before reactor bridge. - connection.addHandlerLast(EnvelopeSlicer.NAME, new EnvelopeSlicer()) - .addHandlerLast(MessageDuplexCodec.NAME, - new MessageDuplexCodec(context)); + connection.addHandlerLast(MessageDuplexCodec.NAME, new MessageDuplexCodec(context)); if (ssl.getSslMode().startSsl()) { connection.addHandlerFirst(SslBridgeHandler.NAME, new SslBridgeHandler(context, ssl)); @@ -120,7 +118,7 @@ final class ReactorNettyClient implements Client { } sink.next((ServerMessage) it); } else { - // ReferenceCounted will released by Netty. + // ReferenceCounted will be released by Netty. throw ClientExceptions.unsupportedProtocol(it.getClass().getTypeName()); } }) @@ -133,6 +131,18 @@ final class ReactorNettyClient implements Client { logger.debug("Request: {}", message); } + if (message == ExitMessage.INSTANCE) { + if (STATE_UPDATER.compareAndSet(this, ST_CONNECTED, ST_CLOSING)) { + logger.debug("Exit message sent"); + } else { + logger.debug("Exit message sent (duplicated / connection already closed)"); + } + } + + if (message.isSequenceReset()) { + resetSequence(connection); + } + return connection.outbound().sendObject(message); }) .onErrorResume(this::resumeError) @@ -211,15 +221,8 @@ public Mono close() { requestQueue.submit(RequestTask.wrap(sink, Mono.fromRunnable(() -> { Sinks.EmitResult result = requests.tryEmitNext(ExitMessage.INSTANCE); - if (result != Sinks.EmitResult.OK) { logger.error("Exit message sending failed due to {}, force closing", result); - } else { - if (STATE_UPDATER.compareAndSet(this, ST_CONNECTED, ST_CLOSING)) { - logger.debug("Exit message sent"); - } else { - logger.debug("Exit message sent (duplicated / connection already closed)"); - } } }))); }).flatMap(Function.identity()).onErrorResume(e -> { @@ -238,6 +241,11 @@ public ByteBufAllocator getByteBufAllocator() { return connection.outbound().alloc(); } + @Override + public ConnectionContext getContext() { + return context; + } + @Override public boolean isConnected() { return state < ST_CLOSED && connection.channel().isOpen(); @@ -250,13 +258,21 @@ public void sslUnsupported() { @Override public void loginSuccess() { - connection.channel().pipeline().fireUserEventTriggered(Lifecycle.COMMAND); + if (context.getCapability().isCompression()) { + connection.channel().pipeline().fireUserEventTriggered(PacketEvent.USE_COMPRESSION); + } else { + resetSequence(connection); + } + } + + private static void resetSequence(Connection connection) { + connection.channel().pipeline().fireUserEventTriggered(PacketEvent.RESET_SEQUENCE); } @Override public String toString() { return String.format("ReactorNettyClient(%s){connectionId=%d}", - isConnected() ? "activating" : "clsoing or closed", context.getConnectionId()); + isConnected() ? "activating" : "closing or closed", context.getConnectionId()); } private void emitNextRequest(ClientMessage request) { @@ -363,17 +379,17 @@ public void error(Throwable e) { @Override public void next(ServerMessage message) { - if (message instanceof WarningMessage) { - int warnings = ((WarningMessage) message).getWarnings(); - if (warnings == 0) { - if (DEBUG_ENABLED) { + if (DEBUG_ENABLED) { + if (message instanceof WarningMessage) { + final int warnings = ((WarningMessage) message).getWarnings(); + if (warnings == 0) { logger.debug("Response: {}", message); + } else { + logger.debug("Response: {}, reports {} warning(s)", message, warnings); } - } else if (INFO_ENABLED) { - logger.info("Response: {}, reports {} warning(s)", message, warnings); + } else { + logger.debug("Response: {}", message); } - } else if (DEBUG_ENABLED) { - logger.debug("Response: {}", message); } responseProcessor.emitNext(message, EmitFailureHandler.FAIL_FAST); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/San.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/San.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/San.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/San.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java similarity index 89% rename from src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java index 823a59187..22038bb43 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java @@ -33,6 +33,7 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; +import reactor.core.Exceptions; import reactor.netty.tcp.SslProvider; import javax.net.ssl.HostnameVerifier; @@ -40,7 +41,6 @@ import javax.net.ssl.SSLException; import java.io.File; import java.net.InetSocketAddress; -import java.util.function.Consumer; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; import static io.netty.handler.ssl.SslProvider.JDK; @@ -77,6 +77,9 @@ final class SslBridgeHandler extends ChannelDuplexHandler { private static final ServerVersion MYSQL_5_7_28 = ServerVersion.create(5, 7, 28); + /** + * It can be retained because reconnect and redirect will re-create the {@link SslBridgeHandler}. + */ private final ConnectionContext context; private final MySqlSslConfiguration ssl; @@ -96,7 +99,7 @@ public void handlerAdded(ChannelHandlerContext ctx) { } @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof SslState) { handleSslState(ctx, (SslState) evt); // Ignore event trigger for next handler, because it used only by this handler. @@ -105,7 +108,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc handleSslCompleted(ctx, (SslHandshakeCompletionEvent) evt); } - super.userEventTriggered(ctx, evt); + ctx.fireUserEventTriggered(evt); } private void handleSslCompleted(ChannelHandlerContext ctx, SslHandshakeCompletionEvent evt) { @@ -148,10 +151,16 @@ private void handleSslState(ChannelHandlerContext ctx, SslState state) { switch (state) { case BRIDGING: logger.debug("SSL event triggered, enable SSL handler to pipeline"); - - SslProvider sslProvider = SslProvider.builder() - .sslContext(MySqlSslContextSpec.forClient(ssl, context)) - .build(); + final SslProvider sslProvider; + try { + // Workaround for a forward incompatible change in reactor-netty version 1.2.0 + // See: https://github.com/reactor/reactor-netty/commit/6d0c24d83a7c5b15e403475272293f847415191c + sslProvider = SslProvider.builder() + .sslContext(MySqlSslContextSpec.forClient(ssl, context).sslContext()) + .build(); + } catch (SSLException e) { + throw Exceptions.propagate(e); + } SslHandler sslHandler = sslProvider.getSslContext().newHandler(ctx.alloc()); this.sslEngine = sslHandler.engine(); @@ -192,22 +201,14 @@ private static boolean isTls13Enabled(ConnectionContext context) { || (version.isGreaterThanOrEqualTo(MYSQL_5_6_0) && version.isEnterprise()); } - private static final class MySqlSslContextSpec implements SslProvider.ProtocolSslContextSpec { + private static final class MySqlSslContextSpec { private final SslContextBuilder builder; - private MySqlSslContextSpec(SslContextBuilder builder) { this.builder = builder; } - - @Override - public MySqlSslContextSpec configure(Consumer customizer) { - requireNonNull(customizer, "customizer must not be null"); - - customizer.accept(builder); - - return this; + private MySqlSslContextSpec(SslContextBuilder builder) { + this.builder = builder; } - @Override public SslContext sslContext() throws SSLException { return builder.build(); } @@ -220,8 +221,10 @@ static MySqlSslContextSpec forClient(MySqlSslConfiguration ssl, ConnectionContex .applicationProtocolConfig(null); String[] tlsProtocols = ssl.getTlsVersion(); - if (tlsProtocols.length > 0) { - builder.protocols(tlsProtocols); + if (tlsProtocols.length > 0 || ssl.getSslMode() == SslMode.TUNNEL) { + if (tlsProtocols.length > 0) { + builder.protocols(tlsProtocols); + } } else if (isTls13Enabled(context)) { builder.protocols(TLS_PROTOCOLS); } else { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java similarity index 86% rename from src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java index cc5e14cbb..ee085cfa3 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java @@ -27,6 +27,11 @@ * streaming {@link ByteBuf}s. *

* It ensures {@link #promise} will be complete. + *

+ * Note: flush is required due to the message may be encoded by another thread, like: + * {@link io.asyncer.r2dbc.mysql.message.client.LocalInfileResponse LocalInfileResponse}, + * {@link io.asyncer.r2dbc.mysql.message.client.PreparedExecuteMessage PreparedExecuteMessage} (Blob/Clob), + * etc. */ final class WriteSubscriber implements CoreSubscriber { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java new file mode 100644 index 000000000..5bd749a44 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java @@ -0,0 +1,179 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.DecoderException; + +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +/** + * An implementation of {@link Compressor} that uses the zlib compression algorithm. + * + * @see io.netty.handler.codec.compression Netty Compression Codecs + */ +final class ZlibCompressor implements Compressor { + + /** + * The maximum size of input buffer and the maximum initial capacity of the compressed data buffer. + *

+ * Note: uncompressed size is already known, so the buffer should be allocated with the exact size. + */ + private static final int MAX_CHUNK_SIZE = 65536; + + private final Deflater deflater = new Deflater(); + + private final Inflater inflater = new Inflater(); + + @Override + public ByteBuf compress(ByteBuf buf) { + int len = buf.readableBytes(); + + if (len == 0) { + return buf.alloc().buffer(0, 0); + } + + try { + if (buf.hasArray()) { + byte[] input = buf.array(); + int offset = buf.arrayOffset() + buf.readerIndex(); + ByteBuf out = buf.alloc().heapBuffer(Math.min(len, MAX_CHUNK_SIZE)); + + deflater.setInput(input, offset, len); + deflater.finish(); + deflateAll(out, len); + + return out; + } else { + byte[] input = new byte[Math.min(len, MAX_CHUNK_SIZE)]; + int readerIndex = buf.readerIndex(); + int writerIndex = buf.writerIndex(); + ByteBuf out = buf.alloc().heapBuffer(Math.min(len, MAX_CHUNK_SIZE)); + + while (writerIndex - readerIndex > 0) { + int numBytes = Math.min(input.length, writerIndex - readerIndex); + + buf.getBytes(readerIndex, input, 0, numBytes); + deflater.setInput(input, 0, numBytes); + readerIndex += numBytes; + deflateAll(out, len); + } + + deflater.finish(); + deflateAll(out, len); + + return out; + } + } finally { + deflater.reset(); + } + } + + @Override + public ByteBuf decompress(ByteBuf buf, int uncompressedSize) { + int len = buf.readableBytes(); + + if (len == 0) { + return buf.alloc().buffer(0, 0); + } + + try { + if (buf.hasArray()) { + byte[] input = buf.array(); + int offset = buf.arrayOffset() + buf.readerIndex(); + ByteBuf out = buf.alloc().heapBuffer(uncompressedSize); + + inflater.setInput(input, offset, len); + inflateAll(out); + + return out; + } else { + byte[] input = new byte[Math.min(len, MAX_CHUNK_SIZE)]; + + int readerIndex = buf.readerIndex(); + int writerIndex = buf.writerIndex(); + ByteBuf out = buf.alloc().heapBuffer(uncompressedSize); + + while (writerIndex - readerIndex > 0) { + int numBytes = Math.min(input.length, writerIndex - readerIndex); + + buf.getBytes(readerIndex, input, 0, numBytes); + inflater.setInput(input, 0, numBytes); + readerIndex += numBytes; + inflateAll(out); + } + + return out; + } + } catch (DataFormatException e) { + throw new DecoderException("zlib decompress failed", e); + } finally { + inflater.reset(); + } + } + + @Override + public void dispose() { + deflater.end(); + inflater.end(); + } + + private void deflateAll(ByteBuf out, int maxSize) { + while (true) { + deflate(out); + + if (!out.isWritable()) { + int size = out.readableBytes(); + + if (size >= maxSize) { + break; + } + + // Capacity = written size * 2 + if (size > (maxSize >> 1)) { + out.ensureWritable(maxSize - size); + } else { + out.ensureWritable(size); + } + } else if (deflater.needsInput()) { + break; + } + } + } + + private void inflateAll(ByteBuf out) throws DataFormatException { + while (out.isWritable() && !inflater.finished()) { + int wid = out.writerIndex(); + int numBytes = inflater.inflate(out.array(), out.arrayOffset() + wid, out.writableBytes()); + + out.writerIndex(wid + numBytes); + } + } + + private void deflate(ByteBuf out) { + int wid = out.writerIndex(); + int written = deflater.deflate(out.array(), out.arrayOffset() + wid, out.writableBytes()); + + while (written > 0) { + wid += written; + out.writerIndex(wid); + written = deflater.deflate(out.array(), out.arrayOffset() + wid, out.writableBytes()); + } + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java new file mode 100644 index 000000000..b25c8ed0c --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import com.github.luben.zstd.Zstd; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.nio.ByteBuffer; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; + +/** + * An implementation of {@link Compressor} that uses the Z-standard compression algorithm. + * + * @see Zstandard + */ +final class ZstdCompressor implements Compressor { + + private final int compressionLevel; + + ZstdCompressor(int compressionLevel) { + require( + compressionLevel >= Zstd.minCompressionLevel() && compressionLevel <= Zstd.maxCompressionLevel(), + "compressionLevel must be a value of Z standard compression levels"); + + this.compressionLevel = compressionLevel; + } + + @Override + public ByteBuf compress(ByteBuf buf) { + ByteBuffer buffer = Zstd.compress(buf.nioBuffer(), compressionLevel); + return Unpooled.wrappedBuffer(buffer); + } + + @Override + public ByteBuf decompress(ByteBuf buf, int uncompressedSize) { + ByteBuffer buffer = Zstd.decompress(buf.nioBuffer(), uncompressedSize); + return Unpooled.wrappedBuffer(buffer); + } + + @Override + public void dispose() { + // Do nothing + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java index e6016d09e..8ecf67881 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.client; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java similarity index 77% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java index 9e26692fb..2c22d1105 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; /** * Codec for classed type when field bytes less or equals than {@link Integer#MAX_VALUE}. @@ -32,9 +32,14 @@ abstract class AbstractClassedCodec implements Codec { } @Override - public final boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public final boolean canDecode(MySqlReadableMetadata metadata, Class target) { return target.isAssignableFrom(this.type) && doCanDecode(metadata); } - protected abstract boolean doCanDecode(MySqlColumnMetadata metadata); + @Override + public final Class getMainClass() { + return this.type; + } + + protected abstract boolean doCanDecode(MySqlReadableMetadata metadata); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java similarity index 97% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java index 8adc58f02..ac74b983b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java @@ -52,5 +52,5 @@ public final void dispose() { } @Nullable - abstract protected Publisher getDiscard(); + protected abstract Publisher getDiscard(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java similarity index 74% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java index 3370d79d0..a3844d28e 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; @@ -25,7 +25,7 @@ * * @param the boxed type of handling primitive data. */ -abstract class AbstractPrimitiveCodec implements PrimitiveCodec { +abstract class AbstractPrimitiveCodec implements Codec { private final Class primitiveClass; @@ -40,12 +40,18 @@ abstract class AbstractPrimitiveCodec implements PrimitiveCodec { } @Override - public final boolean canDecode(MySqlColumnMetadata metadata, Class target) { - return target.isAssignableFrom(boxedClass) && canPrimitiveDecode(metadata); + public final boolean canDecode(MySqlReadableMetadata metadata, Class target) { + return (target.isAssignableFrom(boxedClass) || target.equals(primitiveClass)) && doCanDecode(metadata); } - @Override public final Class getPrimitiveClass() { return primitiveClass; } + + @Override + public final Class getMainClass() { + return boxedClass; + } + + protected abstract boolean doCanDecode(MySqlReadableMetadata metadata); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java index 7d7e7008c..ced23b056 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -39,7 +39,7 @@ private BigDecimalCodec() { } @Override - public BigDecimal decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public BigDecimal decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -82,7 +82,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java similarity index 94% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java index de31cdfac..622c9edf0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -40,7 +40,7 @@ private BigIntegerCodec() { } @Override - public BigInteger decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public BigInteger decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -93,7 +93,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } @@ -140,7 +140,7 @@ private static BigInteger decimalBigInteger(ByteBuf buf) { return new BigDecimal(buf.toString(StandardCharsets.US_ASCII)).toBigInteger(); } - private static class BigIntegerMySqlParameter extends AbstractMySqlParameter { + private static final class BigIntegerMySqlParameter extends AbstractMySqlParameter { private final BigInteger value; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java index 839c81a6e..7ad97b1fb 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -41,7 +41,7 @@ private BitSetCodec() { } @Override - public BitSet decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public BitSet decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return BitSet.valueOf(EMPTY_BYTES); @@ -91,7 +91,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.BIT; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java similarity index 94% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java index 0363986e3..29ec42b14 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.lob.LobUtils; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -50,19 +50,24 @@ private BlobCodec() { } @Override - public Blob decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Class getMainClass() { + return Blob.class; + } + + @Override + public Blob decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createBlob(value); } @Override - public Blob decodeMassive(List value, MySqlColumnMetadata metadata, Class target, + public Blob decodeMassive(List value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createBlob(value); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { MySqlType type = metadata.getType(); return (type.isLob() || type == MySqlType.GEOMETRY) && target.isAssignableFrom(Blob.class); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java similarity index 56% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index f2c479e52..8fb98c273 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java @@ -16,12 +16,15 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import java.math.BigInteger; + import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.r2dbc.spi.R2dbcNonTransientResourceException; import reactor.core.publisher.Mono; /** @@ -29,6 +32,8 @@ */ final class BooleanCodec extends AbstractPrimitiveCodec { + private static final Integer INTEGER_ONE = Integer.valueOf(1); + static final BooleanCodec INSTANCE = new BooleanCodec(); private BooleanCodec() { @@ -36,9 +41,37 @@ private BooleanCodec() { } @Override - public Boolean decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Boolean decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { - return binary || metadata.getType() == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0'; + MySqlType dataType = metadata.getType(); + + if (dataType == MySqlType.VARCHAR) { + if (!value.isReadable()) { + return createFromLong(0); + } + + String s = value.toString(metadata.getCharCollation(context).getCharset()); + + if (s.equalsIgnoreCase("Y") || s.equalsIgnoreCase("yes") || + s.equalsIgnoreCase("T") || s.equalsIgnoreCase("true")) { + return createFromLong(1); + } else if (s.equalsIgnoreCase("N") || s.equalsIgnoreCase("no") || + s.equalsIgnoreCase("F") || s.equalsIgnoreCase("false")) { + return createFromLong(0); + } else if (s.matches("-?\\d*\\.\\d*") || s.matches("-?\\d*\\.\\d+[eE]-?\\d+") + || s.matches("-?\\d*[eE]-?\\d+")) { + return createFromDouble(Double.parseDouble(s)); + } else if (s.matches("-?\\d+")) { + if (!CodecUtils.isGreaterThanLongMax(s)) { + return createFromLong(CodecUtils.parseLong(value)); + } + return createFromBigInteger(new BigInteger(s)); + } + throw new R2dbcNonTransientResourceException("The value '" + s + "' of type '" + dataType + + "' cannot be encoded into a Boolean.", "22018"); + } + + return binary || dataType == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0'; } @Override @@ -48,13 +81,26 @@ public boolean canEncode(Object value) { @Override public MySqlParameter encode(Object value, CodecContext context) { - return (Boolean) value? BooleanMySqlParameter.TRUE : BooleanMySqlParameter.FALSE; + return (Boolean) value ? BooleanMySqlParameter.TRUE : BooleanMySqlParameter.FALSE; } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); - return (type == MySqlType.BIT || type == MySqlType.TINYINT) && metadata.getNativePrecision() == 1; + return ((type == MySqlType.BIT || type == MySqlType.TINYINT) && + INTEGER_ONE.equals(metadata.getPrecision())) || type == MySqlType.VARCHAR; + } + + public Boolean createFromLong(long l) { + return (l == -1 || l > 0); + } + + public Boolean createFromDouble(double d) { + return (d == -1.0d || d > 0); + } + + public Boolean createFromBigInteger(BigInteger b) { + return b.compareTo(BigInteger.valueOf(0)) > 0 || b.compareTo(BigInteger.valueOf(-1)) == 0; } private static final class BooleanMySqlParameter extends AbstractMySqlParameter { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java similarity index 94% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java index aa4beb2fb..4dd9b989c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -42,7 +42,7 @@ private ByteArrayCodec() { } @Override - public byte[] decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public byte[] decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return EMPTY_BYTES; @@ -62,7 +62,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isBinary(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java new file mode 100644 index 000000000..c31261f5a --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java @@ -0,0 +1,150 @@ +/* + * Copyright 2025 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.codec; + +import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; +import io.asyncer.r2dbc.mysql.constant.MySqlType; +import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_BYTES; + +/** + * Codec for {@link InputStream}. + */ +final class ByteArrayInputStreamCodec extends AbstractClassedCodec { + + static final ByteArrayInputStreamCodec INSTANCE = new ByteArrayInputStreamCodec(); + + private ByteArrayInputStreamCodec() { + super(ByteArrayInputStream.class); + } + + @Override + public ByteArrayInputStream decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, + CodecContext context) { + if (!value.isReadable()) { + return new ByteArrayInputStream(EMPTY_BYTES); + } + return new ByteArrayInputStream(value.array()); + } + + @Override + protected boolean doCanDecode(MySqlReadableMetadata metadata) { + return metadata.getType().isBinary(); + } + + @Override + public boolean canEncode(Object value) { + return value instanceof ByteArrayInputStream; + } + + @Override + public MySqlParameter encode(Object value, CodecContext context) { + return new ByteArrayInputStreamMysqlParameter((ByteArrayInputStream) value); + } + + private static final class ByteArrayInputStreamMysqlParameter extends AbstractMySqlParameter { + + private final ByteArrayInputStream value; + + private ByteArrayInputStreamMysqlParameter(ByteArrayInputStream value) { + this.value = value; + } + + @Override + public Mono publishBinary(ByteBufAllocator allocator) { + return Mono.fromSupplier(() -> { + int size = value.available(); + if (size == 0) { + return allocator.buffer(Byte.BYTES).writeByte(0); + } + + int addedSize = VarIntUtils.varIntBytes(size); + ByteBuf buf = allocator.buffer(addedSize + size); + + try { + VarIntUtils.writeVarInt(buf, size); + int readBytes = buf.writeBytes(value, size); + if (readBytes != size) { + buf.release(); + throw new IllegalStateException("Expected to read " + size + " bytes, but got " + readBytes); + } + + return buf; + } catch (Exception e) { + buf.release(); + throw new RuntimeException(e); + } + }); + } + + @Override + public Mono publishText(ParameterWriter writer) { + return Mono.fromRunnable(() -> { + try { + int size = value.available(); + byte[] byteArray = new byte[size]; + int readBytes = value.read(byteArray); + + if (size != 0 && readBytes != size) { + throw new IllegalStateException("Expected to read " + size + " bytes, but got " + readBytes); + } + + writer.writeHex(byteArray); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ByteArrayInputStreamMysqlParameter)) { + return false; + } + + ByteArrayInputStreamMysqlParameter that = (ByteArrayInputStreamMysqlParameter) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public MySqlType getType() { + return MySqlType.VARBINARY; + } + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java similarity index 94% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java index 0720e6c81..598551d29 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -41,7 +41,7 @@ private ByteBufferCodec() { } @Override - public ByteBuffer decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public ByteBuffer decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return ByteBuffer.wrap(EMPTY_BYTES); @@ -66,7 +66,7 @@ public boolean canEncode(Object value) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isBinary(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java index c8257cf3d..e21c029c8 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -36,7 +36,7 @@ private ByteCodec() { } @Override - public Byte decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Byte decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return (byte) IntegerCodec.decodeInt(value, binary, metadata.getType()); } @@ -52,7 +52,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java index ba204c348..84d990a39 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.lob.LobUtils; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -53,19 +53,24 @@ private ClobCodec() { } @Override - public Clob decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Class getMainClass() { + return Clob.class; + } + + @Override + public Clob decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createClob(value, metadata.getCharCollation(context)); } @Override - public Clob decodeMassive(List value, MySqlColumnMetadata metadata, Class target, + public Clob decodeMassive(List value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createClob(value, metadata.getCharCollation(context)); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { MySqlType type = metadata.getType(); return (type.isLob() || type == MySqlType.JSON) && target.isAssignableFrom(Clob.class); @@ -81,7 +86,7 @@ public MySqlParameter encode(Object value, CodecContext context) { return new ClobMySqlParameter((Clob) value, context); } - private static class ClobMySqlParameter extends AbstractLobMySqlParameter { + private static final class ClobMySqlParameter extends AbstractLobMySqlParameter { private final AtomicReference clob; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java similarity index 56% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java index daaf6b64f..744652c74 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java @@ -16,48 +16,47 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; /** * Codec to encode and decode values based on MySQL data binary/text protocol. *

- * Use {@link ParametrizedCodec} for support {@code ParameterizedType} encoding/decoding. + * Use {@link ParameterizedCodec} for support {@code ParameterizedType} encoding/decoding. * * @param the type that is handled by this codec. */ public interface Codec { /** - * Decode a {@link ByteBuf} as specified {@link Class}. + * Decodes a {@link ByteBuf} as specified {@link Class}. * * @param value the {@link ByteBuf}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link Class}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - T decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, - CodecContext context); + T decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context); /** - * Check if can decode the field value as specified {@link Class}. + * Checks if the field value can be decoded as specified {@link Class}. * - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link Class}. - * @return if can decode. + * @return if it can decode. */ - boolean canDecode(MySqlColumnMetadata metadata, Class target); + boolean canDecode(MySqlReadableMetadata metadata, Class target); /** - * Check if can encode the specified value. + * Checks if it can encode the specified value. * * @param value the specified value. - * @return if can encode. + * @return if it can encode. */ boolean canEncode(Object value); @@ -69,4 +68,16 @@ T decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean b * @return encoded {@link MySqlParameter}. */ MySqlParameter encode(Object value, CodecContext context); + + /** + * Gets the main {@link Class} that is handled by this codec. It is used to fast path the codec lookup if it is not + * {@code null}. If same main {@link Class} is handled by multiple codecs, the codec with the highest priority will + * be used. The priority of the fast path is determined by its order in {@link Codecs}. + * + * @return the main {@link Class}, or {@code null} if it is not in fast path. + */ + @Nullable + default Class getMainClass() { + return null; + } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java similarity index 69% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java index c674f3b16..5b58fa5f6 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java @@ -28,28 +28,36 @@ public interface CodecContext { /** - * Get the {@link ZoneId} of server-side. + * Checks if the connection is set to preserve instants, i.e. convert instant values to connection time + * zone. + * + * @return if preserve instants. + */ + boolean isPreserveInstants(); + + /** + * Gets the {@link ZoneId} of connection. * * @return the {@link ZoneId}. */ - ZoneId getServerZoneId(); + ZoneId getTimeZone(); /** - * Get the option for zero date handling which is set by connection configuration. + * Gets the option for zero date handling which is set by connection configuration. * * @return the {@link ZeroDateOption}. */ ZeroDateOption getZeroDateOption(); /** - * Get the MySQL server version, which is available after database user logon. + * Gets the MySQL server version, which is available after database user logon. * * @return the {@link ServerVersion}. */ ServerVersion getServerVersion(); /** - * Get the {@link CharCollation} that the client is using. + * Gets the {@link CharCollation} that the client is using. * * @return the {@link CharCollation}. */ @@ -61,4 +69,10 @@ public interface CodecContext { * @return if is MariaDB. */ boolean isMariaDb(); + + /** + * + * @return true if tinyInt(1) is treated as bit. + */ + boolean isTinyInt1isBit(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java similarity index 86% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java index aff4799b0..d9ac71e99 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java @@ -16,8 +16,8 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.netty.buffer.ByteBufAllocator; import org.jetbrains.annotations.Nullable; @@ -33,7 +33,7 @@ public interface Codecs { * Decode a {@link FieldValue} as specified {@link Class type}. * * @param value the {@link FieldValue}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param type the specified {@link Class}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. @@ -42,14 +42,14 @@ public interface Codecs { * @throws IllegalArgumentException if any parameter is {@code null}, or {@code value} cannot be decoded. */ @Nullable - T decode(FieldValue value, MySqlColumnMetadata metadata, Class type, boolean binary, + T decode(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context); /** * Decode a {@link FieldValue} as a specified {@link ParameterizedType type}. * * @param value the {@link FieldValue}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param type the specified {@link ParameterizedType}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. @@ -58,7 +58,7 @@ T decode(FieldValue value, MySqlColumnMetadata metadata, Class type, bool * @throws IllegalArgumentException if any parameter is {@code null}, or {@code value} cannot be decoded. */ @Nullable - T decode(FieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, boolean binary, + T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context); /** @@ -93,10 +93,9 @@ T decode(FieldValue value, MySqlColumnMetadata metadata, ParameterizedType t /** * Create a builder from a {@link ByteBufAllocator}. * - * @param allocator the {@link ByteBufAllocator}. * @return a {@link CodecsBuilder}. */ - static CodecsBuilder builder(ByteBufAllocator allocator) { - return new DefaultCodecs.Builder(allocator); + static CodecsBuilder builder() { + return new DefaultCodecs.Builder(); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java similarity index 98% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java index 480eafa06..3d4a23e43 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java @@ -27,7 +27,7 @@ import java.time.temporal.Temporal; /** - * An utility considers date/time generic logic for {@link Codec} implementations. + * A utility considers date/time generic logic for {@link Codec} implementations. */ final class DateTimes { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java similarity index 54% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index c8049308a..01e47348c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -16,20 +16,22 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; +import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.asyncer.r2dbc.mysql.message.LargeFieldValue; import io.asyncer.r2dbc.mysql.message.NormalFieldValue; -import io.netty.buffer.ByteBufAllocator; +import io.r2dbc.spi.Blob; +import io.r2dbc.spi.Clob; import io.r2dbc.spi.Parameter; import org.jetbrains.annotations.Nullable; import javax.annotation.concurrent.GuardedBy; import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -43,54 +45,102 @@ */ final class DefaultCodecs implements Codecs { - private final Codec[] codecs; + private static final List> DEFAULT_CODECS = InternalArrays.asImmutableList( + ByteCodec.INSTANCE, + ShortCodec.INSTANCE, + IntegerCodec.INSTANCE, + LongCodec.INSTANCE, + BigIntegerCodec.INSTANCE, - private final ParametrizedCodec[] parametrizedCodecs; + BigDecimalCodec.INSTANCE, // Only all decimals + FloatCodec.INSTANCE, // Decimal (precision < 7) or float + DoubleCodec.INSTANCE, // Decimal (precision < 16) or double or float + + BooleanCodec.INSTANCE, + BitSetCodec.INSTANCE, + + ZonedDateTimeCodec.INSTANCE, + LocalDateTimeCodec.INSTANCE, + InstantCodec.INSTANCE, + OffsetDateTimeCodec.INSTANCE, + + LocalDateCodec.INSTANCE, + + LocalTimeCodec.INSTANCE, + DurationCodec.INSTANCE, + OffsetTimeCodec.INSTANCE, + + YearCodec.INSTANCE, + + StringCodec.INSTANCE, + + EnumCodec.INSTANCE, + SetCodec.INSTANCE, + + ClobCodec.INSTANCE, + BlobCodec.INSTANCE, + + ByteBufferCodec.INSTANCE, + ByteArrayCodec.INSTANCE, + ByteArrayInputStreamCodec.INSTANCE + ); + + private final List> codecs; + + private final ParameterizedCodec[] parameterizedCodecs; private final MassiveCodec[] massiveCodecs; - private final MassiveParametrizedCodec[] massiveParametrizedCodecs; + private final MassiveParameterizedCodec[] massiveParameterizedCodecs; - private final Map> primitiveCodecs; + private final Map, Codec> fastPath; - private DefaultCodecs(Codec[] codecs) { - this.codecs = requireNonNull(codecs, "codecs must not be null"); + private DefaultCodecs(List> codecs) { + requireNonNull(codecs, "codecs must not be null"); - Map> primitiveCodecs = new HashMap<>(); - List> parametrizedCodecs = new ArrayList<>(); + Map, Codec> fastPath = new HashMap<>(); + List> parameterizedCodecs = new ArrayList<>(); List> massiveCodecs = new ArrayList<>(); - List> massiveParamCodecs = new ArrayList<>(); + List> massiveParamCodecs = new ArrayList<>(); for (Codec codec : codecs) { - if (codec instanceof PrimitiveCodec) { - // Primitive codec must be class-based codec, cannot support ParameterizedType. - PrimitiveCodec c = (PrimitiveCodec) codec; - primitiveCodecs.put(c.getPrimitiveClass(), c); - } else if (codec instanceof ParametrizedCodec) { - parametrizedCodecs.add((ParametrizedCodec) codec); + Class mainClass = codec.getMainClass(); + + if (mainClass != null) { + fastPath.putIfAbsent(mainClass, codec); + } + + if (codec instanceof AbstractPrimitiveCodec) { + AbstractPrimitiveCodec c = (AbstractPrimitiveCodec) codec; + + fastPath.putIfAbsent(c.getPrimitiveClass(), c); + } else if (codec instanceof ParameterizedCodec) { + parameterizedCodecs.add((ParameterizedCodec) codec); } if (codec instanceof MassiveCodec) { massiveCodecs.add((MassiveCodec) codec); - if (codec instanceof MassiveParametrizedCodec) { - massiveParamCodecs.add((MassiveParametrizedCodec) codec); + if (codec instanceof MassiveParameterizedCodec) { + massiveParamCodecs.add((MassiveParameterizedCodec) codec); } } } - this.primitiveCodecs = primitiveCodecs; + this.fastPath = fastPath; + this.codecs = codecs; this.massiveCodecs = massiveCodecs.toArray(new MassiveCodec[0]); - this.massiveParametrizedCodecs = massiveParamCodecs.toArray(new MassiveParametrizedCodec[0]); - this.parametrizedCodecs = parametrizedCodecs.toArray(new ParametrizedCodec[0]); + this.massiveParameterizedCodecs = massiveParamCodecs.toArray(new MassiveParameterizedCodec[0]); + this.parameterizedCodecs = parameterizedCodecs.toArray(new ParameterizedCodec[0]); } /** - * Note: this method should NEVER release {@code buf} because of it come from {@code MySqlRow} which will - * release this buffer. + * Note: this method should NEVER release {@code buf} because of it come from {@code MySqlRow} which will release + * this buffer. */ + @Nullable @Override - public T decode(FieldValue value, MySqlColumnMetadata metadata, Class type, boolean binary, + public T decode(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { requireNonNull(value, "value must not be null"); requireNonNull(metadata, "info must not be null"); @@ -103,12 +153,9 @@ public T decode(FieldValue value, MySqlColumnMetadata metadata, Class typ return null; } - Class target = chooseClass(metadata, type); + Class target = chooseClass(metadata, type, context); - // Fast map for primitive classes. - if (target.isPrimitive()) { - return decodePrimitive(value, metadata, target, binary, context); - } else if (value instanceof NormalFieldValue) { + if (value instanceof NormalFieldValue) { return decodeNormal((NormalFieldValue) value, metadata, target, binary, context); } else if (value instanceof LargeFieldValue) { return decodeMassive((LargeFieldValue) value, metadata, target, binary, context); @@ -117,8 +164,9 @@ public T decode(FieldValue value, MySqlColumnMetadata metadata, Class typ throw new IllegalArgumentException("Unknown value " + value.getClass().getSimpleName()); } + @Nullable @Override - public T decode(FieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, + public T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { requireNonNull(value, "value must not be null"); requireNonNull(metadata, "info must not be null"); @@ -172,13 +220,20 @@ public MySqlParameter encode(Object value, CodecContext context) { requireNonNull(value, "value must not be null"); requireNonNull(context, "context must not be null"); - final Object valueToEncode = getValueToEncode(value); + Object valueToEncode = getValueToEncode(value); + if (null == valueToEncode) { return encodeNull(); } + Codec fast = encodeFast(valueToEncode); + + if (fast != null && fast.canEncode(valueToEncode)) { + return fast.encode(valueToEncode, context); + } + for (Codec codec : codecs) { - if (codec.canEncode(valueToEncode)) { + if (codec != fast && codec.canEncode(valueToEncode)) { return codec.encode(valueToEncode, context); } } @@ -200,25 +255,48 @@ public MySqlParameter encodeNull() { } @Nullable - private T decodePrimitive(FieldValue value, MySqlColumnMetadata metadata, Class type, - boolean binary, CodecContext context) { - @SuppressWarnings("unchecked") - PrimitiveCodec codec = (PrimitiveCodec) this.primitiveCodecs.get(type); + @SuppressWarnings("unchecked") + private Codec decodeFast(Class type) { + Codec codec = (Codec) fastPath.get(type); + + if (codec == null && type.isEnum()) { + return (Codec) fastPath.get(Enum.class); + } + + return codec; + } - if (codec != null && value instanceof NormalFieldValue && codec.canPrimitiveDecode(metadata)) { - return codec.decode(((NormalFieldValue) value).getBufferSlice(), metadata, type, binary, context); + @Nullable + @SuppressWarnings("unchecked") + private Codec encodeFast(Object value) { + Codec codec = (Codec) fastPath.get(value.getClass()); + + if (codec == null) { + if (value instanceof ByteBuffer) { + return (Codec) fastPath.get(ByteBuffer.class); + } else if (value instanceof Blob) { + return (Codec) fastPath.get(Blob.class); + } else if (value instanceof Clob) { + return (Codec) fastPath.get(Clob.class); + } else if (value instanceof Enum) { + return (Codec) fastPath.get(Enum.class); + } } - // Mismatch, no one else can support this primitive class. - throw new IllegalArgumentException("Cannot decode " + value.getClass().getSimpleName() + " of " + - type + " for " + metadata.getType()); + return codec; } @Nullable - private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, Class type, + private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { + Codec fast = decodeFast(type); + + if (fast != null && fast.canDecode(metadata, type)) { + return fast.decode(value.getBufferSlice(), metadata, type, binary, context); + } + for (Codec codec : codecs) { - if (codec.canDecode(metadata, type)) { + if (codec != fast && codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") Codec c = (Codec) codec; return c.decode(value.getBufferSlice(), metadata, type, binary, context); @@ -229,9 +307,9 @@ private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, } @Nullable - private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, + private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { - for (ParametrizedCodec codec : parametrizedCodecs) { + for (ParameterizedCodec codec : parameterizedCodecs) { if (codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") T result = (T) codec.decode(value.getBufferSlice(), metadata, type, binary, context); @@ -243,10 +321,16 @@ private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, } @Nullable - private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, Class type, + private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { + Codec fast = decodeFast(type); + + if (fast instanceof MassiveCodec && fast.canDecode(metadata, type)) { + return ((MassiveCodec) fast).decodeMassive(value.getBufferSlices(), metadata, type, binary, context); + } + for (MassiveCodec codec : massiveCodecs) { - if (codec.canDecode(metadata, type)) { + if (codec != fast && codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") MassiveCodec c = (MassiveCodec) codec; return c.decodeMassive(value.getBufferSlices(), metadata, type, binary, context); @@ -257,9 +341,9 @@ private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, } @Nullable - private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, + private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { - for (MassiveParametrizedCodec codec : massiveParametrizedCodecs) { + for (MassiveParameterizedCodec codec : massiveParameterizedCodecs) { if (codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") T result = (T) codec.decodeMassive(value.getBufferSlices(), metadata, type, binary, context); @@ -270,76 +354,58 @@ private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, throw new IllegalArgumentException("Cannot decode massive " + type + " for " + metadata.getType()); } - private static Class chooseClass(MySqlColumnMetadata metadata, Class type) { - Class javaType = metadata.getType().getJavaType(); + /** + * Chooses the {@link Class} to use for decoding. It helps to find {@link Codec} on the fast path. e.g. + * {@link Object} -> {@link String} for {@code TEXT}, {@link Number} -> {@link Integer} for {@code INT}, etc. + * + * @param metadata the metadata of the column or the {@code OUT} parameter. + * @param type the {@link Class} specified by the user. + * @return the {@link Class} to use for decoding. + */ + private static Class chooseClass(final MySqlReadableMetadata metadata, Class type, + final CodecContext codecContext) { + final Class javaType = getDefaultJavaType(metadata, codecContext); return type.isAssignableFrom(javaType) ? javaType : type; } - private static Codec[] defaultCodecs(ByteBufAllocator allocator) { - return new Codec[] { - ByteCodec.INSTANCE, - ShortCodec.INSTANCE, - IntegerCodec.INSTANCE, - LongCodec.INSTANCE, - BigIntegerCodec.INSTANCE, - BigDecimalCodec.INSTANCE, // Only all decimals - FloatCodec.INSTANCE, // Decimal (precision < 7) or float - DoubleCodec.INSTANCE, // Decimal (precision < 16) or double or float - - BooleanCodec.INSTANCE, - BitSetCodec.INSTANCE, - - ZonedDateTimeCodec.INSTANCE, - LocalDateTimeCodec.INSTANCE, - InstantCodec.INSTANCE, - OffsetDateTimeCodec.INSTANCE, - - LocalDateCodec.INSTANCE, - - LocalTimeCodec.INSTANCE, - DurationCodec.INSTANCE, - OffsetTimeCodec.INSTANCE, - - YearCodec.INSTANCE, - - StringCodec.INSTANCE, + private static boolean shouldBeTreatedAsBoolean(final @Nullable Integer precision, final MySqlType type, + final CodecContext context) { + if (precision == null || precision != 1) { + return false; + } + // ref: https://github.com/asyncer-io/r2dbc-mysql/issues/277 + // BIT(1) should be treated as Boolean by default. + return type == MySqlType.BIT || type == MySqlType.TINYINT && context.isTinyInt1isBit(); + } - EnumCodec.INSTANCE, - SetCodec.INSTANCE, + private static Class getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) { + final MySqlType type = metadata.getType(); + final Integer precision = metadata.getPrecision(); - ClobCodec.INSTANCE, - BlobCodec.INSTANCE, + if (shouldBeTreatedAsBoolean(precision, type, codecContext)) { + return Boolean.class; + } - ByteBufferCodec.INSTANCE, - ByteArrayCodec.INSTANCE - }; + return type.getJavaType(); } static final class Builder implements CodecsBuilder { - private final ByteBufAllocator allocator; - @GuardedBy("lock") private final ArrayList> codecs = new ArrayList<>(); private final ReentrantLock lock = new ReentrantLock(); - Builder(ByteBufAllocator allocator) { - this.allocator = allocator; - } - @Override public CodecsBuilder addFirst(Codec codec) { lock.lock(); try { if (codecs.isEmpty()) { - Codec[] defaultCodecs = defaultCodecs(allocator); - - codecs.ensureCapacity(defaultCodecs.length + 1); + codecs.ensureCapacity(DEFAULT_CODECS.size() + 1); // Add first. codecs.add(codec); - codecs.addAll(InternalArrays.asImmutableList(defaultCodecs)); + codecs.addAll(DEFAULT_CODECS); } else { codecs.add(0, codec); } @@ -354,7 +420,7 @@ public CodecsBuilder addLast(Codec codec) { lock.lock(); try { if (codecs.isEmpty()) { - codecs.addAll(InternalArrays.asImmutableList(defaultCodecs(allocator))); + codecs.addAll(DEFAULT_CODECS); } codecs.add(codec); } finally { @@ -369,9 +435,9 @@ public Codecs build() { try { try { if (codecs.isEmpty()) { - return new DefaultCodecs(defaultCodecs(allocator)); + return new DefaultCodecs(DEFAULT_CODECS); } - return new DefaultCodecs(codecs.toArray(new Codec[0])); + return new DefaultCodecs(InternalArrays.asImmutableList(codecs.toArray(new Codec[0]))); } finally { codecs.clear(); codecs.trimToSize(); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java index 728838787..986666ab8 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -38,7 +38,7 @@ private DoubleCodec() { } @Override - public Double decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Double decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -68,7 +68,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java similarity index 97% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java index e4ae98b87..1c83d06b6 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -48,7 +48,7 @@ private DurationCodec() { } @Override - public Duration decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Duration decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return binary ? decodeBinary(value) : decodeText(value); } @@ -64,7 +64,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.TIME; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java similarity index 87% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java index 9a2a0ce78..b1ebf638e 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -29,16 +29,22 @@ /** * Codec for {@code enum class}. */ -final class EnumCodec implements Codec> { +@SuppressWarnings("rawtypes") +final class EnumCodec implements Codec { static final EnumCodec INSTANCE = new EnumCodec(); private EnumCodec() { } + @Override + public Class getMainClass() { + return Enum.class; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override - public Enum decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Enum decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { Charset charset = metadata.getCharCollation(context).getCharset(); @@ -46,7 +52,7 @@ public Enum decode(ByteBuf value, MySqlColumnMetadata metadata, Class targ } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return metadata.getType() == MySqlType.ENUM && target.isEnum(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java index 581b289f8..4e45c00e3 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -38,7 +38,7 @@ private FloatCodec() { } @Override - public Float decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Float decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -68,7 +68,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java similarity index 80% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java index 23147bbbb..78894a24d 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -26,6 +26,8 @@ import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; /** * Codec for {@link Instant}. @@ -38,7 +40,12 @@ private InstantCodec() { } @Override - public Instant decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Class getMainClass() { + return Instant.class; + } + + @Override + public Instant decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { LocalDateTime origin = LocalDateTimeCodec.decodeOrigin(value, binary, context); @@ -46,7 +53,10 @@ public Instant decode(ByteBuf value, MySqlColumnMetadata metadata, Class targ return null; } - return origin.toInstant(context.getServerZoneId().getRules().getOffset(origin)); + ZoneId zone = context.isPreserveInstants() ? context.getTimeZone() : ZoneOffset.systemDefault(); + + return origin.toInstant(zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() + .getOffset(origin)); } @Override @@ -60,7 +70,7 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, Instant.class); } @@ -108,7 +118,8 @@ public int hashCode() { } private LocalDateTime serverValue() { - return LocalDateTime.ofInstant(value, context.getServerZoneId()); + return LocalDateTime.ofInstant(value, context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault()); } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java index 7e67e2a6f..3ec04aa59 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.codec.ShortCodec.ShortMySqlParameter; import io.asyncer.r2dbc.mysql.constant.MySqlType; @@ -41,7 +41,7 @@ private IntegerCodec() { } @Override - public Integer decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Integer decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decodeInt(value, binary, metadata.getType()); } @@ -65,7 +65,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java index 8f72fcb92..a3f9fbbd3 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -41,7 +41,7 @@ final class LocalDateCodec extends AbstractClassedCodec { } @Override - public LocalDate decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public LocalDate decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { int bytes = value.readableBytes(); LocalDate date = binary ? readDateBinary(value, bytes) : readDateText(value); @@ -64,7 +64,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.DATE; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java similarity index 91% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java index a0d553d67..a9567cae6 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -36,7 +36,7 @@ *

* For now, supports only A.D. calendar in {@link ChronoLocalDateTime}. */ -final class LocalDateTimeCodec implements ParametrizedCodec { +final class LocalDateTimeCodec implements ParameterizedCodec { static final LocalDateTimeCodec INSTANCE = new LocalDateTimeCodec(); @@ -46,13 +46,18 @@ private LocalDateTimeCodec() { } @Override - public LocalDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Class getMainClass() { + return LocalDateTime.class; + } + + @Override + public LocalDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decodeOrigin(value, binary, context); } @Override - public ChronoLocalDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, + public ChronoLocalDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { return decodeOrigin(value, binary, context); } @@ -68,12 +73,12 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { return DateTimes.canDecodeChronology(metadata.getType(), target, ChronoLocalDateTime.class); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, LocalDateTime.class); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java similarity index 97% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java index 101f88af6..c0102709b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -55,7 +55,7 @@ private LocalTimeCodec() { } @Override - public LocalTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public LocalTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decodeOrigin(binary, value); } @@ -71,7 +71,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.TIME; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java index 862a8f2ce..d57c57c8f 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.codec.IntegerCodec.IntMySqlParameter; import io.asyncer.r2dbc.mysql.codec.ShortCodec.ShortMySqlParameter; @@ -42,7 +42,7 @@ private LongCodec() { } @Override - public Long decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Long decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -73,7 +73,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java similarity index 84% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java index 18bdb80a2..65cbd2222 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; @@ -33,13 +33,13 @@ public interface MassiveCodec extends Codec { * Decode a massive value as specified {@link Class}. * * @param value {@link ByteBuf}s list. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link Class}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - T decodeMassive(List value, MySqlColumnMetadata metadata, Class target, boolean binary, + T decodeMassive(List value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParameterizedCodec.java similarity index 77% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParameterizedCodec.java index 290a4b815..7c501bda0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParameterizedCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; @@ -28,20 +28,19 @@ * * @param the type that is handled by this codec. */ -public interface MassiveParametrizedCodec extends ParametrizedCodec, MassiveCodec { +public interface MassiveParameterizedCodec extends ParameterizedCodec, MassiveCodec { /** * Decode a massive value as specified {@link ParameterizedType}. * * @param value {@link ByteBuf}s list. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link ParameterizedType}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - Object decodeMassive(List value, MySqlColumnMetadata metadata, ParameterizedType target, - boolean binary, - CodecContext context); + Object decodeMassive(List value, MySqlReadableMetadata metadata, ParameterizedType target, + boolean binary, CodecContext context); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java similarity index 86% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java index d1681ae31..df1ffedf1 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -40,7 +40,12 @@ private OffsetDateTimeCodec() { } @Override - public OffsetDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Class getMainClass() { + return OffsetDateTime.class; + } + + @Override + public OffsetDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { LocalDateTime origin = LocalDateTimeCodec.decodeOrigin(value, binary, context); @@ -48,7 +53,7 @@ public OffsetDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class< return null; } - ZoneId zone = context.getServerZoneId(); + ZoneId zone = context.isPreserveInstants() ? context.getTimeZone() : ZoneId.systemDefault(); return OffsetDateTime.of(origin, zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() .getOffset(origin)); @@ -65,7 +70,7 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, OffsetDateTime.class); } @@ -113,7 +118,9 @@ public int hashCode() { } private LocalDateTime serverValue() { - ZoneId zone = context.getServerZoneId(); + ZoneId zone = context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault().normalized(); + return zone instanceof ZoneOffset ? value.withOffsetSameInstant((ZoneOffset) zone).toLocalDateTime() : value.toZonedDateTime().withZoneSameInstant(zone).toLocalDateTime(); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java similarity index 82% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java index db1009c37..57fb77b17 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -42,10 +42,11 @@ private OffsetTimeCodec() { } @Override - public OffsetTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public OffsetTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { + // OffsetTime is not an instant value, so preserveInstants is not used here. LocalTime origin = LocalTimeCodec.decodeOrigin(binary, value); - ZoneId zone = context.getServerZoneId(); + ZoneId zone = ZoneId.systemDefault().normalized(); return OffsetTime.of(origin, zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() .getStandardOffset(Instant.EPOCH)); @@ -62,7 +63,7 @@ public boolean canEncode(Object value) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.TIME; } @@ -112,9 +113,14 @@ public int hashCode() { } private LocalTime serverValue() { - ZoneId zone = context.getServerZoneId(); - ZoneOffset offset = zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() - .getStandardOffset(Instant.EPOCH); + // OffsetTime is not an instant value, so preserveInstants is not used here. + ZoneId zone = ZoneId.systemDefault().normalized(); + + if (zone instanceof ZoneOffset) { + return value.withOffsetSameInstant((ZoneOffset) zone).toLocalTime(); + } + + ZoneOffset offset = zone.getRules().getStandardOffset(Instant.EPOCH); return value.toLocalTime() .plusSeconds(offset.getTotalSeconds() - value.getOffset().getTotalSeconds()); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParameterizedCodec.java similarity index 67% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParameterizedCodec.java index 966dfe189..3326b4742 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParameterizedCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; @@ -29,28 +29,28 @@ * * @param the type without parameter that is handled by this codec. */ -public interface ParametrizedCodec extends Codec { +public interface ParameterizedCodec extends Codec { /** - * Decode a {@link ByteBuf} as specified {@link ParameterizedType}. + * Decodes a {@link ByteBuf} as specified {@link ParameterizedType}. * * @param value the {@link ByteBuf}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link ParameterizedType}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - Object decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedType target, boolean binary, + Object decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context); /** - * Check if can decode the field value as specified {@link ParameterizedType}. + * Checks if the field value can be decoded as specified {@link ParameterizedType}. * - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link ParameterizedType}. - * @return if can decode. + * @return if it can decode. */ - boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target); + boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java index 67e8db371..1b88e9269 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -41,7 +41,7 @@ * Codec for {@link Set}{@code <}{@link String}{@code >}, {@link Set}{@code <}{@link Enum}{@code >} and the * {@link String}{@code []}. */ -final class SetCodec implements ParametrizedCodec { +final class SetCodec implements ParameterizedCodec { static final SetCodec INSTANCE = new SetCodec(); @@ -49,7 +49,12 @@ private SetCodec() { } @Override - public String[] decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Class getMainClass() { + return String[].class; + } + + @Override + public String[] decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return EMPTY_STRINGS; @@ -67,7 +72,7 @@ public String[] decode(ByteBuf value, MySqlColumnMetadata metadata, Class tar @SuppressWarnings({ "unchecked", "rawtypes" }) @Override - public Set decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedType target, boolean binary, + public Set decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { if (!value.isReadable()) { return Collections.emptySet(); @@ -105,12 +110,12 @@ public Set decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedT } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return metadata.getType() == MySqlType.SET && target.isAssignableFrom(String[].class); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { if (metadata.getType() != MySqlType.SET) { return false; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java index 952ce2de7..34bcaf305 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; @@ -37,7 +37,7 @@ private ShortCodec() { } @Override - public Short decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Short decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return (short) IntegerCodec.decodeInt(value, binary, metadata.getType()); } @@ -59,7 +59,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java index 134dc9159..2a4bfe5a7 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -39,7 +39,7 @@ private StringCodec() { } @Override - public String decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public String decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return ""; @@ -59,7 +59,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isString(); } @@ -84,7 +84,7 @@ static ByteBuf encodeCharSequence(ByteBufAllocator allocator, CharSequence value } } - private static class StringMySqlParameter extends AbstractMySqlParameter { + private static final class StringMySqlParameter extends AbstractMySqlParameter { private final CharSequence value; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java similarity index 90% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java index 90a54c1bb..fa7f8b9c0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java @@ -16,8 +16,8 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.codec.IntegerCodec.IntMySqlParameter; import io.asyncer.r2dbc.mysql.codec.ShortCodec.ShortMySqlParameter; @@ -40,7 +40,7 @@ private YearCodec() { } @Override - public Year decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Year decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return binary ? Year.of(value.readShortLE()) : Year.of(CodecUtils.parseInt(value)); } @@ -66,7 +66,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.YEAR; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java similarity index 80% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java index dcaee0cc7..f16fcc274 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -28,6 +28,7 @@ import java.lang.reflect.ParameterizedType; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.chrono.ChronoZonedDateTime; @@ -36,7 +37,7 @@ *

* For now, supports only A.D. calendar in {@link ChronoZonedDateTime}. */ -final class ZonedDateTimeCodec implements ParametrizedCodec { +final class ZonedDateTimeCodec implements ParameterizedCodec { static final ZonedDateTimeCodec INSTANCE = new ZonedDateTimeCodec(); @@ -44,13 +45,18 @@ private ZonedDateTimeCodec() { } @Override - public ZonedDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Class getMainClass() { + return ZonedDateTime.class; + } + + @Override + public ZonedDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decode0(value, binary, context); } @Override - public ChronoZonedDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, + public ChronoZonedDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { return decode0(value, binary, context); } @@ -66,19 +72,25 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { return DateTimes.canDecodeChronology(metadata.getType(), target, ChronoZonedDateTime.class); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, ZonedDateTime.class); } @Nullable private static ZonedDateTime decode0(ByteBuf value, boolean binary, CodecContext context) { LocalDateTime origin = LocalDateTimeCodec.decodeOrigin(value, binary, context); - return origin == null ? null : ZonedDateTime.of(origin, context.getServerZoneId()); + + if (origin == null) { + return null; + } + + return ZonedDateTime.of(origin, context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault()); } private static final class ZonedDateTimeMySqlParameter extends AbstractMySqlParameter { @@ -127,7 +139,10 @@ public int hashCode() { } private LocalDateTime serverValue() { - return value.withZoneSameInstant(context.getServerZoneId()) + ZoneId zoneId = context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault().normalized(); + + return value.withZoneSameInstant(zoneId) .toLocalDateTime(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java index b89762436..4e233c883 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.codec.lob; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java index 8b78cb24b..2a17502a0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java @@ -20,7 +20,7 @@ import java.nio.charset.UnsupportedCharsetException; /** - * MySQL character collation target of {@link Charset}. + * A character collation {@link Charset} target of MySQL. */ interface CharsetTarget { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java new file mode 100644 index 000000000..05945bf74 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.constant; + +/** + * The compression algorithm for client/server communication. + */ +public enum CompressionAlgorithm { + + /** + * Do not use compression protocol. + */ + UNCOMPRESSED, + + /** + * Use zlib compression algorithm for client/server communication. + *

+ * If zlib is not available, the connection will throw an exception when logging in. + */ + ZLIB, + + /** + * Use Z-Standard compression algorithm for client/server communication. + *

+ * If zstd is not available, the connection will throw an exception when logging in. + */ + ZSTD, +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java similarity index 91% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java index 8628966db..2485d897b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java @@ -16,13 +16,14 @@ package io.asyncer.r2dbc.mysql.constant; -import io.asyncer.r2dbc.mysql.ColumnDefinition; +import io.asyncer.r2dbc.mysql.api.MySqlNativeTypeMetadata; import io.r2dbc.spi.Type; import java.math.BigDecimal; import java.math.BigInteger; import java.nio.ByteBuffer; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZonedDateTime; @@ -146,7 +147,7 @@ public int getBinarySize() { }, /** - * A IEEE-754 single-precision floating point number type. It cannot be unsigned when the server version + * An IEEE-754 single-precision floating point number type. It cannot be unsigned when the server version * is 8.0 or higher. Otherwise, the server will report a warning when defining the column. */ FLOAT(MySqlType.ID_FLOAT, Float.class) { @@ -167,7 +168,7 @@ public int getBinarySize() { }, /** - * A IEEE-754 double-precision floating point number type. It cannot be unsigned when the server version + * An IEEE-754 double-precision floating point number type. It cannot be unsigned when the server version * is 8.0 or higher. Otherwise, the server will report a warning when defining the column. */ DOUBLE(MySqlType.ID_DOUBLE, Double.class) { @@ -271,7 +272,7 @@ public int getBinarySize() { /** * A date time type. It does not contain timezone. It uses string format to transfer the value. */ - DATETIME(MySqlType.ID_DATETIME, ZonedDateTime.class), + DATETIME(MySqlType.ID_DATETIME, LocalDateTime.class), /** * A year type. It contains neither leap year information nor timezone. @@ -332,7 +333,7 @@ public boolean isBinary() { }, /** - * A enumerable string type. It is a virtual type, server will enabled {@code ENUMERABLE} in column + * An enumerable string type. It is a virtual type, server will enabled {@code ENUMERABLE} in column * definitions and type is as {@link #VARCHAR}. */ ENUM(MySqlType.ID_ENUM, String.class) { @@ -661,7 +662,7 @@ public boolean isBinary() { } /** - * Get the fixed byte size of the data type in the binary protocol, otherwise {@literal 0} means that + * Gets the fixed byte size of the data type in the binary protocol, otherwise {@literal 0} means that * there is no fixed size. * * @return the fixed size in binary protocol. @@ -670,24 +671,24 @@ public int getBinarySize() { return 0; } - public static MySqlType of(int id, ColumnDefinition definition) { + public static MySqlType of(MySqlNativeTypeMetadata metadata) { // Maybe need to check if it is a string-like type? - if (definition.isSet()) { + if (metadata.isSet()) { return SET; - } else if (definition.isEnum()) { + } else if (metadata.isEnum()) { return ENUM; } - switch (id) { + switch (metadata.getTypeId()) { case ID_DECIMAL: case ID_NEW_DECIMAL: return DECIMAL; case ID_TINYINT: - return definition.isUnsigned() ? TINYINT_UNSIGNED : TINYINT; + return metadata.isUnsigned() ? TINYINT_UNSIGNED : TINYINT; case ID_SMALLINT: - return definition.isUnsigned() ? SMALLINT_UNSIGNED : SMALLINT; + return metadata.isUnsigned() ? SMALLINT_UNSIGNED : SMALLINT; case ID_INT: - return definition.isUnsigned() ? INT_UNSIGNED : INT; + return metadata.isUnsigned() ? INT_UNSIGNED : INT; case ID_FLOAT: return FLOAT; case ID_DOUBLE: @@ -697,9 +698,9 @@ public static MySqlType of(int id, ColumnDefinition definition) { case ID_TIMESTAMP: return TIMESTAMP; case ID_BIGINT: - return definition.isUnsigned() ? BIGINT_UNSIGNED : BIGINT; + return metadata.isUnsigned() ? BIGINT_UNSIGNED : BIGINT; case ID_MEDIUMINT: - return definition.isUnsigned() ? MEDIUMINT_UNSIGNED : MEDIUMINT; + return metadata.isUnsigned() ? MEDIUMINT_UNSIGNED : MEDIUMINT; case ID_DATE: return DATE; case ID_TIME: @@ -711,7 +712,7 @@ public static MySqlType of(int id, ColumnDefinition definition) { case ID_VARCHAR: case ID_VAR_STRING: case ID_STRING: - return definition.isBinary() ? VARBINARY : VARCHAR; + return metadata.isBinary() ? VARBINARY : VARCHAR; case ID_BIT: return BIT; case ID_JSON: @@ -721,18 +722,17 @@ public static MySqlType of(int id, ColumnDefinition definition) { case ID_SET: return SET; case ID_TINYBLOB: - return definition.isBinary() ? TINYBLOB : TINYTEXT; + return metadata.isBinary() ? TINYBLOB : TINYTEXT; case ID_MEDIUMBLOB: - return definition.isBinary() ? MEDIUMBLOB : MEDIUMTEXT; + return metadata.isBinary() ? MEDIUMBLOB : MEDIUMTEXT; case ID_LONGBLOB: - return definition.isBinary() ? LONGBLOB : LONGTEXT; + return metadata.isBinary() ? LONGBLOB : LONGTEXT; case ID_BLOB: - return definition.isBinary() ? BLOB : TEXT; + return metadata.isBinary() ? BLOB : TEXT; case ID_GEOMETRY: - // Most of Geometry libraries were using byte[] to encode/decode which based on WKT + // Most Geometry libraries were using byte[] to encode/decode which based on WKT // (includes Extended-WKT) or WKB // MySQL using WKB for encoding/decoding, so use byte[] instead of ByteBuffer by default type. - // It maybe change after R2DBC SPI specify default type for GEOMETRY. return GEOMETRY; default: return UNKNOWN; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/Envelopes.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java similarity index 57% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/Envelopes.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java index 0bf5db0fa..b36a9d77f 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/constant/Envelopes.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java @@ -17,11 +17,11 @@ package io.asyncer.r2dbc.mysql.constant; /** - * Constants for MySQL protocol envelopes (e.g. business layer packages). + * Constants for MySQL protocol packets. *

* WARNING: do NOT use it outer than {@literal r2dbc-mysql}. */ -public final class Envelopes { +public final class Packets { /** * The length of the byte size field, it is 3 bytes. @@ -29,19 +29,26 @@ public final class Envelopes { public static final int SIZE_FIELD_SIZE = 3; /** - * The byte size of header part. + * The max bytes size of payload, value is 16777215. (i.e. max value of int24, (2 ** 24) - 1) */ - public static final int PART_HEADER_SIZE = SIZE_FIELD_SIZE + 1; + public static final int MAX_PAYLOAD_SIZE = 0xFFFFFF; /** - * The max bytes size of each envelope, value is 16777215. (i.e. max value of int24, (2 ** 24) - 1) + * The header size of a compression frame, which includes entire frame size (unsigned int24), compression + * sequence id (unsigned int8) and compressed size (unsigned int24). */ - public static final int MAX_ENVELOPE_SIZE = (1 << (SIZE_FIELD_SIZE << 3)) - 1; + public static final int COMPRESS_HEADER_SIZE = SIZE_FIELD_SIZE + 1 + SIZE_FIELD_SIZE; + + /** + * The header size of a normal frame, which includes entire frame size (unsigned int24) and normal + * sequence id (unsigned int8). + */ + public static final int NORMAL_HEADER_SIZE = SIZE_FIELD_SIZE + 1; /** * The terminal of C-style string or C-style binary data. */ public static final byte TERMINAL = 0; - private Envelopes() { } + private Packets() { } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java similarity index 90% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java index 685dae9a7..f90c6c7da 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java @@ -50,12 +50,20 @@ public final class ServerStatuses { public static final short LAST_ROW_SENT = 128; // public static final short DB_DROPPED = 256; -// public static final short NO_BACKSLASH_ESCAPES = 512; + + /** + * Server does not permit backslash escapes. + * + * @since 1.1.3 + */ + public static final short NO_BACKSLASH_ESCAPES = 512; + // public static final short METADATA_CHANGED = 1024; // public static final short QUERY_WAS_SLOW = 2048; // public static final short PS_OUT_PARAMS = 4096; // public static final short IN_TRANS_READONLY = 8192; // public static final short SESSION_STATE_CHANGED = 16384; - private ServerStatuses() { } + private ServerStatuses() { + } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java similarity index 96% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java index 144e8c1fc..6c403ec53 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java @@ -25,4 +25,4 @@ * discouraged, as it may lead to unpredictable behavior and compatibility issues. */ @NotNullByDefault -package io.asyncer.r2dbc.mysql.internal; \ No newline at end of file +package io.asyncer.r2dbc.mysql.internal; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java similarity index 89% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java index 47aa878ed..bfcb57a0b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.internal.util; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import org.jetbrains.annotations.Nullable; @@ -28,6 +28,8 @@ import reactor.core.publisher.Operators; import reactor.util.context.Context; +import java.util.concurrent.atomic.AtomicInteger; + /** * An implementation of {@link Flux}{@code <}{@link ByteBuf}{@code >} that considers cumulate buffers as * envelopes of the MySQL socket protocol. @@ -38,26 +40,26 @@ final class FluxEnvelope extends FluxOperator { private final int size; - private final int start; + private final AtomicInteger sequenceId; private final boolean cumulate; - FluxEnvelope(Flux source, ByteBufAllocator alloc, int size, int start, + FluxEnvelope(Flux source, ByteBufAllocator alloc, int size, AtomicInteger sequenceId, boolean cumulate) { super(source); this.alloc = alloc; this.size = size; - this.start = start; + this.sequenceId = sequenceId; this.cumulate = cumulate; } @Override public void subscribe(CoreSubscriber actual) { if (cumulate) { - this.source.subscribe(new CumulateEnvelopeSubscriber(actual, alloc, size, start)); + this.source.subscribe(new CumulateEnvelopeSubscriber(actual, alloc, size, sequenceId)); } else { - this.source.subscribe(new DirectEnvelopeSubscriber(actual, alloc, start)); + this.source.subscribe(new DirectEnvelopeSubscriber(actual, alloc, sequenceId)); } } } @@ -68,16 +70,17 @@ final class DirectEnvelopeSubscriber implements CoreSubscriber, Scannab private final ByteBufAllocator alloc; + private final AtomicInteger sequenceId; + private boolean done; private Subscription s; - private int envelopeId; - - DirectEnvelopeSubscriber(CoreSubscriber actual, ByteBufAllocator alloc, int start) { + DirectEnvelopeSubscriber(CoreSubscriber actual, ByteBufAllocator alloc, + AtomicInteger sequenceId) { this.actual = actual; this.alloc = alloc; - this.envelopeId = start; + this.sequenceId = sequenceId; } @Override @@ -97,9 +100,9 @@ public void onNext(ByteBuf buf) { } try { - ByteBuf header = this.alloc.buffer(Envelopes.PART_HEADER_SIZE) + ByteBuf header = this.alloc.ioBuffer(Packets.NORMAL_HEADER_SIZE) .writeMediumLE(buf.readableBytes()) - .writeByte(this.envelopeId++); + .writeByte(this.sequenceId.getAndIncrement()); this.actual.onNext(header); this.actual.onNext(buf); @@ -172,20 +175,20 @@ final class CumulateEnvelopeSubscriber implements CoreSubscriber, Scann private final int size; + private final AtomicInteger sequenceId; + private boolean done; private Subscription s; private ByteBuf cumulated; - private int envelopeId; - CumulateEnvelopeSubscriber(CoreSubscriber actual, ByteBufAllocator alloc, int size, - int start) { + AtomicInteger sequenceId) { this.actual = actual; this.alloc = alloc; this.size = size; - this.envelopeId = start; + this.sequenceId = sequenceId; } @Override @@ -217,9 +220,9 @@ public void onNext(ByteBuf buf) { while (cumulated.readableBytes() >= this.size) { // It will make the cumulated be shared (e.g. refCnt() > 1), that means // the reallocation of the cumulated may not be safe, see cumulate(...). - this.actual.onNext(this.alloc.buffer(Envelopes.PART_HEADER_SIZE) + this.actual.onNext(this.alloc.ioBuffer(Packets.NORMAL_HEADER_SIZE) .writeMediumLE(this.size) - .writeByte(this.envelopeId++)); + .writeByte(this.sequenceId.getAndIncrement())); this.actual.onNext(cumulated.readRetainedSlice(this.size)); } @@ -266,7 +269,7 @@ public void onComplete() { ByteBuf cumulated = this.cumulated; this.cumulated = null; - // The protocol need least one envelope, and the last must small than maximum size of envelopes. + // The protocol need least one envelope, and the last must smaller than maximum size of envelopes. // - If there has no previous envelope, then the cumulated is null, should produce an empty // envelope header. // - If previous envelope is a max-size envelope, then the cumulated is null, should produce an @@ -275,8 +278,8 @@ public void onComplete() { ByteBuf header = null; try { - header = this.alloc.buffer(Envelopes.PART_HEADER_SIZE); - header.writeMediumLE(size).writeByte(this.envelopeId++); + header = this.alloc.ioBuffer(Packets.NORMAL_HEADER_SIZE); + header.writeMediumLE(size).writeByte(this.sequenceId.getAndIncrement()); } catch (Throwable e) { if (cumulated != null) { cumulated.release(); @@ -356,15 +359,15 @@ private static ByteBuf cumulate(ByteBufAllocator alloc, @Nullable ByteBuf cumula int oldBytes = cumulated.readableBytes(); int bufBytes = buf.readableBytes(); int newBytes = oldBytes + bufBytes; - ByteBuf result = releasing = alloc.buffer(alloc.calculateNewCapacity(newBytes, - Integer.MAX_VALUE)); + int newCapacity = alloc.calculateNewCapacity(newBytes, Integer.MAX_VALUE); + ByteBuf result = releasing = alloc.ioBuffer(newCapacity); // Avoid to calling writeBytes(...) with redundancy check and stack depth comparison. result.setBytes(0, cumulated, cumulated.readerIndex(), oldBytes) .setBytes(oldBytes, buf, buf.readerIndex(), bufBytes) .writerIndex(newBytes); buf.readerIndex(buf.writerIndex()); - // Release the old cumulated If write succeed (return will be succeed). + // Release the old cumulated If write succeed. releasing = cumulated; return result; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java similarity index 87% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java index 299f82790..a0fdce06b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java @@ -16,12 +16,14 @@ package io.asyncer.r2dbc.mysql.internal.util; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import reactor.core.Fuseable; import reactor.core.publisher.Flux; +import java.util.concurrent.atomic.AtomicInteger; + import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -56,12 +58,12 @@ public static Flux discardOnCancel(Flux source) { } public static Flux envelope(Flux source, ByteBufAllocator allocator, - int envelopeIdStart, boolean cumulate) { + AtomicInteger sequenceId, boolean cumulate) { requireNonNull(source, "source must not be null"); requireNonNull(allocator, "allocator must not be null"); + requireNonNull(sequenceId, "sequenceId must not be null"); - return new FluxEnvelope(source, allocator, Envelopes.MAX_ENVELOPE_SIZE, - envelopeIdStart & 0xFF, cumulate); + return new FluxEnvelope(source, allocator, Packets.MAX_PAYLOAD_SIZE, sequenceId, cumulate); } private OperatorUtils() { } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java similarity index 98% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java index 1c72a49df..0d8e89294 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java @@ -80,7 +80,7 @@ private void tryRead() { } private void read() { - ByteBuf buf = this.allocator.buffer(this.bufferSize); + ByteBuf buf = this.allocator.ioBuffer(this.bufferSize); ByteBuffer byteBuffer = buf.nioBuffer(buf.writerIndex(), buf.writableBytes()); this.channel.read(byteBuffer, this.position.get(), buf, this); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java new file mode 100644 index 000000000..1a96e2d79 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java @@ -0,0 +1,152 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.internal.util; + +import java.time.Duration; +import java.time.ZoneId; +import java.time.ZoneOffset; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty; + +/** + * A utility for processing {@link String} and simple statements in MySQL/MariaDB. + */ +public final class StringUtils { + + private static final char QUOTE = '`'; + + private static final String ZONE_PREFIX_POSIX = "posix/"; + + private static final String ZONE_PREFIX_RIGHT = "right/"; + + private static final int ZONE_PREFIX_LENGTH = 6; + + /** + * Quotes identifier with backticks, it will escape backticks in the identifier. + * + * @param identifier the identifier + * @return quoted identifier + */ + public static String quoteIdentifier(String identifier) { + requireNonEmpty(identifier, "identifier must not be empty"); + + int index = identifier.indexOf(QUOTE); + + if (index == -1) { + return QUOTE + identifier + QUOTE; + } + + int len = identifier.length(); + StringBuilder builder = new StringBuilder(len + 10).append(QUOTE); + int fromIndex = 0; + + while (index != -1) { + builder.append(identifier, fromIndex, index) + .append(QUOTE) + .append(QUOTE); + fromIndex = index + 1; + index = identifier.indexOf(QUOTE, fromIndex); + } + + if (fromIndex < len) { + builder.append(identifier, fromIndex, len); + } + + return builder.append(QUOTE).toString(); + } + + /** + * Extends a SQL statement with {@code RETURNING} clause. + * + * @param sql the original SQL statement. + * @param returning quoted column identifiers. + * @return the SQL statement with {@code RETURNING} clause. + */ + public static String extendReturning(String sql, String returning) { + return returning.isEmpty() ? sql : sql + " RETURNING " + returning; + } + + /** + * Generates a {@link String} indicating the statement timeout variable. e.g. {@code "max_statement_time=1.5"} for + * MariaDB or {@code "max_execution_time=1500"} for MySQL. + * + * @param timeout the statement timeout + * @param isMariaDb whether the current server is MariaDB + * @return the statement timeout variable + */ + public static String statementTimeoutVariable(Duration timeout, boolean isMariaDb) { + // mariadb: https://mariadb.com/kb/en/aborting-statements/ + // mysql: https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/ + // ref: https://github.com/mariadb-corporation/mariadb-connector-r2dbc + if (isMariaDb) { + // MariaDB supports fractional seconds with microsecond precision + double seconds = (timeout.getSeconds() + timeout.getNano() / 1_000_000_000.0); + return "max_statement_time=" + seconds; + } + + return "max_execution_time=" + timeout.toMillis(); + } + + /** + * Generates a statement to set the lock wait timeout for the current session. It is using InnoDB-specific session + * variable {@code innodb_lock_wait_timeout}. + * + * @param timeout the lock wait timeout + * @return the lock wait timeout statement + */ + public static String lockWaitTimeoutStatement(Duration timeout) { + return "SET innodb_lock_wait_timeout=" + timeout.getSeconds(); + } + + /** + * Parses a normalized {@link ZoneId} from a time zone string of MySQL. + *

+ * Note: since java 14.0.2, 11.0.8, 8u261 and 7u271, America/Nuuk is already renamed from America/Godthab. See also + * tzdata2020a + * + * @param zoneId the time zone string + * @return the normalized {@link ZoneId} + * @throws IllegalArgumentException if the time zone string is {@code null} or empty + * @throws java.time.DateTimeException if the time zone string has an invalid format + * @throws java.time.zone.ZoneRulesException if the time zone string cannot be found + */ + public static ZoneId parseZoneId(String zoneId) { + requireNonEmpty(zoneId, "zoneId must not be empty"); + + String realId; + + if (zoneId.startsWith(ZONE_PREFIX_POSIX) || zoneId.startsWith(ZONE_PREFIX_RIGHT)) { + realId = zoneId.substring(ZONE_PREFIX_LENGTH); + } else { + realId = zoneId; + } + + switch (realId) { + case "Factory": + // It seems like UTC. + return ZoneOffset.UTC; + case "ROC": + // It is equal to +08:00. + return ZoneOffset.ofHours(8); + } + + return ZoneId.of(realId, ZoneId.SHORT_IDS).normalized(); + } + + private StringUtils() { + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java similarity index 99% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java index 76bcec9fb..ac6959c40 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java @@ -178,7 +178,7 @@ public static int varIntBytes(int value) { * Reserve a seat of an unknown var integer in {@code buf} header. *

* Note: make sure the var integer will be set into the {@code buf} header, can not use it when you want - * write a var integer into a {@code buf} which has data before the var integer. i.e. the {@code buf} + * to write a var integer into a {@code buf} which has data before the var integer. i.e. the {@code buf} * should be a new {@link ByteBuf}. * * @param buf that want reserve to this {@link ByteBuf}. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java similarity index 86% rename from src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java index 66b9b03d0..b98519375 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java @@ -22,12 +22,12 @@ * A sealed interface for field, it has 3-implementations: {@link NullFieldValue}, {@link NormalFieldValue} * and {@link LargeFieldValue}. *

- * WARNING: it is sealed interface, should NEVER extends or implemented by another interface or class. + * WARNING: it is sealed interface, should NEVER extend or implemented by another interface or class. */ public interface FieldValue extends ReferenceCounted { /** - * Check if value is {@code null}. + * Checks if value is {@code null}. * * @return if value is {@code null}. */ @@ -36,7 +36,7 @@ default boolean isNull() { } /** - * Get an instance for {@code null} value. + * Gets an instance for {@code null} value. * * @return a field contains a {@code null} value. */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java index f9d334f48..2459cf272 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java @@ -28,7 +28,7 @@ /** * An implementation of {@link FieldValue} considers large field value which bytes width/size is greater than - * {@link Integer#MAX_VALUE}, it would be exists when MySQL server return LOB types (i.e. BLOB, CLOB), + * {@link Integer#MAX_VALUE}, it is used by the MySQL server returns LOB types (i.e. BLOB, CLOB), e.g. * LONGTEXT length can be unsigned int32. * * @see FieldValue diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java similarity index 78% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java index 4c6d2b1de..3aa96cd70 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java @@ -28,20 +28,12 @@ */ public final class AuthResponse extends SizedClientMessage implements SubsequenceClientMessage { - private final int envelopeId; - private final byte[] authentication; - public AuthResponse(int envelopeId, byte[] authentication) { - this.envelopeId = envelopeId; + public AuthResponse(byte[] authentication) { this.authentication = requireNonNull(authentication, "authentication must not be null"); } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override protected int size() { return authentication.length; @@ -58,17 +50,17 @@ public boolean equals(Object o) { AuthResponse that = (AuthResponse) o; - return envelopeId == that.envelopeId && Arrays.equals(authentication, that.authentication); + return Arrays.equals(authentication, that.authentication); } @Override public int hashCode() { - return 31 * envelopeId + Arrays.hashCode(authentication); + return Arrays.hashCode(authentication); } @Override public String toString() { - return "AuthResponse{envelopeId=" + envelopeId + ", authentication=REDACTED}"; + return "AuthResponse{authentication=REDACTED}"; } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java similarity index 75% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java index 3080da66f..047884a17 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java @@ -27,6 +27,24 @@ */ public interface ClientMessage { + /** + * Returns whether the sequence should be reset before encoding this message. + * + * @return {@code true} if the sequence should be reset. + */ + default boolean isSequenceReset() { + return true; + } + + /** + * Returns whether the encoded buffers can be cumulated to maximize the payload size. + * + * @return {@code true} if can be cumulated. + */ + default boolean isCumulative() { + return true; + } + /** * Encode a message into {@link ByteBuf}s. * diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java similarity index 60% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java index ca05f9831..c7b135a34 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java @@ -22,7 +22,7 @@ import java.nio.charset.Charset; import java.util.Map; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; /** * An abstraction of {@link SubsequenceClientMessage} considers handshake response. @@ -33,24 +33,24 @@ public interface HandshakeResponse extends SubsequenceClientMessage { * Construct an instance of {@link HandshakeResponse}, it is implemented by the protocol version that is * given by {@link Capability}. * - * @param envelopeId the beginning envelope ID of this message. - * @param capability the current {@link Capability}. - * @param collationId the {@code CharCollation} ID, or 0 if server does not return a collation ID. - * @param user the username for login. - * @param authentication the password authentication for login. - * @param authType the authentication plugin type. - * @param database the connecting database, may be empty. - * @param attributes the connecting attributes. + * @param capability the current {@link Capability}. + * @param collationId the {@code CharCollation} ID, or 0 if server does not return. + * @param user the username for login. + * @param authentication the password authentication for login. + * @param authType the authentication plugin type. + * @param database the connecting database, may be empty. + * @param attributes the connecting attributes. + * @param zstdCompressionLevel the Zstd compression level. * @return the instance implemented by the specified protocol version. */ - static HandshakeResponse from(int envelopeId, Capability capability, int collationId, String user, - byte[] authentication, String authType, String database, Map attributes) { + static HandshakeResponse from(Capability capability, int collationId, String user, byte[] authentication, + String authType, String database, Map attributes, int zstdCompressionLevel) { if (capability.isProtocol41()) { - return new HandshakeResponse41(envelopeId, capability, collationId, user, authentication, - authType, database, attributes); + return new HandshakeResponse41(capability, collationId, user, authentication, authType, database, + attributes, zstdCompressionLevel); } - return new HandshakeResponse320(envelopeId, capability, user, authentication, database); + return new HandshakeResponse320(capability, user, authentication, database); } /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java similarity index 87% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java index c93705ea6..e3547faeb 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java @@ -23,7 +23,7 @@ import java.nio.charset.Charset; import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -41,19 +41,14 @@ final class HandshakeResponse320 extends ScalarClientMessage implements Handshak private final String database; - HandshakeResponse320(int envelopeId, Capability capability, String user, byte[] authentication, + HandshakeResponse320(Capability capability, String user, byte[] authentication, String database) { - this.header = new SslRequest320(envelopeId, capability); + this.header = new SslRequest320(capability); this.user = requireNonNull(user, "user must not be null"); this.authentication = requireNonNull(authentication, "authentication must not be null"); this.database = requireNonNull(database, "database must not be null"); } - @Override - public int getEnvelopeId() { - return header.getEnvelopeId(); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -79,8 +74,7 @@ public int hashCode() { @Override public String toString() { - return "HandshakeResponse320{envelopeId=" + header.getEnvelopeId() + - ", capability=" + header.getCapability() + ", user='" + user + + return "HandshakeResponse320{capability=" + header.getCapability() + ", user='" + user + "', authentication=REDACTED, database='" + database + "'}"; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java similarity index 82% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java index f727364f5..40bbd5ce0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java @@ -49,21 +49,17 @@ final class HandshakeResponse41 extends ScalarClientMessage implements Handshake private final Map attributes; - // private final byte zStdCompressionLevel; // When Z-Standard compression supporting + private final int zstdCompressionLevel; - HandshakeResponse41(int envelopeId, Capability capability, int collationId, String user, - byte[] authentication, String authType, String database, Map attributes) { - this.header = new SslRequest41(envelopeId, capability, collationId); + HandshakeResponse41(Capability capability, int collationId, String user, byte[] authentication, + String authType, String database, Map attributes, int zstdCompressionLevel) { + this.header = new SslRequest41(capability, collationId); this.user = requireNonNull(user, "user must not be null"); this.authentication = requireNonNull(authentication, "authentication must not be null"); this.database = requireNonNull(database, "database must not be null"); this.authType = requireNonNull(authType, "authType must not be null"); this.attributes = requireNonNull(attributes, "attributes must not be null"); - } - - @Override - public int getEnvelopeId() { - return header.getEnvelopeId(); + this.zstdCompressionLevel = zstdCompressionLevel; } @Override @@ -71,15 +67,16 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof HandshakeResponse41)) { return false; } HandshakeResponse41 that = (HandshakeResponse41) o; - return header.equals(that.header) && user.equals(that.user) && - Arrays.equals(authentication, that.authentication) && authType.equals(that.authType) && - database.equals(that.database) && attributes.equals(that.attributes); + return zstdCompressionLevel == that.zstdCompressionLevel && header.equals(that.header) && + user.equals(that.user) && Arrays.equals(authentication, that.authentication) && + authType.equals(that.authType) && database.equals(that.database) && + attributes.equals(that.attributes); } @Override @@ -89,16 +86,18 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(authentication); result = 31 * result + authType.hashCode(); result = 31 * result + database.hashCode(); - return 31 * result + attributes.hashCode(); + result = 31 * result + attributes.hashCode(); + return 31 * result + zstdCompressionLevel; } @Override public String toString() { - return "HandshakeResponse41{envelopeId=" + header.getEnvelopeId() + - ", capability=" + header.getCapability() + + return "HandshakeResponse41{capability=" + header.getCapability() + ", collationId=" + header.getCollationId() + ", user='" + user + "', authentication=REDACTED, authType='" + authType + - "', database='" + database + "', attributes=" + attributes + '}'; + "', database='" + database + "', attributes=" + attributes + + ", zstdCompressionLevel=" + zstdCompressionLevel + + '}'; } @Override @@ -124,13 +123,17 @@ protected void writeTo(ByteBuf buf, ConnectionContext context) { } if (capability.isPluginAuthAllowed()) { - // This must be an UTF-8 string. + // This must be a UTF-8 string. HandshakeResponse.writeCString(buf, authType, StandardCharsets.UTF_8); } if (capability.isConnectionAttributesAllowed()) { writeAttrs(buf, charset); } + + if (capability.isZstdCompression()) { + buf.writeByte(zstdCompressionLevel); + } } private void writeAttrs(ByteBuf buf, Charset charset) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java new file mode 100644 index 000000000..20e71ae95 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.message.client; + +import io.asyncer.r2dbc.mysql.ConnectionContext; +import io.netty.buffer.ByteBuf; + +public final class InitDbMessage extends ScalarClientMessage { + + private static final byte FLAG = 0x02; + + private final String database; + + public InitDbMessage(String database) { + this.database = database; + } + + @Override + protected void writeTo(ByteBuf buf, ConnectionContext context) { + // RestOfPacketString, no need terminal or length + buf.writeByte(FLAG).writeCharSequence(database, context.getClientCollation().getCharset()); + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java similarity index 89% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java index c9ca2419f..63a360f49 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java @@ -38,20 +38,22 @@ */ public final class LocalInfileResponse implements SubsequenceClientMessage { - private final int envelopeId; - private final String path; private final SynchronousSink errorSink; - public LocalInfileResponse(int envelopeId, String path, SynchronousSink errorSink) { + public LocalInfileResponse(String path, SynchronousSink errorSink) { requireNonNull(path, "path must not be null"); - this.envelopeId = envelopeId; this.path = path; this.errorSink = errorSink; } + @Override + public boolean isCumulative() { + return false; + } + @Override public Flux encode(ByteBufAllocator allocator, ConnectionContext context) { return Flux.defer(() -> { @@ -93,11 +95,6 @@ public Flux encode(ByteBufAllocator allocator, ConnectionContext contex }); } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -109,17 +106,16 @@ public boolean equals(Object o) { LocalInfileResponse that = (LocalInfileResponse) o; - return envelopeId == that.envelopeId && path.equals(that.path); + return path.equals(that.path); } @Override public int hashCode() { - return 31 * envelopeId + path.hashCode(); + return path.hashCode(); } @Override public String toString() { - return "LocalInfileResponse{envelopeId=" + envelopeId + - ", path='" + path + "'}"; + return "LocalInfileResponse{path='" + path + "'}"; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java index 04a99754e..a22a64b8b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java @@ -34,7 +34,7 @@ /** * A default implementation of {@link ParameterWriter}. *

- * WARNING: It is not safe for multithreaded access. + * WARNING: It is not thread safe */ final class ParamWriter extends ParameterWriter { @@ -45,14 +45,17 @@ final class ParamWriter extends ParameterWriter { private final StringBuilder builder; + private final boolean noBackslashEscapes; + private final Query query; private int index; private Mode mode; - private ParamWriter(Query query) { + private ParamWriter(boolean noBackslashEscapes, Query query) { this.builder = newBuilder(query); + this.noBackslashEscapes = noBackslashEscapes; this.query = query; this.index = 1; this.mode = 1 < query.getPartSize() ? Mode.AVAILABLE : Mode.FULL; @@ -318,15 +321,19 @@ private void write0(char[] s, int off, int len) { } private void escape(char c) { + if (c == '\'') { + // MySQL will auto-combine consecutive strings, whatever backslash is used or not, e.g. '1''2' -> '1\'2' + builder.append('\'').append('\''); + return; + } else if (noBackslashEscapes) { + builder.append(c); + return; + } + switch (c) { case '\\': builder.append('\\').append('\\'); break; - case '\'': - // MySQL will auto-combine consecutive strings, like '1''2' -> '12'. - // Sure, there can use "\\'", but this will be better. (For some logging systems) - builder.append('\'').append('\''); - break; // Maybe useful in the future, keep '"' here. // case '"': buf.append('\\').append('"'); break; // SHIFT-JIS, WINDOWS-932, EUC-JP and eucJP-OPEN will encode '\u00a5' (the sign of Japanese Yen @@ -335,20 +342,19 @@ private void escape(char c) { // case '\u00a5': do something; break; // case '\u20a9': do something; break; case 0: - // MySQL is based on C/C++, must escape '\0' which is an end flag in C style string. + // Should escape '\0' which is an end flag in C style string. builder.append('\\').append('0'); break; case '\032': - // It seems like a problem on Windows 32, maybe check current OS here? + // It gives some problems on Win32. builder.append('\\').append('Z'); break; case '\n': - // Should escape it for some logging such as Relational Database Service (RDS) Logging - // System, etc. Sure, it is not necessary, but this will be better. + // Should be escaped for better logging. builder.append('\\').append('n'); break; case '\r': - // Should escape it for some logging such as RDS Logging System, etc. + // Should be escaped for better logging. builder.append('\\').append('r'); break; default: @@ -357,9 +363,9 @@ private void escape(char c) { } } - static Mono publish(Query query, Flux values) { + static Mono publish(boolean noBackslashEscapes, Query query, Flux values) { return Mono.defer(() -> { - ParamWriter writer = new ParamWriter(query); + ParamWriter writer = new ParamWriter(noBackslashEscapes, query); return OperatorUtils.discardOnCancel(values) .doOnDiscard(MySqlParameter.class, DISPOSE) diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java similarity index 97% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java index d6ea6d783..6cd7edf9c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java @@ -87,7 +87,7 @@ public Mono encode(ByteBufAllocator allocator, ConnectionContext contex return Flux.fromArray(values); }); - return ParamWriter.publish(query, parameters).handle((it, sink) -> { + return ParamWriter.publish(context.isNoBackslashEscapes(), query, parameters).handle((it, sink) -> { ByteBuf buf = allocator.buffer(); try { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java similarity index 96% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java index 7f4598013..e62906101 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java @@ -28,7 +28,7 @@ */ abstract class ScalarClientMessage implements ClientMessage { - abstract protected void writeTo(ByteBuf buf, ConnectionContext context); + protected abstract void writeTo(ByteBuf buf, ConnectionContext context); @Override public Mono encode(ByteBufAllocator allocator, ConnectionContext context) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java index 4b53ddbed..7e6062924 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java @@ -29,9 +29,9 @@ */ abstract class SizedClientMessage implements ClientMessage { - abstract protected int size(); + protected abstract int size(); - abstract protected void writeTo(ByteBuf buf); + protected abstract void writeTo(ByteBuf buf); @Override public Mono encode(ByteBufAllocator allocator, ConnectionContext context) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java similarity index 84% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java index 16420a412..81cf4eebf 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java @@ -36,18 +36,17 @@ public interface SslRequest extends SubsequenceClientMessage { * Construct an instance of {@link SslRequest}, it is implemented by the protocol version that is given by * {@link Capability}. * - * @param envelopeId the beginning envelope ID of this message. * @param capability the current {@link Capability}. * @param collationId the {@code CharCollation} ID, or 0 if server does not return a collation ID. * @return the instance implemented by the specified protocol version. */ - static SslRequest from(int envelopeId, Capability capability, int collationId) { + static SslRequest from(Capability capability, int collationId) { require(capability.isSslEnabled(), "capability must be SSL enabled"); if (capability.isProtocol41()) { - return new SslRequest41(envelopeId, capability, collationId); + return new SslRequest41(capability, collationId); } - return new SslRequest320(envelopeId, capability); + return new SslRequest320(capability); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java similarity index 74% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java index 096b2c46e..216476189 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.client; import io.asyncer.r2dbc.mysql.Capability; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; @@ -27,24 +27,16 @@ */ final class SslRequest320 extends SizedClientMessage implements SslRequest { - private static final int SIZE = Short.BYTES + Envelopes.SIZE_FIELD_SIZE; - - private final int envelopeId; + private static final int SIZE = Short.BYTES + Packets.SIZE_FIELD_SIZE; private final Capability capability; - SslRequest320(int envelopeId, Capability capability) { + SslRequest320(Capability capability) { require(!capability.isProtocol41(), "protocol 4.1 capability should never be set"); - this.envelopeId = envelopeId; this.capability = capability; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public Capability getCapability() { return capability; @@ -61,18 +53,17 @@ public boolean equals(Object o) { SslRequest320 that = (SslRequest320) o; - return envelopeId == that.envelopeId && capability.equals(that.capability); + return capability.equals(that.capability); } @Override public int hashCode() { - return 31 * envelopeId + capability.hashCode(); + return capability.hashCode(); } @Override public String toString() { - return "SslRequest320{envelopeId=" + envelopeId + - ", capability=" + capability + '}'; + return "SslRequest320{capability=" + capability + '}'; } @Override @@ -84,6 +75,6 @@ protected int size() { protected void writeTo(ByteBuf buf) { // Protocol 3.20 only allows low 16-bits capabilities. buf.writeShortLE(capability.getBaseBitmap() & 0xFFFF) - .writeMediumLE(Envelopes.MAX_ENVELOPE_SIZE); + .writeMediumLE(Packets.MAX_PAYLOAD_SIZE); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java similarity index 80% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java index bca7e099c..2e270ae29 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.client; import io.asyncer.r2dbc.mysql.Capability; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; @@ -34,25 +34,17 @@ final class SslRequest41 extends SizedClientMessage implements SslRequest { private static final int BUF_SIZE = Integer.BYTES + Integer.BYTES + Byte.BYTES + RESERVED_SIZE + MARIA_DB_CAPABILITY_SIZE; - private final int envelopeId; - private final Capability capability; private final int collationId; - SslRequest41(int envelopeId, Capability capability, int collationId) { + SslRequest41(Capability capability, int collationId) { require(collationId > 0, "collationId must be a positive integer"); - this.envelopeId = envelopeId; this.capability = capability; this.collationId = collationId; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -64,22 +56,19 @@ public boolean equals(Object o) { SslRequest41 that = (SslRequest41) o; - return envelopeId == that.envelopeId && - collationId == that.collationId && + return collationId == that.collationId && capability.equals(that.capability); } @Override public int hashCode() { - int result = 31 * envelopeId + capability.hashCode(); + int result = capability.hashCode(); return 31 * result + collationId; } @Override public String toString() { - return "SslRequest41{envelopeId=" + envelopeId + - ", capability=" + capability + - ", collationId=" + collationId + '}'; + return "SslRequest41{capability=" + capability + ", collationId=" + collationId + '}'; } @Override @@ -95,7 +84,7 @@ protected int size() { @Override protected void writeTo(ByteBuf buf) { buf.writeIntLE(capability.getBaseBitmap()) - .writeIntLE(Envelopes.MAX_ENVELOPE_SIZE) + .writeIntLE(Packets.MAX_PAYLOAD_SIZE) .writeByte(collationId & 0xFF); // only low 8-bits if (capability.isMariaDb()) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java similarity index 84% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java index 091e7a57b..fb2364b92 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java @@ -24,10 +24,8 @@ */ public interface SubsequenceClientMessage extends ClientMessage { - /** - * Gets the current envelope ID used to serialize subsequent request messages. - * - * @return the current envelope ID. - */ - int getEnvelopeId(); + @Override + default boolean isSequenceReset() { + return false; + } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java similarity index 93% rename from src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java index d2611e3b5..4248ee533 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java @@ -22,4 +22,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.message; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java similarity index 72% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java index 543cbb8e3..ba987287e 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java @@ -25,27 +25,20 @@ public final class AuthMoreDataMessage implements ServerMessage { private static final byte AUTH_SUCCEED = 3; - private final int envelopeId; - private final boolean failed; - private AuthMoreDataMessage(int envelopeId, boolean failed) { - this.envelopeId = envelopeId; + private AuthMoreDataMessage(boolean failed) { this.failed = failed; } - public int getEnvelopeId() { - return envelopeId; - } - public boolean isFailed() { return failed; } - static AuthMoreDataMessage decode(int envelopeId, ByteBuf buf) { + static AuthMoreDataMessage decode(ByteBuf buf) { buf.skipBytes(1); // auth more data message header, 0x01 - return new AuthMoreDataMessage(envelopeId, buf.readByte() != AUTH_SUCCEED); + return new AuthMoreDataMessage(buf.readByte() != AUTH_SUCCEED); } @Override @@ -59,16 +52,16 @@ public boolean equals(Object o) { AuthMoreDataMessage that = (AuthMoreDataMessage) o; - return envelopeId == that.envelopeId && failed == that.failed; + return failed == that.failed; } @Override public int hashCode() { - return (envelopeId << 1) | (failed ? 1 : 0); + return (failed ? 1 : 0); } @Override public String toString() { - return "AuthMoreDataMessage{envelopeId=" + envelopeId + ", failed=" + failed + '}'; + return "AuthMoreDataMessage{failed=" + failed + '}'; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java similarity index 71% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java index dc3a1a142..ba1d38479 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java @@ -21,7 +21,7 @@ import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -29,22 +29,15 @@ */ public final class ChangeAuthMessage implements ServerMessage { - private final int envelopeId; - private final String authType; private final byte[] salt; - private ChangeAuthMessage(int envelopeId, String authType, byte[] salt) { - this.envelopeId = envelopeId; + private ChangeAuthMessage(String authType, byte[] salt) { this.authType = requireNonNull(authType, "authType must not be null"); this.salt = requireNonNull(salt, "salt must not be null"); } - public int getEnvelopeId() { - return envelopeId; - } - public String getAuthType() { return authType; } @@ -58,29 +51,26 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof ChangeAuthMessage)) { return false; } ChangeAuthMessage that = (ChangeAuthMessage) o; - return envelopeId == that.envelopeId && authType.equals(that.authType) && - Arrays.equals(salt, that.salt); + return authType.equals(that.authType) && Arrays.equals(salt, that.salt); } @Override public int hashCode() { - int result = envelopeId; - result = 31 * result + authType.hashCode(); - return 31 * result + Arrays.hashCode(salt); + return 31 * authType.hashCode() + Arrays.hashCode(salt); } @Override public String toString() { - return "ChangeAuthMessage{envelopeId=" + envelopeId + ", authType='" + authType + "', salt=REDACTED}"; + return "ChangeAuthMessage{authType='" + authType + "', salt=REDACTED}"; } - static ChangeAuthMessage decode(int envelopeId, ByteBuf buf) { + static ChangeAuthMessage decode(ByteBuf buf) { buf.skipBytes(1); // skip generic header 0xFE of change authentication messages String authType = HandshakeHeader.readCStringAscii(buf); @@ -90,6 +80,6 @@ static ChangeAuthMessage decode(int envelopeId, ByteBuf buf) { ByteBufUtil.getBytes(buf); // The terminal character has been removed from salt. - return new ChangeAuthMessage(envelopeId, authType, salt); + return new ChangeAuthMessage(authType, salt); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java similarity index 88% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java index a2aac7a8b..b4a9d9fdd 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java @@ -16,7 +16,6 @@ package io.asyncer.r2dbc.mysql.message.server; -import io.asyncer.r2dbc.mysql.ColumnDefinition; import io.asyncer.r2dbc.mysql.ConnectionContext; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -53,15 +52,14 @@ public final class DefinitionMetadataMessage implements ServerMessage { private final short typeId; - private final ColumnDefinition definition; + private final int definitions; private final short decimals; private DefinitionMetadataMessage(@Nullable String database, String table, @Nullable String originTable, String column, @Nullable String originColumn, int collationId, long size, short typeId, - ColumnDefinition definition, short decimals) { + int definitions, short decimals) { require(size >= 0, "size must not be a negative integer"); - require(collationId > 0, "collationId must be a positive integer"); this.database = database; this.table = requireNonNull(table, "table must not be null"); @@ -71,7 +69,7 @@ private DefinitionMetadataMessage(@Nullable String database, String table, @Null this.collationId = collationId; this.size = size; this.typeId = typeId; - this.definition = requireNonNull(definition, "definition must not be null"); + this.definitions = definitions; this.decimals = decimals; } @@ -91,8 +89,8 @@ public short getTypeId() { return typeId; } - public ColumnDefinition getDefinition() { - return definition; + public int getDefinitions() { + return definitions; } public short getDecimals() { @@ -111,7 +109,7 @@ public boolean equals(Object o) { return collationId == that.collationId && size == that.size && typeId == that.typeId && - definition.equals(that.definition) && + definitions == that.definitions && decimals == that.decimals && Objects.equals(database, that.database) && table.equals(that.table) && @@ -123,14 +121,14 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(database, table, originTable, column, originColumn, collationId, size, typeId, - definition, decimals); + definitions, decimals); } @Override public String toString() { return "DefinitionMetadataMessage{database='" + database + "', table='" + table + "' (origin:'" + originTable + "'), column='" + column + "' (origin:'" + originColumn + "'), collationId=" + - collationId + ", size=" + size + ", type=" + typeId + ", definition=" + definition + + collationId + ", size=" + size + ", type=" + typeId + ", definitions=" + definitions + ", decimals=" + decimals + '}'; } @@ -155,11 +153,11 @@ private static DefinitionMetadataMessage decode320(ByteBuf buf, ConnectionContex short typeId = buf.readUnsignedByte(); buf.skipBytes(1); // Constant 0x3 - ColumnDefinition definition = ColumnDefinition.of(buf.readShortLE()); + int definitions = buf.readUnsignedShortLE(); short decimals = buf.readUnsignedByte(); - return new DefinitionMetadataMessage(null, table, null, column, null, collation.getId(), size, typeId, - definition, decimals); + return new DefinitionMetadataMessage(null, table, null, column, null, 0, size, typeId, + definitions, decimals); } private static DefinitionMetadataMessage decode41(ByteBuf buf, ConnectionContext context) { @@ -179,10 +177,10 @@ private static DefinitionMetadataMessage decode41(ByteBuf buf, ConnectionContext int collationId = buf.readUnsignedShortLE(); long size = buf.readUnsignedIntLE(); short typeId = buf.readUnsignedByte(); - ColumnDefinition definition = ColumnDefinition.of(buf.readShortLE(), collationId); + int definitions = buf.readUnsignedShortLE(); return new DefinitionMetadataMessage(database, table, originTable, column, originColumn, collationId, - size, typeId, definition, buf.readUnsignedByte()); + size, typeId, definitions, buf.readUnsignedByte()); } private static String readVarIntSizedString(ByteBuf buf, Charset charset) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java index a06751fcd..83aa23306 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.server; /** - * A EOF message for current context in protocol 3.20. + * An EOF message for current context in protocol 3.20. *

* Note: It is also Old Change Authentication Request. */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java similarity index 97% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java index c134f804b..489870fe8 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java @@ -20,7 +20,7 @@ import io.netty.buffer.ByteBuf; /** - * A EOF message for current context in protocol 4.1. + * An EOF message for current context in protocol 4.1. */ final class Eof41Message implements EofMessage, WarningMessage, ServerStatusMessage { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java similarity index 99% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java index a7ef07fb8..feef32599 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java @@ -97,6 +97,7 @@ public R2dbcException toException(@Nullable String sql) { case 1907: // Statement executing timeout case 3024: // Query execution was interrupted, maximum statement execution time exceeded case 1969: // Query execution was interrupted + case 1968: // Query execution was interrupted (max_statement_time exceeded) return new R2dbcTimeoutException(message, sqlState, code); case 1613: // Transaction rollback because of took too long return new R2dbcRollbackException(message, sqlState, code); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java similarity index 98% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java index 6ad92fea9..6785a930f 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java @@ -21,7 +21,7 @@ import java.nio.charset.StandardCharsets; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java similarity index 70% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java index 81cb22eb1..c74c1a3ff 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java @@ -26,56 +26,48 @@ public interface HandshakeRequest extends ServerMessage { /** - * Get the handshake request header. + * Gets the handshake request header. * * @return the header. */ HandshakeHeader getHeader(); /** - * Get the envelope identifier of this message packet. - * - * @return envelope identifier. - */ - int getEnvelopeId(); - - /** - * Get the server-side capability. + * Gets the server-side capability. * * @return the server-side capability. */ Capability getServerCapability(); /** - * Get the authentication plugin type name. + * Gets the authentication plugin type name. * * @return the authentication plugin type. */ String getAuthType(); /** - * Get the challenge salt for authentication. + * Gets the challenge salt for authentication. * * @return the challenge salt. */ byte[] getSalt(); /** - * Decode a {@link HandshakeRequest} from a envelope {@link ByteBuf}. + * Decodes a {@link HandshakeRequest} from a payload {@link ByteBuf} of a normal packet. * - * @param envelopeId envelope identifier. - * @param buf the {@link ByteBuf}. + * @param buf the {@link ByteBuf}. * @return decoded {@link HandshakeRequest}. */ - static HandshakeRequest decode(int envelopeId, ByteBuf buf) { + static HandshakeRequest decode(ByteBuf buf) { HandshakeHeader header = HandshakeHeader.decode(buf); int version = header.getProtocolVersion(); switch (version) { case 10: - return HandshakeV10Request.decode(envelopeId, buf, header); + return HandshakeV10Request.decode(buf, header); case 9: - return HandshakeV9Request.decode(envelopeId, buf, header); + return HandshakeV9Request.decode(buf, header); } throw new R2dbcPermissionDeniedException("Does not support handshake protocol version " + version); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java similarity index 82% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java index 47cd0bdc4..5f5a1de67 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java @@ -24,7 +24,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -42,8 +42,6 @@ final class HandshakeV10Request implements HandshakeRequest, ServerStatusMessage private final HandshakeHeader header; - private final int envelopeId; - private final byte[] salt; private final Capability serverCapability; @@ -52,10 +50,9 @@ final class HandshakeV10Request implements HandshakeRequest, ServerStatusMessage private final String authType; - private HandshakeV10Request(HandshakeHeader header, int envelopeId, byte[] salt, + private HandshakeV10Request(HandshakeHeader header, byte[] salt, Capability serverCapability, short serverStatuses, String authType) { this.header = requireNonNull(header, "header must not be null"); - this.envelopeId = envelopeId; this.salt = requireNonNull(salt, "salt must not be null"); this.serverCapability = requireNonNull(serverCapability, "serverCapability must not be null"); this.serverStatuses = serverStatuses; @@ -67,11 +64,6 @@ public HandshakeHeader getHeader() { return header; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public byte[] getSalt() { return salt; @@ -97,35 +89,35 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof HandshakeV10Request)) { return false; } HandshakeV10Request that = (HandshakeV10Request) o; - return envelopeId == that.envelopeId && serverStatuses == that.serverStatuses && - header.equals(that.header) && Arrays.equals(salt, that.salt) && - serverCapability.equals(that.serverCapability) && authType.equals(that.authType); + return serverStatuses == that.serverStatuses && header.equals(that.header) && + Arrays.equals(salt, that.salt) && serverCapability.equals(that.serverCapability) && + authType.equals(that.authType); } @Override public int hashCode() { - int hash = 31 * header.hashCode() + envelopeId; - hash = 31 * hash + Arrays.hashCode(salt); - hash = 31 * hash + serverCapability.hashCode(); - hash = 31 * hash + serverStatuses; - return 31 * hash + authType.hashCode(); + int result = header.hashCode(); + result = 31 * result + Arrays.hashCode(salt); + result = 31 * result + serverCapability.hashCode(); + result = 31 * result + (int) serverStatuses; + return 31 * result + authType.hashCode(); } @Override public String toString() { - return "HandshakeV10Request{header=" + header + ", envelopeId=" + envelopeId + + return "HandshakeV10Request{header=" + header + ", salt=REDACTED, serverCapability=" + serverCapability + ", serverStatuses=" + serverStatuses + ", authType='" + authType + "'}"; } - static HandshakeV10Request decode(int envelopeId, ByteBuf buf, HandshakeHeader header) { - Builder builder = new Builder(envelopeId, header); + static HandshakeV10Request decode(ByteBuf buf, HandshakeHeader header) { + Builder builder = new Builder(header); ByteBuf salt = buf.alloc().buffer(); try { @@ -194,8 +186,6 @@ static HandshakeV10Request decode(int envelopeId, ByteBuf buf, HandshakeHeader h private static final class Builder { - private final int envelopeId; - private final HandshakeHeader header; private String authType; @@ -206,14 +196,12 @@ private static final class Builder { private short serverStatuses; - private Builder(int envelopeId, HandshakeHeader header) { - this.envelopeId = envelopeId; + private Builder(HandshakeHeader header) { this.header = header; } HandshakeV10Request build() { - return new HandshakeV10Request(header, envelopeId, salt, serverCapability, serverStatuses, - authType); + return new HandshakeV10Request(header, salt, serverCapability, serverStatuses, authType); } void authType(String authType) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java similarity index 72% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java index ea34e1c55..b92d29256 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java @@ -23,7 +23,7 @@ import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_BYTES; @@ -36,13 +36,10 @@ final class HandshakeV9Request implements HandshakeRequest { private final HandshakeHeader header; - private final int envelopeId; - private final byte[] salt; - private HandshakeV9Request(HandshakeHeader header, int envelopeId, byte[] salt) { + private HandshakeV9Request(HandshakeHeader header, byte[] salt) { this.header = requireNonNull(header, "header must not be null"); - this.envelopeId = envelopeId; this.salt = requireNonNull(salt, "salt must not be null"); } @@ -51,11 +48,6 @@ public HandshakeHeader getHeader() { return header; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public Capability getServerCapability() { return SERVER_CAPABILITY; @@ -76,31 +68,31 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof HandshakeV9Request)) { return false; } HandshakeV9Request that = (HandshakeV9Request) o; - return envelopeId == that.envelopeId && header.equals(that.header) && Arrays.equals(salt, that.salt); + return header.equals(that.header) && Arrays.equals(salt, that.salt); } @Override public int hashCode() { - int hash = 31 * header.hashCode() + envelopeId; - return 31 * hash + Arrays.hashCode(salt); + int result = header.hashCode(); + return 31 * result + Arrays.hashCode(salt); } @Override public String toString() { - return "HandshakeV9Request{header=" + header + ", envelopeId=" + envelopeId + ", salt=REDACTED}"; + return "HandshakeV9Request{header=" + header + ", salt=REDACTED}"; } - static HandshakeV9Request decode(int envelopeId, ByteBuf buf, HandshakeHeader header) { + static HandshakeV9Request decode(ByteBuf buf, HandshakeHeader header) { int bytes = buf.readableBytes(); if (bytes <= 0) { - return new HandshakeV9Request(header, envelopeId, EMPTY_BYTES); + return new HandshakeV9Request(header, EMPTY_BYTES); } byte[] salt; @@ -111,6 +103,6 @@ static HandshakeV9Request decode(int envelopeId, ByteBuf buf, HandshakeHeader he salt = ByteBufUtil.getBytes(buf); } - return new HandshakeV9Request(header, envelopeId, salt); + return new HandshakeV9Request(header, salt); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java similarity index 96% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java index 9842687b1..d26975823 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.message.server; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.asyncer.r2dbc.mysql.message.FieldValue; @@ -36,7 +36,7 @@ /** * An implementation of {@link FieldReader} for large result which bytes more than {@link Integer#MAX_VALUE}, - * it would be exists when MySQL server return LOB types (i.e. BLOB, CLOB), LONGTEXT length can be unsigned + * it is used by the MySQL server returns LOB types (i.e. BLOB, CLOB), e.g. LONGTEXT length can be unsigned * int32. */ final class LargeFieldReader extends AbstractReferenceCounted implements FieldReader { @@ -136,7 +136,7 @@ protected void deallocate() { private List readSlice(ByteBuf current, long length) { ByteBuf buf = current; List results = new ArrayList<>(Math.max( - (int) Math.min(Long.divideUnsigned(length, Envelopes.MAX_ENVELOPE_SIZE) + 2, Byte.MAX_VALUE), + (int) Math.min(Long.divideUnsigned(length, Packets.MAX_PAYLOAD_SIZE) + 2, Byte.MAX_VALUE), 10 )); long totalSize = 0; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java similarity index 70% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java index e493e2bf8..433059f1b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java @@ -25,26 +25,19 @@ */ public final class LocalInfileRequest implements ServerMessage { - private final int envelopeId; - private final String path; - private LocalInfileRequest(int envelopeId, String path) { - this.envelopeId = envelopeId; + private LocalInfileRequest(String path) { this.path = path; } - public int getEnvelopeId() { - return envelopeId; - } - public String getPath() { return path; } - static LocalInfileRequest decode(int envelopeId, ByteBuf buf, ConnectionContext context) { + static LocalInfileRequest decode(ByteBuf buf, ConnectionContext context) { buf.skipBytes(1); // Constant 0xFB - return new LocalInfileRequest(envelopeId, buf.toString(context.getClientCollation().getCharset())); + return new LocalInfileRequest(buf.toString(context.getClientCollation().getCharset())); } @Override @@ -58,17 +51,16 @@ public boolean equals(Object o) { LocalInfileRequest that = (LocalInfileRequest) o; - return envelopeId == that.envelopeId && path.equals(that.path); + return path.equals(that.path); } @Override public int hashCode() { - return 31 * envelopeId + path.hashCode(); + return path.hashCode(); } @Override public String toString() { - return "LocalInfileRequest{envelopeId=" + envelopeId + - ", path='" + path + "'}"; + return "LocalInfileRequest{path='" + path + "'}"; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java similarity index 94% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java index 978ee8411..435748f87 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java @@ -81,7 +81,7 @@ final SyntheticMetadataMessage putPart(ServerMessage message) { } @Nullable - abstract protected SyntheticMetadataMessage checkComplete(int index, @Nullable EofMessage eof); + protected abstract SyntheticMetadataMessage checkComplete(int index, @Nullable EofMessage eof); /** * Put a column metadata message into this context. @@ -89,19 +89,19 @@ final SyntheticMetadataMessage putPart(ServerMessage message) { * @param metadata the column metadata message. * @return current index after putting the metadata. */ - abstract protected int putMetadata(DefinitionMetadataMessage metadata); + protected abstract int putMetadata(DefinitionMetadataMessage metadata); /** * Get the current index, for {@link #checkComplete(int, EofMessage)} when receive a EOF message. * * @return the current index. */ - abstract protected int currentIndex(); + protected abstract int currentIndex(); /** * Get checkpoints for logging. * * @return serializable object, like {@link String} or {@link Integer}. */ - abstract protected Object loggingPoints(); + protected abstract Object loggingPoints(); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java similarity index 58% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java index da937fe17..a06a00fbe 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java @@ -21,8 +21,12 @@ import io.asyncer.r2dbc.mysql.constant.ServerStatuses; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; +import org.jetbrains.annotations.Nullable; import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; @@ -34,6 +38,8 @@ */ public final class OkMessage implements WarningMessage, ServerStatusMessage, CompleteMessage { + private static final int SESSION_TRACK_SYSTEM_VARIABLES = 0; + private static final int MIN_SIZE = 7; private final boolean isEndOfRows; @@ -51,14 +57,17 @@ public final class OkMessage implements WarningMessage, ServerStatusMessage, Com private final String information; + private final Map systemVariables; + private OkMessage(boolean isEndOfRows, long affectedRows, long lastInsertId, short serverStatuses, - int warnings, String information) { + int warnings, String information, Map systemVariables) { this.isEndOfRows = isEndOfRows; this.affectedRows = affectedRows; this.lastInsertId = lastInsertId; this.serverStatuses = serverStatuses; this.warnings = warnings; this.information = requireNonNull(information, "information must not be null"); + this.systemVariables = requireNonNull(systemVariables, "systemVariables must not be null"); } public boolean isEndOfRows() { @@ -83,6 +92,11 @@ public int getWarnings() { return warnings; } + @Nullable + public String getSystemVariable(String key) { + return systemVariables.get(key); + } + @Override public boolean isDone() { return (serverStatuses & ServerStatuses.MORE_RESULTS_EXISTS) == 0; @@ -104,7 +118,8 @@ public boolean equals(Object o) { lastInsertId == okMessage.lastInsertId && serverStatuses == okMessage.serverStatuses && warnings == okMessage.warnings && - information.equals(okMessage.information); + information.equals(okMessage.information) && + systemVariables.equals(okMessage.systemVariables); } @Override @@ -114,7 +129,8 @@ public int hashCode() { result = 31 * result + (int) (lastInsertId ^ (lastInsertId >>> 32)); result = 31 * result + serverStatuses; result = 31 * result + warnings; - return 31 * result + information.hashCode(); + result = 31 * result + information.hashCode(); + return 31 * result + systemVariables.hashCode(); } @Override @@ -124,7 +140,9 @@ public String toString() { ", affectedRows=" + Long.toUnsignedString(affectedRows) + ", lastInsertId=" + Long.toUnsignedString(lastInsertId) + ", serverStatuses=" + Integer.toHexString(serverStatuses) + - ", information='" + information + "'}"; + ", information='" + information + + "', systemVariables=" + systemVariables + + '}'; } return "OkMessage{isEndOfRows=" + isEndOfRows + @@ -132,7 +150,9 @@ public String toString() { ", lastInsertId=" + Long.toUnsignedString(lastInsertId) + ", serverStatuses=" + Integer.toHexString(serverStatuses) + ", warnings=" + warnings + - ", information='" + information + "'}"; + ", information='" + information + + "', systemVariables=" + systemVariables + + "}"; } static boolean isValidSize(int bytes) { @@ -164,26 +184,79 @@ static OkMessage decode(boolean isEndOfRows, ByteBuf buf, ConnectionContext cont if (sizeAfterVarInt < 0) { return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, - warnings, buf.toString(charset)); + warnings, buf.toString(charset), Collections.emptyMap()); + } + + int oldReaderIndex = buf.readerIndex(); + long infoSize = VarIntUtils.readVarInt(buf); + + if (infoSize > sizeAfterVarInt) { + // Compatible code, the information may be an EOF encoded string at early versions of MySQL. + String info = buf.toString(oldReaderIndex, buf.writerIndex() - oldReaderIndex, charset); + + return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, + info, Collections.emptyMap()); } - int readerIndex = buf.readerIndex(); - long size = VarIntUtils.readVarInt(buf); - String information; + // All the following have lengths should be less than Integer.MAX_VALUE + String information = buf.readCharSequence((int) infoSize, charset).toString(); + Map systemVariables = Collections.emptyMap(); + + while (VarIntUtils.checkNextVarInt(buf) >= 0) { + int stateInfoSize = (int) VarIntUtils.readVarInt(buf); + ByteBuf stateInfo = buf.readSlice(stateInfoSize); + + while (stateInfo.isReadable()) { + if (stateInfo.readByte() == SESSION_TRACK_SYSTEM_VARIABLES) { + systemVariables = readServerVariables(stateInfo, context); + } else { + // Ignore other state info + int skipBytes = (int) VarIntUtils.readVarInt(stateInfo); - if (size > sizeAfterVarInt) { - information = buf.toString(readerIndex, buf.writerIndex() - readerIndex, charset); - } else { - // JVM does NOT support strings longer than Integer.MAX_VALUE - information = buf.toString(buf.readerIndex(), (int) size, charset); + stateInfo.skipBytes(skipBytes); + } + } } // Ignore session track, it is not human-readable and useless for R2DBC client. return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, - information); + information, systemVariables); } // Maybe have no human-readable message - return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, ""); + return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, "", + Collections.emptyMap()); + } + + private static Map readServerVariables(ByteBuf buf, ConnectionContext context) { + // All lengths should NOT be greater than Integer.MAX_VALUE + Map map = new HashMap<>(); + Charset charset = context.getClientCollation().getCharset(); + int size = (int) VarIntUtils.readVarInt(buf); + ByteBuf sessionVar = buf.readSlice(size); + + while (sessionVar.readableBytes() > 0) { + int variableSize = (int) VarIntUtils.readVarInt(sessionVar); + String variable = sessionVar.toString(sessionVar.readerIndex(), variableSize, charset); + + sessionVar.skipBytes(variableSize); + + int valueSize = (int) VarIntUtils.readVarInt(sessionVar); + String value = sessionVar.toString(sessionVar.readerIndex(), valueSize, charset); + + sessionVar.skipBytes(valueSize); + map.put(variable, value); + } + + switch (map.size()) { + case 0: + return Collections.emptyMap(); + case 1: { + Map.Entry entry = map.entrySet().iterator().next(); + return Collections.singletonMap(entry.getKey(), entry.getValue()); + } + default: + return map; + } } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java similarity index 91% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java index b5acdf728..cea0cf63c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.server; /** - * A implementation of {@link DecodeContext} for waiting {@code PreparedOkMessage}. + * An implementation of {@link DecodeContext} for waiting {@code PreparedOkMessage}. */ final class PrepareQueryDecodeContext implements DecodeContext { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java similarity index 95% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java index 9e4a3ac74..e914b440b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.message.server; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.netty.util.ReferenceCounted; @@ -45,7 +45,7 @@ public final class RowMessage implements ReferenceCounted, ServerMessage { * @param context information context array. * @return the {@link FieldValue} array. */ - public FieldValue[] decode(boolean isBinary, MySqlColumnMetadata[] context) { + public FieldValue[] decode(boolean isBinary, MySqlReadableMetadata[] context) { return isBinary ? binary(context) : text(context.length); } @@ -69,7 +69,7 @@ private FieldValue[] text(int size) { } } - private FieldValue[] binary(MySqlColumnMetadata[] context) { + private FieldValue[] binary(MySqlReadableMetadata[] context) { reader.skipOneByte(); // constant 0x00 int size = context.length; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java similarity index 84% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java index f81a06200..b48bc4f74 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.server; import io.asyncer.r2dbc.mysql.ConnectionContext; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -58,24 +58,25 @@ public final class ServerMessageDecoder { /** * Decode a server-side message from {@link #parts} and current envelope. * - * @param envelope the current envelope. + * @param payload the payload of the current packet. * @param context the connection context. * @param decodeContext the decode context. * @return the server-side message, or {@code null} if {@code envelope} is not last packet. */ @Nullable - public ServerMessage decode(ByteBuf envelope, ConnectionContext context, DecodeContext decodeContext) { - requireNonNull(envelope, "envelope must not be null"); + public ServerMessage decode(ByteBuf payload, ConnectionContext context, DecodeContext decodeContext) { + requireNonNull(payload, "payload must not be null"); requireNonNull(context, "context must not be null"); requireNonNull(decodeContext, "decodeContext must not be null"); - List buffers = this.parts; - Byte id = readNotFinish(buffers, envelope); - if (id == null) { + parts.add(payload); + + if (payload.readableBytes() == Packets.MAX_PAYLOAD_SIZE) { + // Not last packet. return null; } - return decodeMessage(buffers, id.intValue() & 0xFF, context, decodeContext); + return decodeMessage(parts, context, decodeContext); } /** @@ -91,8 +92,8 @@ public void dispose() { } @Nullable - private static ServerMessage decodeMessage(List buffers, int envelopeId, - ConnectionContext context, DecodeContext decodeContext) { + private static ServerMessage decodeMessage(List buffers, ConnectionContext context, + DecodeContext decodeContext) { if (decodeContext instanceof ResultDecodeContext) { return decodeResult(buffers, context, (ResultDecodeContext) decodeContext); } @@ -104,14 +105,14 @@ private static ServerMessage decodeMessage(List buffers, int envelopeId try { if (decodeContext instanceof CommandDecodeContext) { - return decodeCommandMessage(envelopeId, combined, context); + return decodeCommandMessage(combined, context); } else if (decodeContext instanceof PreparedMetadataDecodeContext) { return decodePreparedMetadata(combined, context, (PreparedMetadataDecodeContext) decodeContext); } else if (decodeContext instanceof PrepareQueryDecodeContext) { return decodePrepareQuery(combined); } else if (decodeContext instanceof LoginDecodeContext) { - return decodeLogin(envelopeId, combined, context); + return decodeLogin(combined, context); } } finally { combined.release(); @@ -171,7 +172,7 @@ private static ServerMessage decodeResult(List buffers, ConnectionConte } finally { combined.release(); } - // Should not has other messages when metadata reading. + // Should not have other messages when metadata reading. } return decodeRow(buffers, firstBuf, header, context, "result"); @@ -194,8 +195,7 @@ private static ServerMessage decodePrepareQuery(ByteBuf buf) { " on prepare query phase"); } - private static ServerMessage decodeCommandMessage(int envelopeId, ByteBuf buf, - ConnectionContext context) { + private static ServerMessage decodeCommandMessage(ByteBuf buf, ConnectionContext context) { short header = buf.getUnsignedByte(buf.readerIndex()); switch (header) { case ERROR: @@ -211,18 +211,22 @@ private static ServerMessage decodeCommandMessage(int envelopeId, ByteBuf buf, // Maybe OK, maybe column count (unsupported EOF on command phase) if (OkMessage.isValidSize(byteSize)) { - // MySQL has hard limit of 4096 columns per-table, - // so if readable bytes upper than 7, it means if it is column count, - // column count is already upper than (1 << 24) - 1 = 16777215, it is impossible. + // MySQL has hard limited of 4096 columns per-table, + // so if readable bytes is greater than 7, it means if it is column count, + // column count is already greater than (1 << 24) - 1 = 16777215, it is impossible. // So it must be OK message, not be column count. return OkMessage.decode(false, buf, context); } else if (EofMessage.isValidSize(byteSize)) { return EofMessage.decode(buf); } + + break; case LOCAL_INFILE: if (buf.readableBytes() > 1) { - return LocalInfileRequest.decode(envelopeId, buf, context); + return LocalInfileRequest.decode(buf, context); } + + break; } if (VarIntUtils.checkNextVarInt(buf) == 0) { @@ -236,7 +240,7 @@ private static ServerMessage decodeCommandMessage(int envelopeId, ByteBuf buf, " on command phase"); } - private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, ConnectionContext context) { + private static ServerMessage decodeLogin(ByteBuf buf, ConnectionContext context) { short header = buf.getUnsignedByte(buf.readerIndex()); switch (header) { case OK: @@ -246,10 +250,10 @@ private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, Connection break; case AUTH_MORE_DATA: // Auth more data - return AuthMoreDataMessage.decode(envelopeId, buf); + return AuthMoreDataMessage.decode(buf); case HANDSHAKE_V9: case HANDSHAKE_V10: // Handshake V9 (not supported) or V10 - return HandshakeRequest.decode(envelopeId, buf); + return HandshakeRequest.decode(buf); case ERROR: // Error return ErrorMessage.decode(buf); case EOF: // Auth exchange message or EOF message @@ -257,7 +261,7 @@ private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, Connection return EofMessage.decode(buf); } - return ChangeAuthMessage.decode(envelopeId, buf); + return ChangeAuthMessage.decode(buf); } throw new R2dbcPermissionDeniedException("Unknown message header 0x" + @@ -265,32 +269,6 @@ private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, Connection " on connection phase"); } - @Nullable - private static Byte readNotFinish(List buffers, ByteBuf envelope) { - try { - int size = envelope.readUnsignedMediumLE(); - if (size < Envelopes.MAX_ENVELOPE_SIZE) { - Byte envelopeId = envelope.readByte(); - - buffers.add(envelope); - // success, no need release - envelope = null; - return envelopeId; - } - - // skip the sequence Id - envelope.skipBytes(1); - buffers.add(envelope); - // success, no need release - envelope = null; - return null; - } finally { - if (envelope != null) { - envelope.release(); - } - } - } - private static boolean isRow(List buffers, ByteBuf firstBuf, short header) { switch (header) { case RowMessage.NULL_VALUE: diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java similarity index 92% rename from src/main/java/io/asyncer/r2dbc/mysql/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java index b0dc5f8d9..4803f5428 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/r2dbc-mysql/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider similarity index 100% rename from src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider rename to r2dbc-mysql/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java new file mode 100644 index 000000000..3e7c5bdb7 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Base class for compression integration tests. + */ +abstract class CompressionIntegrationTestSupport extends IntegrationTestSupport { + + CompressionIntegrationTestSupport(CompressionAlgorithm algorithm) { + super(configuration(builder -> builder.compressionAlgorithms(algorithm))); + } + + @Test + void simpleQuery() { + byte[] hello = "Hello".getBytes(StandardCharsets.US_ASCII); + byte[] repeatedBytes = new byte[hello.length * 50]; + + for (int i = 0; i < 50; i++) { + System.arraycopy(hello, 0, repeatedBytes, i * hello.length, hello.length); + } + + String repeated = new String(repeatedBytes, StandardCharsets.US_ASCII); + + complete(connection -> connection.createStatement("SELECT REPEAT('Hello', 50)").execute() + .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, String.class))) + .collectList() + .doOnNext(actual -> assertThat(actual).isEqualTo(Collections.singletonList(repeated)))); + } + + @ParameterizedTest + @ValueSource(strings = { "stations", "users" }) + @SuppressWarnings("SqlSourceToSinkFlow") + void loadDataLocalInfile(String name) throws URISyntaxException, IOException { + URL tdlUrl = Objects.requireNonNull(getClass().getResource(String.format("/local/%s.sql", name))); + URL csvUrl = Objects.requireNonNull(getClass().getResource(String.format("/local/%s.csv", name))); + String tdl = new String(Files.readAllBytes(Paths.get(tdlUrl.toURI())), StandardCharsets.UTF_8); + String path = Paths.get(csvUrl.toURI()).toString(); + String loadData = String.format("LOAD DATA LOCAL INFILE '%s' INTO TABLE `%s` " + + "FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'", path, name); + String select = String.format("SELECT * FROM `%s` ORDER BY `id`", name); + AtomicInteger count = new AtomicInteger(-1); + + complete(conn -> conn.createStatement(tdl) + .execute() + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(conn.createStatement(loadData).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .reduce(0L, Long::sum) + .doOnNext(it -> count.set(it.intValue())) + .doOnNext(it -> assertThat(it).isGreaterThan(0)) + .thenMany(conn.createStatement(select).execute()) + .flatMap(result -> result.map(r -> 1)) + .reduce(0, Integer::sum) + .doOnNext(it -> assertThat(it).isEqualTo(count.get()))); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java similarity index 64% rename from src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java index dce1b0ddb..eb4a3ea3c 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java @@ -16,10 +16,13 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.cache.Caches; import io.asyncer.r2dbc.mysql.constant.ServerStatuses; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; +import io.r2dbc.spi.IsolationLevel; import org.junit.jupiter.api.Test; +import java.time.Duration; import java.time.ZoneId; import static org.assertj.core.api.Assertions.assertThat; @@ -31,46 +34,51 @@ public class ConnectionContextTest { @Test - void getServerZoneId() { + void getTimeZone() { for (int i = -12; i <= 12; ++i) { String id = i < 0 ? "UTC" + i : "UTC+" + i; ConnectionContext context = new ConnectionContext( ZeroDateOption.USE_NULL, null, - 8192, ZoneId.of(id)); + 8192, true, true, ZoneId.of(id)); - assertThat(context.getServerZoneId()).isEqualTo(ZoneId.of(id)); + assertThat(context.getTimeZone()).isEqualTo(ZoneId.of(id)); } } @Test - void shouldSetServerZoneId() { + void setTwiceTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, null); - assertThat(context.shouldSetServerZoneId()).isTrue(); - context.setServerZoneId(ZoneId.systemDefault()); - assertThat(context.shouldSetServerZoneId()).isFalse(); + 8192, true, true, null); + + context.initSession( + Caches.createPrepareCache(0), + IsolationLevel.REPEATABLE_READ, + false, Duration.ZERO, + null, + ZoneId.systemDefault() + ); + assertThatIllegalStateException().isThrownBy(() -> context.initSession( + Caches.createPrepareCache(0), + IsolationLevel.REPEATABLE_READ, + false, + Duration.ZERO, + null, + ZoneId.systemDefault() + )); } @Test - void shouldNotSetServerZoneId() { + void badSetTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, ZoneId.systemDefault()); - assertThat(context.shouldSetServerZoneId()).isFalse(); - } - - @Test - void setTwiceServerZoneId() { - ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, null); - context.setServerZoneId(ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.setServerZoneId(ZoneId.systemDefault())); - } - - @Test - void badSetServerZoneId() { - ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.setServerZoneId(ZoneId.systemDefault())); + 8192, true, true, ZoneId.systemDefault()); + assertThatIllegalStateException().isThrownBy(() -> context.initSession( + Caches.createPrepareCache(0), + IsolationLevel.REPEATABLE_READ, + false, + Duration.ZERO, + null, + ZoneId.systemDefault() + )); } public static ConnectionContext mock() { @@ -83,9 +91,9 @@ public static ConnectionContext mock(boolean isMariaDB) { public static ConnectionContext mock(boolean isMariaDB, ZoneId zoneId) { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, zoneId); + 8192, true, true, zoneId); - context.init(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"), + context.initHandshake(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"), Capability.of(~(isMariaDB ? 1 : 0))); context.setServerStatuses(ServerStatuses.AUTO_COMMIT); diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java similarity index 52% rename from src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index ed66f34ab..4e4fa34ae 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -16,11 +16,19 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlBatch; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.TransactionDefinition; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import org.testcontainers.shaded.com.fasterxml.jackson.databind.node.ArrayNode; @@ -41,6 +49,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Stream; import static io.r2dbc.spi.IsolationLevel.READ_COMMITTED; import static io.r2dbc.spi.IsolationLevel.READ_UNCOMMITTED; @@ -49,123 +59,190 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link MySqlConnection}. + * Integration tests for {@link MySqlSimpleConnection}. */ class ConnectionIntegrationTest extends IntegrationTestSupport { - private static final MySqlConnectionConfiguration config = configuration( - "r2dbc", false, false, null, null); - ConnectionIntegrationTest() { - super(config); + super(configuration(builder -> builder)); } @Test void isInTransaction() { - complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.context().isInTransaction()) .isFalse()) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(connection.commitTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse())); + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse())); } - @Test - void autoRollbackPreRelease() { - // Mock pool allocate/release. - complete(conn -> conn.postAllocate() - .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") - .execute()) + @ParameterizedTest + @ValueSource(strings = { + "test", + "test`data", + "test\ndata", + "I'm feeling good", + }) + void sqlModeNoBackslashEscapes(String value) { + String tdl = "CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` VARCHAR(50) NOT NULL)"; + + // Add NO_BACKSLASH_ESCAPES instead of replace + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.context().isNoBackslashEscapes()) + .isFalse()) + .thenMany(connection.createStatement(tdl).execute()) .flatMap(MySqlResult::getRowsUpdated) - .then(conn.beginTransaction()) - .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") + .thenMany(connection.createStatement("INSERT INTO test VALUES (1, ?)") + .bind(0, value) .execute()) .flatMap(MySqlResult::getRowsUpdated) - .single() - .doOnNext(it -> assertThat(it).isEqualTo(1)) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isTrue()) - .then(conn.preRelease()) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) - .then(conn.postAllocate()) - .thenMany(conn.createStatement("SELECT * FROM test") + .thenMany(connection.createStatement("SELECT COUNT(0) FROM `test` WHERE `value` = ?") + .bind(0, value) .execute()) - .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) - .count() - .doOnNext(it -> assertThat(it).isZero())); - } - - @Test - void shouldNotRollbackCommittedPreRelease() { - // Mock pool allocate/release. - complete(conn -> conn.postAllocate() - .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") + .flatMap(result -> result.map((row, metadata) -> row.get(0, Integer.class))) + .collectList() + .doOnNext(counts -> assertThat(counts).isEqualTo(Collections.singletonList(1))) + .thenMany(connection.createStatement("SELECT @@sql_mode").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get(0, String.class))) + .map(modes -> Stream.concat(Stream.of(modes.split(",")), Stream.of("NO_BACKSLASH_ESCAPES")) + .toArray(String[]::new)) + .last() + .flatMapMany(modes -> connection.createStatement("SET sql_mode = ?") + .bind(0, modes) .execute()) .flatMap(MySqlResult::getRowsUpdated) - .then(conn.beginTransaction()) - .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") + .doOnComplete(() -> assertThat(connection.context().isNoBackslashEscapes()).isTrue()) + .thenMany(connection.createStatement("INSERT INTO test VALUES (2, ?)") + .bind(0, value) .execute()) .flatMap(MySqlResult::getRowsUpdated) - .single() - .doOnNext(it -> assertThat(it).isEqualTo(1)) - .then(conn.commitTransaction()) - .then(conn.preRelease()) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) - .then(conn.postAllocate()) - .thenMany(conn.createStatement("SELECT * FROM test") + .thenMany(connection.createStatement("SELECT COUNT(0) FROM `test` WHERE `value` = ?") + .bind(0, value) .execute()) - .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) + .flatMap(result -> result.map((row, metadata) -> row.get(0, Integer.class))) .collectList() - .doOnNext(it -> assertThat(it).isEqualTo(Collections.singletonList(1)))); + .doOnNext(counts -> assertThat(counts).isEqualTo(Collections.singletonList(2)))); + } + + @DisabledIf("envIsLessThanMySql56") + @Test + void startTransaction() { + TransactionDefinition readOnlyConsistent = MySqlTransactionDefinition.mutability(false) + .consistent(); + TransactionDefinition readWriteConsistent = MySqlTransactionDefinition.mutability(true) + .consistent(); + + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.context().isInTransaction()) + .isFalse()) + .then(connection.beginTransaction(readOnlyConsistent)) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) + .then(connection.rollbackTransaction()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) + .then(connection.beginTransaction(readWriteConsistent)) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) + .then(connection.rollbackTransaction()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse())); + } + + @Test + void autoRollbackPreRelease() { + // Mock pool allocate/release. + complete(connection -> Mono.just(connection) + .cast(MySqlSimpleConnection.class) + .flatMap(conn -> connection.postAllocate() + .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .then(conn.beginTransaction()) + .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .single() + .doOnNext(it -> assertThat(it).isEqualTo(1)) + .doOnSuccess(ignored -> assertThat(conn.context().isInTransaction()).isTrue()) + .then(conn.preRelease()) + .doOnSuccess(ignored -> assertThat(conn.context().isInTransaction()).isFalse()) + .then(conn.postAllocate()) + .thenMany(conn.createStatement("SELECT * FROM test") + .execute()) + .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) + .count() + .doOnNext(it -> assertThat(it).isZero()))); + } + + @Test + void shouldNotRollbackCommittedPreRelease() { + // Mock pool allocate/release. + complete(connection -> Mono.just(connection) + .cast(MySqlSimpleConnection.class) + .flatMap(conn -> conn.postAllocate() + .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .then(conn.beginTransaction()) + .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .single() + .doOnNext(it -> assertThat(it).isEqualTo(1)) + .then(conn.commitTransaction()) + .then(conn.preRelease()) + .doOnSuccess(ignored -> assertThat(conn.context().isInTransaction()).isFalse()) + .then(conn.postAllocate()) + .thenMany(conn.createStatement("SELECT * FROM test") + .execute()) + .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) + .collectList() + .doOnNext(it -> assertThat(it).isEqualTo(Collections.singletonList(1))))); } @Test void transactionDefinitionLockWaitTimeout() { - complete(connection -> connection.beginTransaction(MySqlTransactionDefinition.builder() - .lockWaitTimeout(Duration.ofSeconds(345)) - .build()) + castedComplete(connection -> connection + .beginTransaction(MySqlTransactionDefinition.empty() + .lockWaitTimeout(Duration.ofSeconds(345))) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isTrue(); + assertThat(connection.context().isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isTrue(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isTrue(); }) .then(connection.rollbackTransaction()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isFalse(); + assertThat(connection.context().isInTransaction()).isFalse(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); })); } @Test void transactionDefinitionIsolationLevel() { - complete(connection -> connection.beginTransaction(MySqlTransactionDefinition.builder() - .isolationLevel(READ_COMMITTED) - .build()) + castedComplete(connection -> connection + .beginTransaction(MySqlTransactionDefinition.from(READ_COMMITTED)) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isTrue(); + assertThat(connection.context().isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(READ_COMMITTED); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); }) .then(connection.rollbackTransaction()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isFalse(); + assertThat(connection.context().isInTransaction()).isFalse(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); })); } @Test void setTransactionLevelNotInTransaction() { - complete(connection -> + castedComplete(connection -> // check initial session isolation level Mono.fromSupplier(connection::getTransactionIsolationLevel) .doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ)) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ)) .then(connection.rollbackTransaction()) @@ -174,7 +251,7 @@ void setTransactionLevelNotInTransaction() { .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED)) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) // ensure transaction isolation level applies to subsequent transactions .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED)) @@ -183,7 +260,7 @@ void setTransactionLevelNotInTransaction() { @Test void setTransactionLevelInTransaction() { - complete(connection -> + castedComplete(connection -> // check initial session transaction isolation level Mono.fromSupplier(connection::getTransactionIsolationLevel) .doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ)) @@ -193,34 +270,33 @@ void setTransactionLevelInTransaction() { .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isNotEqualTo(READ_COMMITTED)) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) // ensure that session isolation level is changed after rollback .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED)) // ensure transaction isolation level applies to subsequent transactions .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) ); } @Test void transactionDefinition() { // The WITH CONSISTENT SNAPSHOT phrase can only be used with the REPEATABLE READ isolation level. - complete(connection -> connection.beginTransaction(MySqlTransactionDefinition.builder() + castedComplete(connection -> connection + .beginTransaction(MySqlTransactionDefinition.from(REPEATABLE_READ) .lockWaitTimeout(Duration.ofSeconds(112)) - .isolationLevel(REPEATABLE_READ) - .withConsistentSnapshot(true) - .build()) + .consistent()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isTrue(); + assertThat(connection.context().isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isTrue(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isTrue(); }) .then(connection.rollbackTransaction()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isFalse(); + assertThat(connection.context().isInTransaction()).isFalse(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); })); } @@ -236,33 +312,33 @@ void setAutoCommit() { @Test void autoCommitAutomaticallyTurnedOffInTransaction() { complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isAutoCommit()).isTrue()) - .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.commitTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); + .then(connection.beginTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.commitTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); } @Test void autoCommitStatusIsRestoredAfterTransaction() { complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isAutoCommit()).isTrue()) - .then(connection.setAutoCommit(false)) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.commitTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.setAutoCommit(true)) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); + .then(connection.setAutoCommit(false)) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.beginTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.commitTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.setAutoCommit(true)) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); } @ParameterizedTest @ValueSource(strings = { "test", "save`point" }) void createSavepointAndRollbackToSavepoint(String savepoint) { - complete(connection -> Mono.from(connection.createStatement( + castedComplete(connection -> Mono.from(connection.createStatement( "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) .flatMap(IntegrationTestSupport::extractRowsUpdated) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (1, 'test1')") .execute())) .flatMap(IntegrationTestSupport::extractRowsUpdated) @@ -273,7 +349,7 @@ void createSavepointAndRollbackToSavepoint(String savepoint) { .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(2)) .then(connection.createSavepoint(savepoint)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (3, 'test3')") .execute())) .flatMap(IntegrationTestSupport::extractRowsUpdated) @@ -284,12 +360,12 @@ void createSavepointAndRollbackToSavepoint(String savepoint) { .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(4)) .then(connection.rollbackTransactionToSavepoint(savepoint)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(2)) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(0)) @@ -299,36 +375,36 @@ void createSavepointAndRollbackToSavepoint(String savepoint) { @ParameterizedTest @ValueSource(strings = { "test", "save`point" }) void createSavepointAndRollbackEntireTransaction(String savepoint) { - complete(connection -> Mono.from(connection.createStatement( - "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (1, 'test1')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (2, 'test2')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) - .doOnSuccess(count -> assertThat(count).isEqualTo(2)) - .then(connection.createSavepoint(savepoint)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (3, 'test3')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (4, 'test4')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) - .doOnSuccess(count -> assertThat(count).isEqualTo(4)) - .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) - .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) - .doOnSuccess(count -> assertThat(count).isEqualTo(0)) + castedComplete(connection -> Mono.from(connection.createStatement( + "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(connection.beginTransaction()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (1, 'test1')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (2, 'test2')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) + .doOnSuccess(count -> assertThat(count).isEqualTo(2)) + .then(connection.createSavepoint(savepoint)) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (3, 'test3')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (4, 'test4')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) + .doOnSuccess(count -> assertThat(count).isEqualTo(4)) + .then(connection.rollbackTransaction()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) + .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) + .doOnSuccess(count -> assertThat(count).isEqualTo(0)) ); } @@ -346,79 +422,78 @@ void rollbackTransactionWithoutBegin() { void setTransactionIsolationLevel() { complete(connection -> Flux.just(READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE) .concatMap(level -> connection.setTransactionIsolationLevel(level) - .map(ignored -> assertThat(level)) - .doOnNext(a -> a.isEqualTo(connection.getTransactionIsolationLevel())))); + .doOnSuccess(ignored -> assertThat(level).isEqualTo(connection.getTransactionIsolationLevel())))); } @Test void errorPropagteRequestQueue() { illegalArgument(connection -> Flux.merge( - connection.createStatement("SELECT 'Result 1', SLEEP(1)").execute(), - connection.createStatement("SELECT 'Result 2'").execute(), - connection.createStatement("SELECT 'Result 3'").execute() - ).flatMap(result -> result.map((row, meta) -> row.get(0, Integer.class))) + connection.createStatement("SELECT 'Result 1', SLEEP(1)").execute(), + connection.createStatement("SELECT 'Result 2'").execute(), + connection.createStatement("SELECT 'Result 3'").execute() + ).flatMap(result -> result.map((row, meta) -> row.get(0, Integer.class))) ); } @Test void commitTransactionShouldRespectQueuedMessages() { final String tdl = "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))"; - complete(connection -> - Mono.from(connection.createStatement(tdl).execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .thenMany(Flux.merge( - connection.beginTransaction(), - connection.createStatement("INSERT INTO test VALUES (1, 'test1')") - .execute(), - connection.commitTransaction() - )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) - .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) - .flatMap(result -> - Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) - ) - .doOnNext(text -> assertThat(text).isEqualTo(1L)) + castedComplete(connection -> + Mono.from(connection.createStatement(tdl).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(Flux.merge( + connection.beginTransaction(), + connection.createStatement("INSERT INTO test VALUES (1, 'test1')") + .execute(), + connection.commitTransaction() + )) + .doOnComplete(() -> assertThat(connection.context().isInTransaction()).isFalse()) + .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) + .flatMap(result -> + Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) + ) + .doOnNext(text -> assertThat(text).isEqualTo(1L)) ); } @Test void rollbackTransactionShouldRespectQueuedMessages() { final String tdl = "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))"; - complete(connection -> - Mono.from(connection.createStatement(tdl).execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .thenMany(Flux.merge( - connection.beginTransaction(), - connection.createStatement("INSERT INTO test VALUES (1, 'test1')") - .execute(), - connection.rollbackTransaction() - )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) - .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) - .doOnNext(count -> assertThat(count).isEqualTo(0L))) + castedComplete(connection -> + Mono.from(connection.createStatement(tdl).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(Flux.merge( + connection.beginTransaction(), + connection.createStatement("INSERT INTO test VALUES (1, 'test1')") + .execute(), + connection.rollbackTransaction() + )) + .doOnComplete(() -> assertThat(connection.context().isInTransaction()).isFalse()) + .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) + .doOnNext(count -> assertThat(count).isEqualTo(0L))) ); } @Test void beginTransactionShouldRespectQueuedMessages() { final String tdl = "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))"; - complete(connection -> - Mono.from(connection.createStatement(tdl).execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.beginTransaction())) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) - .thenMany(Flux.merge( - connection.createStatement("INSERT INTO test VALUES (1, 'test1')").execute(), - connection.commitTransaction(), - connection.beginTransaction() - )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isTrue()) - .then(Mono.from(connection.rollbackTransaction())) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) - .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) - .doOnNext(count -> assertThat(count).isEqualTo(1L))) + castedComplete(connection -> + Mono.from(connection.createStatement(tdl).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.beginTransaction())) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) + .thenMany(Flux.merge( + connection.createStatement("INSERT INTO test VALUES (1, 'test1')").execute(), + connection.commitTransaction(), + connection.beginTransaction() + )) + .doOnComplete(() -> assertThat(connection.context().isInTransaction()).isTrue()) + .then(Mono.from(connection.rollbackTransaction())) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) + .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) + .doOnNext(count -> assertThat(count).isEqualTo(1L))) ); } @@ -504,6 +579,44 @@ void loadDataLocalInfile(String name) throws URISyntaxException, IOException { .doOnNext(it -> assertThat(it).isEqualTo(json))); } + @Test + public void tinyInt1isBitTrueTestValue1() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Boolean.class)) + .doOnNext(value -> assertThat(value).isEqualTo(true)) + ); + } + + @Test + public void tinyInt1isBitTrueTestUnsignedTinyInt1isNotBoolean() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1) UNSIGNED)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Short.class)) + .doOnNext(value -> assertThat(value).isEqualTo(Short.valueOf((short)1))) + ); + } + + @Test + public void tinyInt1isBitTrueTestValue0() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 0)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Boolean.class)) + .doOnNext(value -> assertThat(value).isEqualTo(false))); + } + @Test void batchCrud() { // TODO: spilt it to multiple test cases and move it to BatchIntegrationTest @@ -570,6 +683,10 @@ void batchCrud() { }); } + private void castedComplete(Function> runner) { + complete(conn -> runner.apply((MySqlSimpleConnection) conn)); + } + private static String formattedSelect(String condition) { if (condition.isEmpty()) { return "SELECT id,value FROM test ORDER BY id"; diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/DateTimeIntegrationTestSupport.java similarity index 91% rename from src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/DateTimeIntegrationTestSupport.java index 5bfb769f1..891c4cb41 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/DateTimeIntegrationTestSupport.java @@ -16,8 +16,10 @@ package io.asyncer.r2dbc.mysql; -import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; import reactor.core.publisher.Flux; import java.time.Instant; @@ -31,14 +33,15 @@ import java.util.Arrays; import java.util.Collections; import java.util.TimeZone; -import java.util.function.Predicate; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; /** - * Base class considers integration tests for time zone conversion. + * Base class considers integration tests for date times. */ -abstract class TimeZoneIntegrationTestSupport extends IntegrationTestSupport { +@Isolated +abstract class DateTimeIntegrationTestSupport extends IntegrationTestSupport { private static final String TIMESTAMP_TABLE = "CREATE TEMPORARY TABLE test " + "(id INT PRIMARY KEY AUTO_INCREMENT, value TIMESTAMP)"; @@ -57,7 +60,11 @@ abstract class TimeZoneIntegrationTestSupport extends IntegrationTestSupport { private static final ZoneId SERVER_ZONE = ZoneId.of("America/New_York"); - static { + private static TimeZone defaultTimeZone; + + @BeforeAll + static void setUpTimeZone() { + defaultTimeZone = TimeZone.getDefault(); TimeZone.setDefault(TimeZone.getTimeZone("GMT+6")); // Make sure test cases contains daylight. @@ -65,8 +72,15 @@ abstract class TimeZoneIntegrationTestSupport extends IntegrationTestSupport { .isEqualTo(DST.atZone(SERVER_ZONE).plusHours(1)); } - TimeZoneIntegrationTestSupport(@Nullable Predicate preferPrepared) { - super(configuration("r2dbc", false, false, SERVER_ZONE, preferPrepared)); + @AfterAll + static void tearDownTimeZone() { + TimeZone.setDefault(defaultTimeZone); + } + + DateTimeIntegrationTestSupport( + Function customizer + ) { + super(configuration(builder -> customizer.apply(builder.connectionTimeZone(SERVER_ZONE.getId())))); } @Test @@ -127,8 +141,7 @@ void queryOffsetTime() { .bind(0, 0) .execute()) .flatMap(r -> r.map((row, meta) -> row.get(0, OffsetTime.class))) - .doOnNext(it -> assertThat(it.getOffset()) - .isEqualTo(SERVER_ZONE.getRules().getStandardOffset(Instant.EPOCH))) + .doOnNext(it -> assertThat(it.getOffset()).isEqualTo(ZoneId.systemDefault().normalized())) .map(OffsetTime::toLocalTime) .collectList() .doOnNext(it -> assertThat(it) @@ -207,8 +220,7 @@ void updateOffsetTime() { .bind(0, 0) .execute()) .flatMap(r -> r.map((row, meta) -> row.get(0, OffsetTime.class))) - .doOnNext(it -> assertThat(it.getOffset()) - .isEqualTo(SERVER_ZONE.getRules().getStandardOffset(Instant.EPOCH))) + .doOnNext(it -> assertThat(it.getOffset()).isEqualTo(ZoneId.systemDefault().normalized())) .map(it -> it.withOffsetSameInstant(ZoneId.systemDefault().getRules() .getStandardOffset(Instant.EPOCH)) .toLocalTime()) diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java similarity index 86% rename from src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java index afece8130..efdeac68d 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java @@ -1,5 +1,6 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import org.junit.jupiter.api.Test; import java.util.concurrent.ThreadLocalRandom; @@ -15,10 +16,7 @@ class InitDbIntegrationTest extends IntegrationTestSupport { private static final String DATABASE = "test-" + ThreadLocalRandom.current().nextInt(10000); InitDbIntegrationTest() { - super(configuration( - DATABASE, true, false, - null, null - )); + super(configuration(builder -> builder.database(DATABASE).createDatabaseIfNotExist(true))); } @Test diff --git a/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java similarity index 61% rename from src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java index 2489d5732..796ebc5c0 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java @@ -16,10 +16,13 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; import io.r2dbc.spi.R2dbcBadGrammarException; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Result; -import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.extension.ExtendWith; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,16 +33,15 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; -import java.time.ZoneId; import java.util.Objects; import java.util.function.Function; -import java.util.function.Predicate; import static org.assertj.core.api.Assertions.assertThat; /** * Base class considers connection factory and general function for integration tests. */ +@ExtendWith(TestContainerExtension.class) abstract class IntegrationTestSupport { private final MySqlConnectionFactory connectionFactory; @@ -64,7 +66,7 @@ void illegalArgument(Function> runner) { process(runner).expectError(IllegalArgumentException.class).verify(Duration.ofSeconds(3)); } - Mono create() { + Mono create() { return connectionFactory.create(); } @@ -82,14 +84,8 @@ static Mono extractRowsUpdated(Result result) { } static MySqlConnectionConfiguration configuration( - String database, boolean createDatabaseIfNotExist, boolean autodetectExtensions, - @Nullable ZoneId serverZoneId, @Nullable Predicate preferPrepared + Function customizer ) { - String password = System.getProperty("test.mysql.password"); - - assertThat(password).withFailMessage("Property test.mysql.password must exists and not be empty") - .isNotNull() - .isNotEmpty(); String localInfilePath; @@ -102,85 +98,47 @@ static MySqlConnectionConfiguration configuration( } MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() - .host("127.0.0.1") + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()) .connectTimeout(Duration.ofSeconds(3)) - .user("root") - .password(password) - .database(database) - .createDatabaseIfNotExist(createDatabaseIfNotExist) - .allowLoadLocalInfileInPath(localInfilePath) - .autodetectExtensions(autodetectExtensions); - - if (serverZoneId != null) { - builder.serverZoneId(serverZoneId); - } + .allowLoadLocalInfileInPath(localInfilePath); - if (preferPrepared == null) { - builder.useClientPrepareStatement(); - } else { - builder.useServerPrepareStatement(preferPrepared); - } - - return builder.build(); + return customizer.apply(builder).build(); } boolean envIsLessThanMySql56() { - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return true; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + if (TestServerUtil.isMariaDb()) { return false; } - + final ServerVersion ver = TestServerUtil.getServerVersion(); return ver.isLessThan(ServerVersion.create(5, 6, 0)); } - boolean envIsLessThanMySql57OrMariaDb102() { - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return true; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + boolean envIsLessThanMySql578OrMariaDb102() { + final ServerVersion ver = TestServerUtil.getServerVersion(); + if (TestServerUtil.isMariaDb()) { return ver.isLessThan(ServerVersion.create(10, 2, 0)); } - return ver.isLessThan(ServerVersion.create(5, 7, 0)); + return ver.isLessThan(ServerVersion.create(5, 7, 8)); } static boolean envIsMariaDb10_5_1() { - String type = System.getProperty("test.db.type"); - - if (!"mariadb".equalsIgnoreCase(type)) { + if (!TestServerUtil.isMariaDb()) { return false; } - ServerVersion ver = ServerVersion.parse(System.getProperty("test.mysql.version")); + final ServerVersion ver = TestServerUtil.getServerVersion(); return ver.isGreaterThanOrEqualTo(ServerVersion.create(10, 5, 1)); } boolean envIsLessThanMySql574OrMariaDb1011() { - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return true; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + final ServerVersion ver = TestServerUtil.getServerVersion(); + if (TestServerUtil.isMariaDb()) { return ver.isLessThan(ServerVersion.create(10, 1, 1)); } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java similarity index 94% rename from src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java index 79a851693..6fb0bce1a 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java @@ -18,12 +18,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.json.JacksonCodecRegistrar; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; -import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -64,7 +67,7 @@ void tearDown() { JacksonCodecRegistrar.tearDown(); } - @DisabledIf("envIsLessThanMySql57OrMariaDb102") + @DisabledIf("envIsLessThanMySql578OrMariaDb102") @Test void json() { create().flatMap(connection -> Mono.from(connection.createStatement(TDL).execute()) @@ -81,7 +84,7 @@ void json() { .verifyComplete(); } - private static Publisher insert(MySqlConnection connection) { + private static Flux insert(MySqlConnection connection) { MySqlStatement statement = connection.createStatement("INSERT INTO test VALUES (DEFAULT, ?)"); for (int i = 0; i < BARS.length; ++i) { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java similarity index 87% rename from src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java index 7205a82bf..f78c5f11b 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java @@ -22,7 +22,8 @@ class JacksonPrepareIntegrationTest extends JacksonIntegrationTestSupport { JacksonPrepareIntegrationTest() { - super(configuration("r2dbc", false, true, null, sql -> false)); + super(configuration(builder -> builder.autodetectExtensions(true) + .useServerPrepareStatement())); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java similarity index 91% rename from src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java index 6d666e520..0b114e033 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java @@ -22,6 +22,6 @@ class JacksonTextIntegrationTest extends JacksonIntegrationTestSupport { JacksonTextIntegrationTest() { - super(configuration("r2dbc", false, true, null, null)); + super(configuration(builder -> builder.autodetectExtensions(true))); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java similarity index 55% rename from src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java index 04c5fac2e..00d192de3 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java @@ -16,39 +16,46 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; import io.r2dbc.spi.Readable; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import java.time.Instant; +import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.util.function.Predicate; +import java.util.List; +import java.util.function.Function; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; /** * Base class considers integration tests for MariaDB. */ abstract class MariaDbIntegrationTestSupport extends IntegrationTestSupport { - MariaDbIntegrationTestSupport(@Nullable Predicate preferPrepared) { - super(configuration("r2dbc", false, false, null, preferPrepared)); + MariaDbIntegrationTestSupport( + Function customizer + ) { + super(configuration(customizer)); } @Test void returningExpression() { complete(conn -> conn.createStatement("CREATE TEMPORARY TABLE test (" + - "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") .execute() .flatMap(IntegrationTestSupport::extractRowsUpdated) .thenMany(conn.createStatement("INSERT INTO test(value) VALUES (?)") .bind(0, 2) - .returnGeneratedValues("CURRENT_TIMESTAMP") + .returnGeneratedValues("POW(value, 4)") .execute()) - .flatMap(result -> result.map(r -> r.get(0, ZonedDateTime.class))) + .flatMap(result -> result.map(r -> r.get(0, Integer.class))) .collectList() - .doOnNext(list -> assertThat(list).hasSize(1) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10))))); + .doOnNext(list -> assertThat(list).hasSize(1)) + .doOnNext(list -> assertThat(list.get(0)).isEqualTo(16))); } @Test @@ -69,13 +76,9 @@ void allReturning() { .execute()) .flatMap(result -> result.map(DataEntity::read)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsExactly(2, 4, 6, 8, 10)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getId).distinct()).hasSize(5)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10)))) - .thenMany(conn.createStatement("REPLACE test(id, value) VALUES (1,?),(2,?),(3,?),(4,?),(5,?)") + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithSelectAll(conn, list)) + .thenMany(conn.createStatement("REPLACE test(id,value) VALUES (1,?),(2,?),(3,?),(4,?),(5,?)") .bind(0, 3) .bind(1, 5) .bind(2, 7) @@ -85,11 +88,8 @@ void allReturning() { .execute()) .flatMap(result -> result.map(DataEntity::read)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsExactly(3, 5, 7, 9, 11)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10))))); + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithSelectAll(conn, list))); } @Test @@ -106,38 +106,30 @@ void partialReturning() { .bind(2, 6) .bind(3, 8) .bind(4, 10) - .returnGeneratedValues("id", "created_at") + .returnGeneratedValues("id", "value") .execute()) - .flatMap(result -> result.map(DataEntity::withoutValue)) + .flatMap(result -> result.map(DataEntity::withoutCreatedAt)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsOnly(0)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getId).distinct()).hasSize(5)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10)))) + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithoutCreatedAt(conn, list)) .thenMany(conn.createStatement("REPLACE test(id, value) VALUES (1,?),(2,?),(3,?),(4,?),(5,?)") .bind(0, 3) .bind(1, 5) .bind(2, 7) .bind(3, 9) .bind(4, 11) - .returnGeneratedValues("id", "created_at") + .returnGeneratedValues("id", "value") .execute()) - .flatMap(result -> result.map(DataEntity::withoutValue)) + .flatMap(result -> result.map(DataEntity::withoutCreatedAt)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsOnly(0)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10)))) - ); + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithoutCreatedAt(conn, list))); } @Test void returningGetRowUpdated() { complete(conn -> conn.createStatement("CREATE TEMPORARY TABLE test(" + - "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") .execute() .flatMap(IntegrationTestSupport::extractRowsUpdated) .thenMany(conn.createStatement("INSERT INTO test(value) VALUES (?),(?)") @@ -149,6 +141,36 @@ void returningGetRowUpdated() { .doOnNext(it -> assertThat(it).isEqualTo(2))); } + private static Mono assertWithSelectAll(MySqlConnection conn, Mono> returning) { + return returning.zipWhen(list -> conn.createStatement("SELECT * FROM test WHERE id IN (?,?,?,?,?)") + .bind(0, list.get(0).getId()) + .bind(1, list.get(1).getId()) + .bind(2, list.get(2).getId()) + .bind(3, list.get(3).getId()) + .bind(4, list.get(4).getId()) + .execute() + .flatMap(result -> result.map(DataEntity::read)) + .collectList()) + .doOnNext(list -> assertThat(list.getT1()).isEqualTo(list.getT2())) + .then(); + } + + private static Mono assertWithoutCreatedAt(MySqlConnection conn, Mono> returning) { + String sql = "SELECT id,value FROM test WHERE id IN (?,?,?,?,?)"; + + return returning.zipWhen(list -> conn.createStatement(sql) + .bind(0, list.get(0).getId()) + .bind(1, list.get(1).getId()) + .bind(2, list.get(2).getId()) + .bind(3, list.get(3).getId()) + .bind(4, list.get(4).getId()) + .execute() + .flatMap(result -> result.map(DataEntity::withoutCreatedAt)) + .collectList()) + .doOnNext(list -> assertThat(list.getT1()).isEqualTo(list.getT2())) + .then(); + } + private static final class DataEntity { private final int id; @@ -175,6 +197,38 @@ ZonedDateTime getCreatedAt() { return createdAt; } + DataEntity incremented() { + return new DataEntity(id, value + 1, createdAt); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DataEntity)) { + return false; + } + + DataEntity that = (DataEntity) o; + + return id == that.id && value == that.value && createdAt.equals(that.createdAt); + } + + @Override + public int hashCode() { + int result = 31 * id + value; + return 31 * result + createdAt.hashCode(); + } + + @Override + public String toString() { + return "DataEntity{id=" + id + + ", value=" + value + + ", createdAt=" + createdAt + + '}'; + } + static DataEntity read(Readable readable) { Integer id = readable.get("id", Integer.TYPE); Integer value = readable.get("value", Integer.class); @@ -187,14 +241,14 @@ static DataEntity read(Readable readable) { return new DataEntity(id, value, createdAt); } - static DataEntity withoutValue(Readable readable) { + static DataEntity withoutCreatedAt(Readable readable) { Integer id = readable.get("id", Integer.TYPE); - ZonedDateTime createdAt = readable.get("created_at", ZonedDateTime.class); + Integer value = readable.get("value", Integer.TYPE); requireNonNull(id, "id must not be null"); - requireNonNull(createdAt, "createdAt must not be null"); + requireNonNull(value, "value must not be null"); - return new DataEntity(id, 0, createdAt); + return new DataEntity(id, value, ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)); } } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java similarity index 92% rename from src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java index b7ac81a8b..8f7ba2998 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java @@ -25,6 +25,6 @@ class MariaDbPrepareIntegrationTest extends MariaDbIntegrationTestSupport { MariaDbPrepareIntegrationTest() { - super(sql -> true); + super(builder -> builder.useServerPrepareStatement(sql -> true)); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java similarity index 96% rename from src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java index 0ab886c5f..fc285ddb3 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java @@ -25,6 +25,6 @@ class MariaDbTextIntegrationTest extends MariaDbIntegrationTestSupport { MariaDbTextIntegrationTest() { - super(null); + super(builder -> builder); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java similarity index 93% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java index 2eaab1e9b..ef764b18d 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java @@ -29,8 +29,7 @@ */ class MySqlBatchingBatchTest { - private final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class), - ConnectionContextTest.mock()); + private final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class)); @Test void add() { @@ -62,8 +61,7 @@ void badAdd() { @Test void addNothing() { - final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class), - ConnectionContextTest.mock()); + final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class)); assertEquals(batch.getSql(), ""); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java similarity index 86% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java index aa3067a1e..e62fea190 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java @@ -16,11 +16,14 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.constant.TlsVersions; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; import io.asyncer.r2dbc.mysql.extension.Extension; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.resolver.AddressResolverGroup; +import io.netty.resolver.DefaultAddressResolverGroup; import org.assertj.core.api.ObjectAssert; import org.assertj.core.api.ThrowableTypeAssert; import org.jetbrains.annotations.Nullable; @@ -206,6 +209,32 @@ void validPasswordSupplier() { .verifyComplete(); } + @Test + void validResolver() { + final AddressResolverGroup resolver = DefaultAddressResolverGroup.INSTANCE; + AddressResolverGroup resolverGroup = MySqlConnectionConfiguration.builder() + .host(HOST) + .user(USER) + .resolver(resolver) + .autodetectExtensions(false) + .build() + .getResolver(); + assertThat(resolverGroup).isSameAs(resolver); + } + + @Test + void invalidMetrics() { + // throw exception when metrics true without micrometer-core dependency + assertThatIllegalArgumentException().isThrownBy(() -> + MySqlConnectionConfiguration + .builder() + .host(HOST) + .user(USER) + .metrics(true) + .build() + ); + } + private static MySqlConnectionConfiguration unixSocketSslMode(SslMode sslMode) { return MySqlConnectionConfiguration.builder() .unixSocket(UNIX_SOCKET) @@ -240,11 +269,18 @@ private static MySqlConnectionConfiguration filledUp() { .sslKey("/path/to/mysql/client-key.pem") .sslKeyPassword("pem-password-in-here") .tlsVersion(TlsVersions.TLS1_1, TlsVersions.TLS1_2, TlsVersions.TLS1_3) - .serverZoneId(ZoneId.systemDefault()) + .compressionAlgorithms(CompressionAlgorithm.ZSTD, CompressionAlgorithm.ZLIB, + CompressionAlgorithm.UNCOMPRESSED) + .preserveInstants(true) + .connectionTimeZone("LOCAL") + .forceConnectionTimeZoneToSession(true) .zeroDateOption(ZeroDateOption.USE_NULL) .sslHostnameVerifier((host, s) -> true) .queryCacheSize(128) .prepareCacheSize(0) + .sessionVariables("sql_mode=ANSI_QUOTES") + .lockWaitTimeout(Duration.ofSeconds(5)) + .statementTimeout(Duration.ofSeconds(10)) .autodetectExtensions(false) .build(); } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java similarity index 70% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index 17f515530..be48a2255 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -16,28 +16,44 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.resolver.AddressResolverGroup; +import io.netty.resolver.DefaultAddressResolverGroup; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Option; import org.assertj.core.api.Assert; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.URLEncoder; import java.time.Duration; import java.time.ZoneId; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.METRICS; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.PASSWORD_PUBLISHER; +import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.RESOLVER; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.USE_SERVER_PREPARE_STATEMENT; import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT; import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; @@ -130,7 +146,7 @@ void validProgrammaticHost() { .option(SSL, true) .option(Option.valueOf(CONNECT_TIMEOUT.name()), Duration.ofSeconds(3).toString()) .option(DATABASE, "r2dbc") - .option(Option.valueOf("serverZoneId"), "Asia/Tokyo") + .option(Option.valueOf("connectionTimeZone"), "Asia/Tokyo") .option(Option.valueOf("useServerPrepareStatement"), AllTruePredicate.class.getName()) .option(Option.valueOf("zeroDate"), "use_round") .option(Option.valueOf("sslMode"), "verify_identity") @@ -159,7 +175,7 @@ void validProgrammaticHost() { assertThat(configuration.getZeroDateOption()).isEqualTo(ZeroDateOption.USE_ROUND); assertThat(configuration.isTcpKeepAlive()).isTrue(); assertThat(configuration.isTcpNoDelay()).isTrue(); - assertThat(configuration.getServerZoneId()).isEqualTo(ZoneId.of("Asia/Tokyo")); + assertThat(configuration.getConnectionTimeZone()).isEqualTo("Asia/Tokyo"); assertThat(configuration.getPreferPrepareStatement()).isExactlyInstanceOf(AllTruePredicate.class); assertThat(configuration.getExtensions()).isEqualTo(Extensions.from(Collections.emptyList(), true)); @@ -177,8 +193,8 @@ void validProgrammaticHost() { @Test void invalidProgrammatic() { - assertThatIllegalStateException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalStateException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(PORT, 3307) .option(USER, "root") @@ -198,8 +214,8 @@ void invalidProgrammatic() { .build())) .withMessageContaining("host"); - assertThatIllegalStateException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalStateException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(HOST, "127.0.0.1") .option(PORT, 3307) @@ -207,8 +223,8 @@ void invalidProgrammatic() { .build())) .withMessageContaining("user"); - assertThatIllegalArgumentException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalArgumentException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(HOST, "127.0.0.1") .option(PORT, 3307) @@ -220,8 +236,8 @@ void invalidProgrammatic() { .build())) .withMessageContaining("sslCert and sslKey"); - assertThatIllegalArgumentException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalArgumentException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(HOST, "127.0.0.1") .option(PORT, 3307) @@ -276,7 +292,7 @@ void validProgrammaticUnixSocket() { .option(Option.valueOf(CONNECT_TIMEOUT.name()), Duration.ofSeconds(3).toString()) .option(DATABASE, "r2dbc") .option(Option.valueOf("createDatabaseIfNotExist"), true) - .option(Option.valueOf("serverZoneId"), "Asia/Tokyo") + .option(Option.valueOf("connectionTimeZone"), "Asia/Tokyo") .option(Option.valueOf("useServerPrepareStatement"), AllTruePredicate.class.getName()) .option(Option.valueOf("zeroDate"), "use_round") .option(Option.valueOf("sslMode"), "verify_identity") @@ -302,7 +318,7 @@ void validProgrammaticUnixSocket() { assertThat(configuration.getZeroDateOption()).isEqualTo(ZeroDateOption.USE_ROUND); assertThat(configuration.isTcpKeepAlive()).isTrue(); assertThat(configuration.isTcpNoDelay()).isTrue(); - assertThat(configuration.getServerZoneId()).isEqualTo(ZoneId.of("Asia/Tokyo")); + assertThat(configuration.getConnectionTimeZone()).isEqualTo("Asia/Tokyo"); assertThat(configuration.getPreferPrepareStatement()).isExactlyInstanceOf(AllTruePredicate.class); assertThat(configuration.getExtensions()).isEqualTo(Extensions.from(Collections.emptyList(), true)); @@ -394,6 +410,40 @@ void invalidServerPreparing() { .build())); } + @ParameterizedTest + @ValueSource(strings = { + "uncompressed", + "zlib", + "zstd", + "zlib,uncompressed", + "zstd,uncompressed", + "zstd,zlib", + "zstd,zlib,uncompressed", + }) + void validCompressionAlgorithms(String name) { + Set algorithms = MySqlConnectionFactoryProvider.setup( + ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(Option.valueOf("compressionAlgorithms"), name) + .build()).getCompressionAlgorithms(); + + assertThat(algorithms).hasSize(name.split(",").length); + } + + @ParameterizedTest + @ValueSource(strings = { "", "gzip", "lz4", "lz4hc", "none", "snappy", "zlib,none", "zstd,none" }) + void invalidCompressionAlgorithms(String name) { + assertThatIllegalArgumentException().isThrownBy(() -> MySqlConnectionFactoryProvider.setup( + ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(Option.valueOf("compressionAlgorithms"), name) + .build())); + } + @Test void validPasswordSupplier() { final Publisher passwordSupplier = Mono.just("123456"); @@ -407,6 +457,128 @@ void validPasswordSupplier() { assertThat(ConnectionFactories.get(options)).isExactlyInstanceOf(MySqlConnectionFactory.class); } + @Test + void validResolver() { + final AddressResolverGroup resolver = DefaultAddressResolverGroup.INSTANCE; + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(RESOLVER, resolver) + .build(); + + assertThat(ConnectionFactories.get(options)).isExactlyInstanceOf(MySqlConnectionFactory.class); + } + + @Test + void invalidMetrics() { + // throw exception when metrics true without micrometer-core dependency + assertThatIllegalArgumentException().isThrownBy(() -> + ConnectionFactories.get(ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(METRICS, true) + .build())); + } + + @Test + void allConfigurationOptions() { + List exceptConfigs = Arrays.asList( + "extendWith", + "username", + "zeroDateOption"); + List exceptOptions = Arrays.asList( + "driver", + "ssl", + "protocol", + "zeroDate"); + Set allOptions = Stream.concat( + Arrays.stream(ConnectionFactoryOptions.class.getFields()), + Arrays.stream(MySqlConnectionFactoryProvider.class.getFields()) + ) + .filter(field -> Modifier.isStatic(field.getModifiers()) && field.getType() == Option.class) + .map(field -> { + try { + return ((Option) field.get(null)).name(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }) + .filter(name -> !exceptOptions.contains(name)) + .collect(Collectors.toSet()); + Set allBuilderOptions = Arrays.stream(MySqlConnectionConfiguration.Builder.class.getMethods()) + .filter(method -> method.getParameterCount() >= 1 && + method.getReturnType() == MySqlConnectionConfiguration.Builder.class && + !exceptConfigs.contains(method.getName())) + .map(Method::getName) + .collect(Collectors.toSet()); + + assertThat(allBuilderOptions).containsExactlyInAnyOrderElementsOf(allOptions); + } + + @ParameterizedTest + @MethodSource + void sessionVariables(String input, List expected) { + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(Option.valueOf("sessionVariables"), input) + .build(); + + assertThat(MySqlConnectionFactoryProvider.setup(options).getSessionVariables()).isEqualTo(expected); + } + + static Stream sessionVariables() { + return Stream.of( + Arguments.of("", Collections.emptyList()), + Arguments.of(" ", Collections.singletonList("")), + Arguments.of("a=b", Collections.singletonList("a=b")), + Arguments.of( + "sql_mode=ANSI_QUOTE,c=d;e=f", + Arrays.asList("sql_mode=ANSI_QUOTE", "c=d", "e=f")), + Arguments.of( + "sql_mode='ANSI_QUOTES,b=c,c=d';c=d,e=f", + Arrays.asList("sql_mode='ANSI_QUOTES,b=c,c=d'", "c=d", "e=f")), + Arguments.of( + "sql_mode=(ANSI_QUOTES,'b=c,c=d,max(');c=(d,e='f)');", + Arrays.asList("sql_mode=(ANSI_QUOTES,'b=c,c=d,max(')", "c=(d,e='f)')", "")), + Arguments.of( + "sql_mode=(ANSI_QUOTES,'b=c,c=d,max(');c=(d,e='f)'); ", + Arrays.asList("sql_mode=(ANSI_QUOTES,'b=c,c=d,max(')", "c=(d,e='f)')", "")), + Arguments.of( + "sql_mode=(ANSI_QUOTES,\"b=c',c=d,max(\");c=(d,'e=\"f)\");',)", + Arrays.asList("sql_mode=(ANSI_QUOTES,\"b=c',c=d,max(\")", "c=(d,'e=\"f)\");',)")), + Arguments.of( + "sql_mode=(((;),);)", + Collections.singletonList("sql_mode=(((;),);)")), + Arguments.of( + "sql_mode=(((';),););',);a=),);d=)", + Arrays.asList("sql_mode=(((';),););',);a=),)", "d=)")), + Arguments.of( + "sql_mode=((\"(';),)\";);',);)a=,)';),b=(();)", + Arrays.asList("sql_mode=((\"(';),)\";);',);)a=,)';)", "b=(();)")), + Arguments.of( + "sql_mode=((\"(';),)\";);',);)a=,)'b=;)\\,c=(();)", + Arrays.asList("sql_mode=((\"(';),)\";);',);)a=,)'b=;)\\", "c=(();)")), + Arguments.of( + "sql_mode='\\','", + Collections.singletonList("sql_mode='\\','")), + Arguments.of( + "sql_mode=\",\\\",'\\\\',',\"", + Collections.singletonList("sql_mode=\",\\\",'\\\\',',\"")), + Arguments.of( + "sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'," + + "transaction_isolation=(SELECT UPPER(`it's ``lvl```) FROM `lvl` WHERE `type` = 'r2dbc')" + + ",`foo``bar`='FOO,BAR'", + Arrays.asList( + "sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'", + "transaction_isolation=(SELECT UPPER(`it's ``lvl```) FROM `lvl` WHERE `type` = 'r2dbc')", + "`foo``bar`='FOO,BAR'" + )) + ); + } } final class MockException extends RuntimeException { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java similarity index 88% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java index 4a8878b92..08ed1fe28 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java @@ -22,7 +22,8 @@ class MySqlPrepareTestKit extends MySqlTestKitSupport { MySqlPrepareTestKit() { - super(IntegrationTestSupport.configuration("r2dbc", false, false, null, sql -> true)); + super(IntegrationTestSupport.configuration(builder -> + builder.useServerPrepareStatement(sql -> true))); } @Override diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java new file mode 100644 index 000000000..aceff07ad --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java @@ -0,0 +1,65 @@ +package io.asyncer.r2dbc.mysql; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.NoSuchElementException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + + +class MySqlRowDescriptorTest { + @ParameterizedTest + @MethodSource("arguments") + public void findColumnByNameTest(final String q, final int index, final String... names) { + + // given + final MySqlRowDescriptor metadata = create(names); + + // when + if (index == -1) { + assertThrows(NoSuchElementException.class, () -> metadata.getColumnMetadata(q)); + return; + } + final MySqlColumnDescriptor actual = metadata.getColumnMetadata(q); + + // then + assertEquals(index, actual.getIndex()); + } + + private static Stream arguments() { + return Stream.of( + // not found + Arguments.of("`alpha`", -1, + new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("omega", -1, + new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + // found + Arguments.of("alpha", 0, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Alpha", 0, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("beta", 1, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Beta", 1, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("delta", 3, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Delta", 3, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("gamma", 4, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Gamma", 4, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }) + ); + } + + private static MySqlRowDescriptor create(final String... names) { + MySqlColumnDescriptor[] metadata = new MySqlColumnDescriptor[names.length]; + for (int i = 0; i < names.length; ++i) { + metadata[i] = + new MySqlColumnDescriptor(i, (short) 0, names[i], 0, 0, 0, 1); + } + return new MySqlRowDescriptor(metadata); + } + + +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java new file mode 100644 index 000000000..b2847c20d --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java @@ -0,0 +1,261 @@ +/* + * Copyright 2023 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; +import io.asyncer.r2dbc.mysql.cache.Caches; +import io.asyncer.r2dbc.mysql.cache.PrepareCache; +import io.asyncer.r2dbc.mysql.client.Client; +import io.asyncer.r2dbc.mysql.client.FluxExchangeable; +import io.asyncer.r2dbc.mysql.codec.Codecs; +import io.asyncer.r2dbc.mysql.constant.ServerStatuses; +import io.asyncer.r2dbc.mysql.message.client.ClientMessage; +import io.asyncer.r2dbc.mysql.message.client.TextQueryMessage; +import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; +import io.r2dbc.spi.IsolationLevel; +import org.assertj.core.api.ThrowableTypeAssert; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link MySqlSimpleConnection}. + */ +class MySqlSimpleConnectionTest { + + private static final Codecs CODECS = mock(Codecs.class); + + @Test + void createStatement() { + String condition = "SELECT * FROM test"; + MySqlSimpleConnection allPrepare = new MySqlSimpleConnection( + mockClient(), + CODECS, + Caches.createQueryCache(0), sql -> true); + MySqlSimpleConnection halfPrepare = new MySqlSimpleConnection( + mockClient(), + CODECS, + Caches.createQueryCache(0), sql -> false); + MySqlSimpleConnection conditionPrepare = new MySqlSimpleConnection( + mockClient(), + CODECS, + Caches.createQueryCache(0), sql -> sql.equals(condition)); + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); + + assertThat(noPrepare.createStatement("SELECT * FROM test WHERE id=1")) + .isExactlyInstanceOf(TextSimpleStatement.class); + assertThat(noPrepare.createStatement(condition)) + .isExactlyInstanceOf(TextSimpleStatement.class); + assertThat(noPrepare.createStatement("SELECT * FROM test WHERE id=?")) + .isExactlyInstanceOf(TextParameterizedStatement.class); + + assertThat(allPrepare.createStatement("SELECT * FROM test WHERE id=1")) + .isExactlyInstanceOf(PrepareSimpleStatement.class); + assertThat(allPrepare.createStatement(condition)) + .isExactlyInstanceOf(PrepareSimpleStatement.class); + assertThat(allPrepare.createStatement("SELECT * FROM test WHERE id=?")) + .isExactlyInstanceOf(PrepareParameterizedStatement.class); + + assertThat(halfPrepare.createStatement("SELECT * FROM test WHERE id=1")) + .isExactlyInstanceOf(TextSimpleStatement.class); + assertThat(halfPrepare.createStatement(condition)) + .isExactlyInstanceOf(TextSimpleStatement.class); + assertThat(halfPrepare.createStatement("SELECT * FROM test WHERE id=?")) + .isExactlyInstanceOf(PrepareParameterizedStatement.class); + + assertThat(conditionPrepare.createStatement("SELECT * FROM test WHERE id=1")) + .isExactlyInstanceOf(TextSimpleStatement.class); + assertThat(conditionPrepare.createStatement(condition)) + .isExactlyInstanceOf(PrepareSimpleStatement.class); + assertThat(conditionPrepare.createStatement("SELECT * FROM test WHERE id=?")) + .isExactlyInstanceOf(PrepareParameterizedStatement.class); + } + + @SuppressWarnings("ConstantConditions") + @Test + void badCreateStatement() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); + assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.createStatement(null)); + } + + @SuppressWarnings("ConstantConditions") + @Test + void badCreateSavepoint() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); + ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); + + asserted.isThrownBy(() -> noPrepare.createSavepoint("")); + asserted.isThrownBy(() -> noPrepare.createSavepoint(null)); + } + + @SuppressWarnings("ConstantConditions") + @Test + void badReleaseSavepoint() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); + ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); + + asserted.isThrownBy(() -> noPrepare.releaseSavepoint("")); + asserted.isThrownBy(() -> noPrepare.releaseSavepoint(null)); + } + + @SuppressWarnings("ConstantConditions") + @Test + void badRollbackTransactionToSavepoint() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); + ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); + + asserted.isThrownBy(() -> noPrepare.rollbackTransactionToSavepoint("")); + asserted.isThrownBy(() -> noPrepare.rollbackTransactionToSavepoint(null)); + } + + @SuppressWarnings("ConstantConditions") + @Test + void badSetTransactionIsolationLevel() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); + assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.setTransactionIsolationLevel(null)); + } + + @ParameterizedTest + @ValueSource(strings = { "READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SERIALIZABLE" }) + void shouldSetTransactionIsolationLevelSuccessfully(String levelSql) { + Client client = mockClient(); + IsolationLevel level = IsolationLevel.valueOf(levelSql); + ClientMessage message = new TextQueryMessage("SET SESSION TRANSACTION ISOLATION LEVEL " + levelSql); + + when(client.exchange(eq(message), any())).thenReturn(Flux.empty()); + + MySqlSimpleConnection noPrepare = newNoPrepare(client); + noPrepare.setTransactionIsolationLevel(level) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(level); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(level); + } + + @ParameterizedTest + @ValueSource(strings = { + "READ UNCOMMITTED,SERIALIZABLE", + "READ COMMITTED,REPEATABLE READ", + "REPEATABLE READ,READ UNCOMMITTED" + }) + void shouldSetTransactionIsolationLevelInTransaction(String levels) { + String[] levelStatements = levels.split(","); + IsolationLevel currentLevel = IsolationLevel.valueOf(levelStatements[0]); + IsolationLevel sessionLevel = IsolationLevel.valueOf(levelStatements[1]); + Client client = mockClient(); + ClientMessage session = new TextQueryMessage("SET SESSION TRANSACTION ISOLATION LEVEL " + sessionLevel.asSql()); + CompleteMessage mockDone = mock(CompleteMessage.class); + @SuppressWarnings("unchecked") + SynchronousSink sink = (SynchronousSink) mock(SynchronousSink.class); + AtomicBoolean completed = new AtomicBoolean(false); + + doAnswer(it -> { + throw it.getArgument(0, Exception.class); + }).when(sink).error(any()); + doAnswer(it -> { + completed.set(true); + return null; + }).when(sink).complete(); + when(mockDone.isDone()).thenReturn(true); + when(client.exchange(eq(session), any())).thenReturn(Flux.empty()); + when(client.exchange(any())).thenAnswer(it -> { + FluxExchangeable exchangeable = it.getArgument(0); + @SuppressWarnings("unchecked") + CoreSubscriber subscriber = mock(CoreSubscriber.class); + exchangeable.subscribe(subscriber); + + while (!completed.get()) { + exchangeable.accept(mockDone, sink); + } + + // Mock server status to be in transaction + client.getContext().setServerStatuses(ServerStatuses.IN_TRANSACTION); + + return Flux.empty(); + }); + + IsolationLevel mockLevel = IsolationLevel.valueOf("DEFAULT"); + client.getContext().initSession( + mock(PrepareCache.class), + mockLevel, + false, + Duration.ZERO, + null, + null + ); + MySqlSimpleConnection noPrepare = newNoPrepare(client); + + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(mockLevel); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(mockLevel); + + noPrepare.beginTransaction(MySqlTransactionDefinition.from(currentLevel)) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(currentLevel); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(mockLevel); + + noPrepare.setTransactionIsolationLevel(sessionLevel) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(currentLevel); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(sessionLevel); + } + + @SuppressWarnings("ConstantConditions") + @Test + void badValidate() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); + assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.validate(null)); + } + + private static Client mockClient() { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock()); + + return client; + } + + private static MySqlSimpleConnection newNoPrepare(Client client) { + return new MySqlSimpleConnection( + client, + CODECS, + Caches.createQueryCache(0), + null + ); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java similarity index 93% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java index c381c8418..ebcb4589e 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java @@ -28,8 +28,7 @@ */ class MySqlSyntheticBatchTest { - private final MySqlSyntheticBatch batch = new MySqlSyntheticBatch(mock(Client.class), mock(Codecs.class), - ConnectionContextTest.mock()); + private final MySqlSyntheticBatch batch = new MySqlSyntheticBatch(mock(Client.class), mock(Codecs.class)); @SuppressWarnings("ConstantConditions") @Test diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java similarity index 85% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java index 635d94921..e1760cef6 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java @@ -17,17 +17,18 @@ package io.asyncer.r2dbc.mysql; import com.zaxxer.hikari.HikariDataSource; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; import io.r2dbc.spi.test.TestKit; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.jdbc.core.JdbcTemplate; import java.time.Duration; -import java.time.ZoneId; import java.util.Optional; -import java.util.TimeZone; /** * Base class considers integration tests of {@link TestKit}. */ +@ExtendWith(TestContainerExtension.class) abstract class MySqlTestKitSupport implements TestKit { private final MySqlConnectionFactory connectionFactory; @@ -88,11 +89,10 @@ private static JdbcTemplate jdbc(MySqlConnectionConfiguration configuration) { source.setConnectionTimeout(Optional.ofNullable(configuration.getConnectTimeout()) .map(Duration::toMillis).orElse(0L)); - ZoneId zoneId = configuration.getServerZoneId(); - - if (zoneId != null) { - source.addDataSourceProperty("serverTimezone", TimeZone.getTimeZone(zoneId).getID()); - } + source.addDataSourceProperty("preserveInstants", configuration.isPreserveInstants()); + source.addDataSourceProperty("connectionTimeZone", configuration.getConnectionTimeZone()); + source.addDataSourceProperty("forceConnectionTimeZoneToSession", + configuration.isForceConnectionTimeZoneToSession()); return new JdbcTemplate(source); } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java similarity index 90% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java index 04c32c719..68fe276fb 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java @@ -22,6 +22,6 @@ class MySqlTextTestKit extends MySqlTestKitSupport { MySqlTextTestKit() { - super(IntegrationTestSupport.configuration("r2dbc", false, false, null, null)); + super(IntegrationTestSupport.configuration(builder -> builder)); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadataTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadataTest.java new file mode 100644 index 000000000..0edff7805 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadataTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.collation.CharCollation; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link MySqlTypeMetadata}. + */ +class MySqlTypeMetadataTest { + + @Test + void allSet() { + MySqlTypeMetadata metadata = new MySqlTypeMetadata(0, -1, 0); + + assertThat(metadata.isBinary()).isTrue(); + assertThat(metadata.isSet()).isTrue(); + assertThat(metadata.isUnsigned()).isTrue(); + assertThat(metadata.isEnum()).isTrue(); + assertThat(metadata.isNotNull()).isTrue(); + } + + @Test + void noSet() { + MySqlTypeMetadata metadata = new MySqlTypeMetadata(0, 0, 0); + + assertThat(metadata.isBinary()).isFalse(); + assertThat(metadata.isSet()).isFalse(); + assertThat(metadata.isUnsigned()).isFalse(); + assertThat(metadata.isEnum()).isFalse(); + assertThat(metadata.isNotNull()).isFalse(); + } + + @Test + void isBinaryUsesCollationId() { + MySqlTypeMetadata metadata = new MySqlTypeMetadata(0, -1, CharCollation.BINARY_ID); + + assertThat(metadata.isBinary()).isTrue(); + + metadata = new MySqlTypeMetadata(0, -1, 33); + assertThat(metadata.isBinary()).isFalse(); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareDateTimeIntegrationTest.java similarity index 80% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareDateTimeIntegrationTest.java index ee6fbc391..11c26a547 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareDateTimeIntegrationTest.java @@ -19,9 +19,9 @@ /** * Integration tests for time zone conversion in the binary protocol. */ -class PrepareTimeZoneIntegrationTest extends TimeZoneIntegrationTestSupport { +class PrepareDateTimeIntegrationTest extends DateTimeIntegrationTestSupport { - PrepareTimeZoneIntegrationTest() { - super(sql -> true); + PrepareDateTimeIntegrationTest() { + super(builder -> builder.useServerPrepareStatement(sql -> true)); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java similarity index 55% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java index 74bb3ee92..94e1591f4 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java @@ -19,40 +19,40 @@ import io.asyncer.r2dbc.mysql.cache.Caches; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.netty.buffer.UnpooledByteBufAllocator; import java.lang.reflect.Field; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** - * Unit tests for {@link PrepareParametrizedStatement}. + * Unit tests for {@link PrepareParameterizedStatement}. */ -class PrepareParametrizedStatementTest implements StatementTestSupport { +class PrepareParameterizedStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); + private final Codecs codecs = Codecs.builder().build(); - private final Codecs codecs = Codecs.builder(UnpooledByteBufAllocator.DEFAULT).build(); + private final Field fetchSize = PrepareParameterizedStatement.class.getDeclaredField("fetchSize"); - private final Field fetchSize = PrepareParametrizedStatement.class.getDeclaredField("fetchSize"); - - PrepareParametrizedStatementTest() throws NoSuchFieldException { + PrepareParameterizedStatementTest() throws NoSuchFieldException { fetchSize.setAccessible(true); } @Override - public int getFetchSize(PrepareParametrizedStatement statement) throws IllegalAccessException { + public int getFetchSize(PrepareParameterizedStatement statement) throws IllegalAccessException { return fetchSize.getInt(statement); } @Override - public PrepareParametrizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { - return new PrepareParametrizedStatement( + public PrepareParameterizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + + return new PrepareParameterizedStatement( client, codecs, - Query.parse(sql), - ConnectionContextTest.mock(isMariaDB), - Caches.createPrepareCache(0) + Query.parse(sql) ); } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java similarity index 94% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java index 45d5a94d7..855e8d3a9 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java @@ -24,12 +24,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Integration tests for {@link PrepareParametrizedStatement} and {@link PrepareSimpleStatement}. + * Integration tests for {@link PrepareParameterizedStatement} and {@link PrepareSimpleStatement}. */ class PrepareQueryIntegrationTest extends QueryIntegrationTestSupport { PrepareQueryIntegrationTest() { - super(configuration("r2dbc", false, false, null, sql -> true)); + super(configuration(builder -> builder.useServerPrepareStatement(sql -> true))); } @Test diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java similarity index 85% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java index 947af752d..56d5ac907 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java @@ -16,21 +16,19 @@ package io.asyncer.r2dbc.mysql; -import io.asyncer.r2dbc.mysql.cache.Caches; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import java.lang.reflect.Field; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link PrepareSimpleStatement}. */ class PrepareSimpleStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); - private final Codecs codecs = mock(Codecs.class); private final Field fetchSize = PrepareSimpleStatement.class.getDeclaredField("fetchSize"); @@ -61,13 +59,11 @@ public int getFetchSize(PrepareSimpleStatement statement) throws IllegalAccessEx @Override public PrepareSimpleStatement makeInstance(boolean isMariaDB, String ignored, String sql) { - return new PrepareSimpleStatement( - client, - codecs, - ConnectionContextTest.mock(isMariaDB), - sql, - Caches.createPrepareCache(0) - ); + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + + return new PrepareSimpleStatement(client, codecs, sql); } @Override diff --git a/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java similarity index 96% rename from src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java index 05d43433b..3afd45a9b 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java @@ -17,9 +17,10 @@ package io.asyncer.r2dbc.mysql; import com.fasterxml.jackson.core.type.TypeReference; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.Result; -import org.jetbrains.annotations.NotNull; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlRow; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -285,7 +286,7 @@ void set() { EnumSet.of(EnumData.ONE, EnumData.THREE)); } - @DisabledIf("envIsLessThanMySql57OrMariaDb102") + @DisabledIf("envIsLessThanMySql578OrMariaDb102") @Test void json() { testType(String.class, false, "JSON", null, "{\"data\": 1}", "[\"data\", 1]", "1", "null", @@ -563,7 +564,7 @@ void insertOnDuplicate() { .bind(2, 20) .execute()) .flatMap(IntegrationTestSupport::extractRowsUpdated) - .doOnNext(it -> assertThat(it).isOne()) // TODO: check capability flag + .doOnNext(it -> assertThat(it).isOne()) .thenMany(connection.createStatement("SELECT value FROM test WHERE id=?") .bind(0, 1) .execute()) @@ -575,7 +576,7 @@ void insertOnDuplicate() { /** * ref: Issue 91 */ - @DisabledIf("envIsLessThanMySql57OrMariaDb102") + @DisabledIf("envIsLessThanMySql578OrMariaDb102") @Test void testUnionQueryWithJsonColumnDecodedAsString() { complete(connection -> @@ -618,11 +619,18 @@ void testUnionQueryWithJsonColumnDecodedAsString() { @Test @DisabledIf("envIsLessThanMySql574OrMariaDb1011") void setStatementTimeoutTest() { - final String sql = "SELECT 1 WHERE SLEEP(1) > 1"; + final String sql = "SELECT COUNT(*) " + + "FROM information_schema.tables a cross join " + + "information_schema.tables b cross join " + + "information_schema.tables c cross join " + + "information_schema.tables d cross join " + + "information_schema.tables e"; + timeout(connection -> connection.setStatementTimeout(Duration.ofMillis(500)) .then(Mono.from(connection.createStatement(sql).execute())) .flatMapMany(result -> Mono.from(result.map((row, metadata) -> row.get(0, String.class)))) .collectList() + .doOnNext(System.out::println) ); } @@ -635,12 +643,12 @@ private static JsonNode parseJson(String json) { } } - private static Flux extractFirstInteger(Result result) { + private static Flux extractFirstInteger(MySqlResult result) { return Flux.from(result.map((row, metadata) -> row.get(0, Integer.class))); } @SuppressWarnings("unchecked") - private static Flux> extractOptionalField(Result result, Type type) { + private static Flux> extractOptionalField(MySqlResult result, Type type) { if (type instanceof Class) { return Flux.from(result.map((row, metadata) -> Optional.ofNullable(row.get(0, (Class) type)))); } @@ -648,7 +656,7 @@ private static Flux> extractOptionalField(Result result, Type ty Optional.ofNullable(((MySqlRow) row).get(0, (ParameterizedType) type)))); } - private static Mono testTimeDuration(Connection connection, Duration origin, LocalTime time) { + private static Mono testTimeDuration(MySqlConnection connection, Duration origin, LocalTime time) { return Mono.from(connection.createStatement("INSERT INTO test VALUES(DEFAULT,?)") .bind(0, origin) .returnGeneratedValues("id") diff --git a/src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java new file mode 100644 index 000000000..cc61d1181 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java @@ -0,0 +1,217 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.internal.util.StringUtils; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; +import io.r2dbc.spi.R2dbcTimeoutException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.time.ZoneId; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Integration tests for session states. + */ +@ExtendWith(TestContainerExtension.class) +class SessionStateIntegrationTest { + + @Test + void forcedLocalTimeZone() { + ZoneId zoneId = ZoneId.systemDefault().normalized(); + + connectionFactory(builder -> builder.connectionTimeZone("local") + .forceConnectionTimeZoneToSession(true)) + .create() + .flatMapMany( + connection -> connection.createStatement("SELECT @@time_zone").execute() + .flatMap(result -> result.map(r -> r.get(0, String.class))) + .map(StringUtils::parseZoneId) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(zoneId) + .verifyComplete(); + } + + @ParameterizedTest + @ValueSource(strings = { + "America/New_York", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Tokyo", + "Europe/London", + "Factory", + "GMT", + "JST", + "ROC", + "UTC", + "+00:00", + "+09:00", + "-09:00", + }) + void forcedConnectionTimeZone(String timeZone) { + ZoneId zoneId = StringUtils.parseZoneId(timeZone); + + connectionFactory(builder -> builder.connectionTimeZone(timeZone) + .forceConnectionTimeZoneToSession(true)) + .create() + .flatMapMany( + connection -> connection.createStatement("SELECT @@time_zone").execute() + .flatMap(result -> result.map(r -> r.get(0, String.class))) + .map(StringUtils::parseZoneId) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(zoneId) + .verifyComplete(); + } + + @ParameterizedTest + @MethodSource + void sessionVariables(Map variables) { + String[] pairs = variables.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .toArray(String[]::new); + String[] keys = variables.keySet().toArray(new String[0]); + String selection = variables.keySet().stream() + .map(it -> "@@session." + it + " AS " + it) + .collect(Collectors.joining(",", "SELECT ", "")); + Map expected = variables.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().startsWith("'") ? + entry.getValue().substring(1, entry.getValue().length() - 1) : entry.getValue())); + + connectionFactory(builder -> builder.sessionVariables(pairs)) + .create() + .flatMapMany(connection -> connection.createStatement(selection).execute() + .flatMap(result -> result.map(r -> { + Map map = new LinkedHashMap<>(); + for (String key : keys) { + map.put(key, r.get(key, String.class)); + } + return map; + })) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(expected) + .verifyComplete(); + } + + @ParameterizedTest + @ValueSource(strings = { "PT1S", "PT10S", "PT1M" }) + void initLockWaitTimeout(String timeout) { + Duration lockWaitTimeout = Duration.parse(timeout); + + connectionFactory(builder -> builder.lockWaitTimeout(lockWaitTimeout)) + .create() + .flatMapMany(connection -> connection.createStatement("SELECT @@innodb_lock_wait_timeout").execute() + .flatMap(result -> result.map(r -> r.get(0, Long.class))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(lockWaitTimeout.getSeconds()) + .verifyComplete(); + } + + @EnabledIf("isGreaterThanOrEqualToMariaDB10_1_1MySql5_7_4") + @ParameterizedTest + @ValueSource(strings = { "PT0.1S", "PT0.5S" }) + void initStatementTimeout(String timeout) { + final String sql = "SELECT COUNT(*) " + + "FROM information_schema.tables a cross join " + + "information_schema.tables b cross join " + + "information_schema.tables c cross join " + + "information_schema.tables d cross join " + + "information_schema.tables e"; + + Duration statementTimeout = Duration.parse(timeout); + + connectionFactory(builder -> builder.statementTimeout(statementTimeout)) + .create() + .flatMapMany(connection -> connection.createStatement(sql).execute() + .flatMap(result -> result.map(r -> r.get(0))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .verifyError(R2dbcTimeoutException.class); + } + + static boolean isGreaterThanOrEqualToMariaDB10_1_1MySql5_7_4() { + final ServerVersion ver = TestServerUtil.getServerVersion(); + if (TestServerUtil.isMariaDb()) { + return ver.isGreaterThanOrEqualTo(ServerVersion.create(10, 1, 1)); + } + + return ver.isGreaterThanOrEqualTo(ServerVersion.create(5, 7, 4)); + } + + static Stream sessionVariables() { + return Stream.of( + Arguments.of(mapOf("sql_mode", "ANSI_QUOTES")), + Arguments.of(mapOf("time_zone", "'+00:00'")), + Arguments.of(mapOf("sql_mode", "'ANSI_QUOTES,STRICT_ALL_TABLES'", "time_zone", "'Asia/Tokyo'")) + ); + } + + private static MySqlConnectionFactory connectionFactory( + Function customizer + ) { + + MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()); + + return MySqlConnectionFactory.from(customizer.apply(builder).build()); + } + + private static Map mapOf(String... paris) { + if (paris.length % 2 != 0) { + throw new IllegalArgumentException("Pairs must be even"); + } + + Map map = new LinkedHashMap<>(); + + for (int i = 0; i < paris.length; i += 2) { + map.put(paris[i], paris[i + 1]); + } + + return map; + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java new file mode 100644 index 000000000..d7723fa15 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java @@ -0,0 +1,309 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.constant.SslMode; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.r2dbc.spi.ValidationDepth; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import javax.net.ssl.SSLException; +import java.net.InetSocketAddress; +import java.security.cert.CertificateException; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(TestContainerExtension.class) +public class SslTunnelIntegrationTest { + + private SelfSignedCertificate server; + + private SelfSignedCertificate client; + + private SslTunnelServer sslTunnelServer; + + @BeforeAll + static void initCachingSha2Password() { + // If the server uses caching_sha2_password, the first time a client connects to the server, the + // server will require a native SSL connection. So all the SSL tunnel tests should be run after + // the caching_sha2_password initialization. + + MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) + .connectTimeout(Duration.ofSeconds(3)) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()) + .createDatabaseIfNotExist(true) + .build(); + + MySqlConnectionFactory.from(configuration).create() + .flatMap(connection -> connection.validate(ValidationDepth.REMOTE) + .flatMap(it -> connection.close().then(Mono.just(it)))) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + } + + @BeforeEach + void setUp() throws CertificateException, SSLException, InterruptedException { + server = new SelfSignedCertificate(); + client = new SelfSignedCertificate(); + final SslContext sslContext = SslContextBuilder.forServer(server.key(), server.cert()).build(); + sslTunnelServer = new SslTunnelServer(TestServerUtil.getHost(), + TestServerUtil.getPort(), + sslContext); + sslTunnelServer.setUp(); + } + + @AfterEach + void tearDown() throws InterruptedException { + server.delete(); + client.delete(); + sslTunnelServer.tearDown(); + } + + @Test + void sslTunnelConnectionTest() { + final MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() + .host("localhost") + .port(sslTunnelServer.getLocalPort()) + .connectTimeout(Duration.ofSeconds(3)) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()) + .sslMode(SslMode.TUNNEL) + .sslKey(client.privateKey().getAbsolutePath()) + .sslCert(client.certificate().getAbsolutePath()) + .sslCa(server.certificate().getAbsolutePath()) + .build(); + + final MySqlConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration); + + final MySqlConnection connection = connectionFactory.create().block(); + assert null != connection; + connection.createStatement("SELECT 3").execute() + .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Long.class))) + .doOnNext(it -> assertThat(it).isEqualTo(3L)) + .blockLast(); + + connection.close().block(); + } + + private static class SslTunnelServer { + + private final String remoteHost; + + private final int remotePort; + + private final SslContext sslContext; + + private volatile ChannelFuture channelFuture; + + private SslTunnelServer(String remoteHost, int remotePort, SslContext sslContext) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + this.sslContext = sslContext; + } + + void setUp() throws InterruptedException { + // Configure the server. + ServerBootstrap b = new ServerBootstrap(); + b.localAddress(0) + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler(new ProxyInitializer(remoteHost, remotePort, sslContext)) + .childOption(ChannelOption.AUTO_READ, false); + + // Start the server. + channelFuture = b.bind().sync(); + } + + void tearDown() throws InterruptedException { + channelFuture.channel().close().sync(); + } + + int getLocalPort() { + return ((InetSocketAddress) channelFuture.channel().localAddress()).getPort(); + } + + } + + private static class ProxyInitializer extends ChannelInitializer { + + private final String remoteHost; + + private final int remotePort; + + private final SslContext sslContext; + + ProxyInitializer(String remoteHost, int remotePort, SslContext sslContext) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + this.sslContext = sslContext; + } + + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(sslContext.newHandler(ch.alloc())); + ch.pipeline().addLast(new ProxyFrontendHandler(remoteHost, remotePort)); + } + } + + private static class ProxyFrontendHandler extends ChannelInboundHandlerAdapter { + + private final String remoteHost; + + private final int remotePort; + + // As we use inboundChannel.eventLoop() when building the Bootstrap this does not need to be + // volatile as the outboundChannel will use the same EventLoop (and therefore Thread) as the + // inboundChannel. + private Channel outboundChannel; + + private ProxyFrontendHandler(String remoteHost, int remotePort) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + final Channel inboundChannel = ctx.channel(); + + // Start the connection attempt. + Bootstrap b = new Bootstrap(); + b.group(inboundChannel.eventLoop()) + .channel(ctx.channel().getClass()) + .handler(new ProxyBackendHandler(inboundChannel)) + .option(ChannelOption.AUTO_READ, false); + ChannelFuture f = b.connect(remoteHost, remotePort); + outboundChannel = f.channel(); + f.addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + // connection complete start to read first data + inboundChannel.read(); + } else { + // Close the connection if the connection attempt has failed. + inboundChannel.close(); + } + }); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + if (outboundChannel.isActive()) { + outboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + // was able to flush out data, start to read the next chunk + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + if (outboundChannel != null) { + closeOnFlush(outboundChannel); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + closeOnFlush(ctx.channel()); + } + + /** + * Closes the specified channel after all queued write requests are flushed. + */ + static void closeOnFlush(Channel ch) { + if (ch.isActive()) { + ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } + } + + private static class ProxyBackendHandler extends ChannelInboundHandlerAdapter { + + private final Channel inboundChannel; + + private ProxyBackendHandler(Channel inboundChannel) { + this.inboundChannel = inboundChannel; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (!inboundChannel.isActive()) { + ProxyFrontendHandler.closeOnFlush(ctx.channel()); + } else { + ctx.read(); + } + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + inboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ProxyFrontendHandler.closeOnFlush(inboundChannel); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ProxyFrontendHandler.closeOnFlush(ctx.channel()); + } + } + +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java new file mode 100644 index 000000000..132195cbb --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.TransactionDefinition; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link StartTransactionState}. + */ +class StartTransactionStateTest { + + @ParameterizedTest + @MethodSource + void buildStartTransaction(TransactionDefinition definition, String excepted) { + assertThat(StartTransactionState.buildStartTransaction(definition)).isEqualTo(excepted); + } + + static Stream buildStartTransaction() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), "BEGIN"), + Arguments.of(MySqlTransactionDefinition.from(IsolationLevel.READ_UNCOMMITTED), "BEGIN"), + Arguments.of(MySqlTransactionDefinition.mutability(false), "START TRANSACTION READ ONLY"), + Arguments.of(MySqlTransactionDefinition.mutability(true), "START TRANSACTION READ WRITE"), + Arguments.of( + MySqlTransactionDefinition.empty().consistent(), + "START TRANSACTION WITH CONSISTENT SNAPSHOT" + ), + Arguments.of( + MySqlTransactionDefinition.mutability(false).consistent(), + "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY" + ), + Arguments.of( + MySqlTransactionDefinition.mutability(true).consistent(), + "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE" + ), + Arguments.of( + MySqlTransactionDefinition.mutability(false) + .consistent("ROCKSDB", 3L), + "START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT FROM SESSION 3, READ ONLY" + ) + ); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java similarity index 91% rename from src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java index 6732b6ec4..ebe788ed9 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import org.junit.jupiter.api.Test; import java.util.NoSuchElementException; @@ -31,11 +32,11 @@ */ interface StatementTestSupport { - String PARAMETRIZED = "SELECT * FROM test WHERE id = ?id AND name = ?"; + String PARAMETERIZED = "SELECT * FROM test WHERE id = ?id AND name = ?"; String SIMPLE = "SELECT * FROM test WHERE id = 1 AND name = 'Mirrors'"; - T makeInstance(boolean isMariaDB, String parametrizedSql, String simpleSql); + T makeInstance(boolean isMariaDB, String parameterizedSql, String simpleSql); boolean supportsBinding(); @@ -47,7 +48,7 @@ default int getFetchSize(T statement) throws IllegalAccessException { default void bind() { assertTrue(supportsBinding(), "Must skip test case #bind() for simple statements"); - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); statement.bind(0, 1); statement.bind("id", 1); statement.bind(1, 1); @@ -56,7 +57,7 @@ default void bind() { @SuppressWarnings("ConstantConditions") @Test default void badBind() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); if (supportsBinding()) { assertThrows(IllegalArgumentException.class, () -> statement.bind(0, null)); @@ -88,7 +89,7 @@ default void badBind() { default void bindNull() { assertTrue(supportsBinding(), "Must skip test case #bindNull() for simple statements"); - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); statement.bindNull(0, Integer.class); statement.bindNull("id", Integer.class); statement.bindNull(1, Integer.class); @@ -97,7 +98,7 @@ default void bindNull() { @SuppressWarnings("ConstantConditions") @Test default void badBindNull() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); if (supportsBinding()) { assertThrows(IllegalArgumentException.class, () -> statement.bindNull(0, null)); @@ -127,7 +128,7 @@ default void badBindNull() { @Test default void add() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); if (!supportsBinding()) { statement.add(); @@ -145,14 +146,14 @@ default void add() { default void badAdd() { assertTrue(supportsBinding(), "Must skip test case #badAdd() for simple statements"); - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); statement.bind(0, 1); assertThrows(IllegalStateException.class, statement::add); } @Test default void mySqlReturnGeneratedValues() { - T s = makeInstance(false, PARAMETRIZED, SIMPLE); + T s = makeInstance(false, PARAMETERIZED, SIMPLE); s.returnGeneratedValues(); @@ -172,7 +173,7 @@ default void mySqlReturnGeneratedValues() { @Test default void mariaDbReturnGeneratedValues() { - T s = makeInstance(true, PARAMETRIZED, SIMPLE); + T s = makeInstance(true, PARAMETERIZED, SIMPLE); s.returnGeneratedValues(); @@ -202,7 +203,7 @@ default void mariaDbReturnGeneratedValues() { @SuppressWarnings("ConstantConditions") @Test default void badReturnGeneratedValues() { - T s = makeInstance(false, PARAMETRIZED, SIMPLE); + T s = makeInstance(false, PARAMETERIZED, SIMPLE); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String) null)); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String[]) null)); @@ -214,7 +215,7 @@ default void badReturnGeneratedValues() { @SuppressWarnings("ConstantConditions") @Test default void mariaDbBadReturnGeneratedValues() { - T s = makeInstance(true, PARAMETRIZED, SIMPLE); + T s = makeInstance(true, PARAMETERIZED, SIMPLE); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String) null)); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String[]) null)); @@ -228,7 +229,7 @@ default void mariaDbBadReturnGeneratedValues() { @Test default void fetchSize() throws IllegalAccessException { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); assertEquals(0, getFetchSize(statement), "Must skip test case #fetchSize() for text-based queries"); for (int i = 1; i <= 10; ++i) { @@ -246,7 +247,7 @@ default void fetchSize() throws IllegalAccessException { @Test default void badFetchSize() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); assertThrows(IllegalArgumentException.class, () -> statement.fetchSize(-1)); assertThrows(IllegalArgumentException.class, () -> statement.fetchSize(-10)); diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextDateTimeIntegrationTest.java similarity index 84% rename from src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextDateTimeIntegrationTest.java index 6b58ae1d4..4d1e153c0 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextDateTimeIntegrationTest.java @@ -19,9 +19,9 @@ /** * Integration tests for time zone conversion in the text protocol. */ -class TextTimeZoneIntegrationTest extends TimeZoneIntegrationTestSupport { +class TextDateTimeIntegrationTest extends DateTimeIntegrationTestSupport { - TextTimeZoneIntegrationTest() { - super(null); + TextDateTimeIntegrationTest() { + super(builder -> builder); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParameterizedStatementTest.java similarity index 63% rename from src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParameterizedStatementTest.java index 38646ec3f..fb58ea526 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParameterizedStatementTest.java @@ -18,18 +18,16 @@ import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.netty.buffer.UnpooledByteBufAllocator; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** - * Unit tests for {@link TextParametrizedStatement}. + * Unit tests for {@link TextParameterizedStatement}. */ -class TextParametrizedStatementTest implements StatementTestSupport { +class TextParameterizedStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); - - private final Codecs codecs = Codecs.builder(UnpooledByteBufAllocator.DEFAULT).build(); + private final Codecs codecs = Codecs.builder().build(); @Override public void fetchSize() { @@ -37,12 +35,15 @@ public void fetchSize() { } @Override - public TextParametrizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { - return new TextParametrizedStatement( + public TextParameterizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + + return new TextParameterizedStatement( client, codecs, - Query.parse(sql), - ConnectionContextTest.mock(isMariaDB) + Query.parse(sql) ); } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java similarity index 89% rename from src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java index a4e7152b7..b3e7478af 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java @@ -17,11 +17,11 @@ package io.asyncer.r2dbc.mysql; /** - * Integration tests for {@link TextSimpleStatement} and {@link TextParametrizedStatement}. + * Integration tests for {@link TextSimpleStatement} and {@link TextParameterizedStatement}. */ class TextQueryIntegrationTest extends QueryIntegrationTestSupport { TextQueryIntegrationTest() { - super(configuration("r2dbc", false, false, null, null)); + super(configuration(builder -> builder)); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java similarity index 86% rename from src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java index 43cb1025c..5c74543b0 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java @@ -20,14 +20,13 @@ import io.asyncer.r2dbc.mysql.codec.Codecs; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link TextSimpleStatement}. */ class TextSimpleStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); - private final Codecs codecs = mock(Codecs.class); @Override @@ -52,7 +51,11 @@ public void fetchSize() { @Override public TextSimpleStatement makeInstance(boolean isMariaDB, String ignored, String sql) { - return new TextSimpleStatement(client, codecs, ConnectionContextTest.mock(isMariaDB), sql); + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + + return new TextSimpleStatement(client, codecs, sql); } @Override diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java new file mode 100644 index 000000000..bf4a0e1f0 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java @@ -0,0 +1,356 @@ +package io.asyncer.r2dbc.mysql; + +import com.zaxxer.hikari.HikariDataSource; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; +import org.assertj.core.data.TemporalUnitOffset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.jdbc.core.JdbcTemplate; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.function.Tuple3; +import reactor.util.function.Tuple4; +import reactor.util.function.Tuple6; +import reactor.util.function.Tuple8; +import reactor.util.function.Tuples; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.Optional; +import java.util.TimeZone; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +/** + * Integration tests for aligning time zone configuration options with jdbc. + */ +@ExtendWith(TestContainerExtension.class) +@Isolated +class TimeZoneIntegrationTest { + + // Earlier versions did not support microseconds, so it is almost always within 1 second, extending to + // 2 seconds due to network reasons + private static final TemporalUnitOffset TINY_WITHIN = within(2, ChronoUnit.SECONDS); + + private static TimeZone defaultTimeZone; + + @BeforeAll + static void setUpTimeZone() { + defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("GMT+9:30")); + } + + @AfterAll + static void tearDownTimeZone() { + TimeZone.setDefault(defaultTimeZone); + } + + @BeforeEach + void setUp() { + String tdl = "CREATE TABLE IF NOT EXISTS test_time_zone (" + + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + + "data1 DATETIME" + dateTimeSuffix(false) + " NOT NULL," + + "data2 TIMESTAMP" + dateTimeSuffix(false) + " NOT NULL)"; + + MySqlConnectionFactory.from(configuration(Function.identity())).create() + .flatMapMany(connection -> connection.createStatement(tdl) + .execute() + .flatMap(MySqlResult::getRowsUpdated) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .then(connection.close())) + .as(StepVerifier::create) + .verifyComplete(); + } + + @AfterEach + void tearDown() { + MySqlConnectionFactory.from(configuration(Function.identity())).create() + .flatMapMany(connection -> connection.createStatement("DROP TABLE IF EXISTS test_time_zone") + .execute() + .flatMap(MySqlResult::getRowsUpdated) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .then(connection.close())) + .as(StepVerifier::create) + .verifyComplete(); + } + + @ParameterizedTest + @MethodSource + void alignDateTimeFunction(boolean instants, String timeZone, boolean force) { + String selectQuery = "SELECT CURRENT_TIMESTAMP" + dateTimeSuffix(true) + + ", NOW" + dateTimeSuffix(true) + + ", CURRENT_TIME" + dateTimeSuffix(true) + + ", CURRENT_DATE()"; + MySqlConnectionConfiguration config = configuration(builder -> builder + .preserveInstants(instants) + .connectionTimeZone(timeZone) + .forceConnectionTimeZoneToSession(force)); + JdbcTemplate jdbc = jdbc(config); + + Tuple4< + Tuple3, + Tuple3, + Tuple3, + LocalDate + > expectedTuples = jdbc.query(selectQuery, (rs, ignored) -> Tuples.of( + Tuples.of( + requireNonNull(rs.getObject(1, LocalDateTime.class)), + requireNonNull(rs.getObject(1, ZonedDateTime.class)), + requireNonNull(rs.getObject(1, OffsetDateTime.class)) + ), + Tuples.of( + requireNonNull(rs.getObject(2, LocalDateTime.class)), + requireNonNull(rs.getObject(2, ZonedDateTime.class)), + requireNonNull(rs.getObject(2, OffsetDateTime.class)) + ), + Tuples.of( + rs.getObject(3, LocalTime.class), + rs.getObject(3, OffsetTime.class), + rs.getObject(3, Duration.class) + ), + rs.getObject(4, LocalDate.class) + )).get(0); + + MySqlConnectionFactory.from(config).create() + .flatMapMany(connection -> connection.createStatement(selectQuery) + .execute() + .flatMap(result -> result.map((row, metadata) -> Tuples.of( + Tuples.of( + requireNonNull(row.get(0, LocalDateTime.class)), + requireNonNull(row.get(0, ZonedDateTime.class)), + requireNonNull(row.get(0, OffsetDateTime.class)), + requireNonNull(row.get(0, Instant.class)) + ), + Tuples.of( + requireNonNull(row.get(1, LocalDateTime.class)), + requireNonNull(row.get(1, ZonedDateTime.class)), + requireNonNull(row.get(1, OffsetDateTime.class)), + requireNonNull(row.get(1, Instant.class)) + ), + Tuples.of( + requireNonNull(row.get(2, LocalTime.class)), + requireNonNull(row.get(2, OffsetTime.class)), + requireNonNull(row.get(2, Duration.class)) + ), + requireNonNull(row.get(3, LocalDate.class)) + ))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty()))) + .as(StepVerifier::create) + .assertNext(data -> { + assertDateTimeTuples(data.getT1(), expectedTuples.getT1()); + assertDateTimeTuples(data.getT2(), expectedTuples.getT2()); + + assertThat(data.getT3().getT1()).isCloseTo(expectedTuples.getT3().getT1(), TINY_WITHIN); + assertThat(data.getT3().getT2().getOffset()) + .isEqualTo(expectedTuples.getT3().getT2().getOffset()); + assertThat(data.getT3().getT2()).isCloseTo(expectedTuples.getT3().getT2(), TINY_WITHIN); + assertThat(data.getT3().getT3()).isCloseTo(expectedTuples.getT3().getT3(), Duration.ofSeconds(2)); + + // If the test case is run close to UTC midnight, it may fail, just run it again + assertThat(data.getT4()).isEqualTo(expectedTuples.getT4()); + }) + .verifyComplete(); + + requireNonNull((HikariDataSource) jdbc.getDataSource()).close(); + } + + @ParameterizedTest + @MethodSource + void alignSendAndReceiveTimeZoneOption(boolean instants, String timeZone, boolean force, Temporal now) { + String insertQuery = "INSERT INTO test_time_zone VALUES (DEFAULT, ?, ?)"; + String selectQuery = "SELECT data1, data2 FROM test_time_zone"; + MySqlConnectionConfiguration config = configuration(builder -> builder + .preserveInstants(instants) + .connectionTimeZone(timeZone) + .forceConnectionTimeZoneToSession(force)); + JdbcTemplate jdbc = jdbc(config); + + assertThat(jdbc.update(insertQuery, now, now)).isOne(); + Tuple6 expectedTuples = jdbc.query(selectQuery, (rs, ignored) -> Tuples.of( + requireNonNull(rs.getObject(1, LocalDateTime.class)), + requireNonNull(rs.getObject(2, LocalDateTime.class)), + requireNonNull(rs.getObject(1, ZonedDateTime.class)), + requireNonNull(rs.getObject(2, ZonedDateTime.class)), + requireNonNull(rs.getObject(1, OffsetDateTime.class)), + requireNonNull(rs.getObject(2, OffsetDateTime.class)) + )).get(0); + Consumer> assertion = actual -> { + assertThat(actual.getT1()).isCloseTo(expectedTuples.getT1(), TINY_WITHIN); + assertThat(actual.getT2()).isCloseTo(expectedTuples.getT2(), TINY_WITHIN); + assertThat(actual.getT3().getZone().normalized()) + .isEqualTo(expectedTuples.getT3().getZone().normalized()); + assertThat(actual.getT3()).isCloseTo(expectedTuples.getT3(), TINY_WITHIN); + assertThat(actual.getT4().getZone().normalized()) + .isEqualTo(expectedTuples.getT4().getZone().normalized()); + assertThat(actual.getT4()).isCloseTo(expectedTuples.getT4(), TINY_WITHIN); + assertThat(actual.getT5().getOffset()).isEqualTo(expectedTuples.getT5().getOffset()); + assertThat(actual.getT5()).isCloseTo(expectedTuples.getT5(), TINY_WITHIN); + assertThat(actual.getT6().getOffset()).isEqualTo(expectedTuples.getT6().getOffset()); + assertThat(actual.getT6()).isCloseTo(expectedTuples.getT6(), TINY_WITHIN); + assertThat(actual.getT7()).isCloseTo(expectedTuples.getT3().toInstant(), TINY_WITHIN); + assertThat(actual.getT8()).isCloseTo(expectedTuples.getT4().toInstant(), TINY_WITHIN); + assertThat(actual.getT7()).isCloseTo(expectedTuples.getT5().toInstant(), TINY_WITHIN); + assertThat(actual.getT8()).isCloseTo(expectedTuples.getT6().toInstant(), TINY_WITHIN); + }; + + MySqlConnectionFactory.from(config).create() + .flatMapMany(connection -> connection.createStatement(insertQuery) + .bind(0, now) + .bind(1, now) + .execute() + .flatMap(MySqlResult::getRowsUpdated) + .thenMany(connection.createStatement(selectQuery).execute()) + .flatMap(result -> result.map(r -> Tuples.of( + requireNonNull(r.get(0, LocalDateTime.class)), + requireNonNull(r.get(1, LocalDateTime.class)), + requireNonNull(r.get(0, ZonedDateTime.class)), + requireNonNull(r.get(1, ZonedDateTime.class)), + requireNonNull(r.get(0, OffsetDateTime.class)), + requireNonNull(r.get(1, OffsetDateTime.class)), + requireNonNull(r.get(0, Instant.class)), + requireNonNull(r.get(1, Instant.class)) + ))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty()))) + .as(StepVerifier::create) + .assertNext(assertion) + .assertNext(assertion) + .verifyComplete(); + + requireNonNull((HikariDataSource) jdbc.getDataSource()).close(); + } + + static Stream alignDateTimeFunction() { + return Stream.of( + Arguments.of(false, "LOCAL", false), + Arguments.of(false, "LOCAL", true), + Arguments.of(true, "LOCAL", false), + Arguments.of(true, "LOCAL", true), + Arguments.of(false, "SERVER", false), + Arguments.of(false, "SERVER", true), + Arguments.of(true, "SERVER", false), + Arguments.of(true, "SERVER", true), + Arguments.of(false, "GMT+2", false), + Arguments.of(false, "GMT+3", true), + Arguments.of(true, "GMT+4", false), + Arguments.of(true, "GMT+5", true) + ); + } + + static Stream alignSendAndReceiveTimeZoneOption() { + ZonedDateTime dateTime = ZonedDateTime.now(); + + return Stream.of(dateTime).flatMap(now -> Stream.of( + now.toLocalDateTime(), + now, + now.toOffsetDateTime(), + now.withZoneSameInstant(ZoneOffset.ofHours(2)).toLocalDateTime(), + now.withZoneSameInstant(ZoneOffset.ofHours(3)), + now.withZoneSameInstant(ZoneOffset.ofHours(4)).toOffsetDateTime(), + now.withZoneSameLocal(ZoneOffset.ofHours(5)).toLocalDateTime(), + now.withZoneSameLocal(ZoneOffset.ofHours(6)), + now.withZoneSameLocal(ZoneOffset.ofHours(7)).toOffsetDateTime() + )).flatMap(temporal -> Stream.of( + Arguments.of(false, "LOCAL", false, temporal), + Arguments.of(false, "LOCAL", true, temporal), + Arguments.of(true, "LOCAL", false, temporal), + Arguments.of(true, "LOCAL", true, temporal), + Arguments.of(false, "SERVER", false, temporal), + Arguments.of(false, "SERVER", true, temporal), + Arguments.of(true, "SERVER", false, temporal), + Arguments.of(true, "SERVER", true, temporal), + Arguments.of(false, "GMT+1", false, temporal), + Arguments.of(false, "GMT+1", true, temporal), + Arguments.of(true, "GMT+1", false, temporal), + Arguments.of(true, "GMT+1", true, temporal) + )); + } + + private static MySqlConnectionConfiguration configuration( + Function customizer + ) { + + MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()); + + return customizer.apply(builder).build(); + } + + private static JdbcTemplate jdbc(MySqlConnectionConfiguration config) { + HikariDataSource source = new HikariDataSource(); + + source.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s", config.getDomain(), + config.getPort(), config.getDatabase())); + source.setUsername(config.getUser()); + source.setPassword(Optional.ofNullable(config.getPassword()) + .map(Object::toString).orElse(null)); + source.setMaximumPoolSize(1); + source.setConnectionTimeout(Optional.ofNullable(config.getConnectTimeout()) + .map(Duration::toMillis).orElse(0L)); + + source.addDataSourceProperty("preserveInstants", config.isPreserveInstants()); + source.addDataSourceProperty("connectionTimeZone", config.getConnectionTimeZone()); + source.addDataSourceProperty("forceConnectionTimeZoneToSession", + config.isForceConnectionTimeZoneToSession()); + + return new JdbcTemplate(source); + } + + private static String dateTimeSuffix(boolean function) { + return isMicrosecondSupported() ? "(6)" : function ? "()" : ""; + } + + private static boolean isMicrosecondSupported() { + final ServerVersion ver = TestServerUtil.getServerVersion(); + + return TestServerUtil.isMariaDb() || + ver.isGreaterThanOrEqualTo(ServerVersion.create(5, 6, 0)); + } + + private static void assertDateTimeTuples( + Tuple4 actual, + Tuple3 expected + ) { + assertThat(actual.getT1()).isCloseTo(expected.getT1(), TINY_WITHIN); + assertThat(actual.getT2().getZone().normalized()) + .isEqualTo(expected.getT2().getZone().normalized()); + assertThat(actual.getT2()).isCloseTo(expected.getT2(), TINY_WITHIN); + assertThat(actual.getT3().getOffset()) + .isEqualTo(expected.getT3().getOffset()); + assertThat(actual.getT3()).isCloseTo(expected.getT3(), TINY_WITHIN); + assertThat(actual.getT4()) + .isCloseTo(expected.getT2().toInstant(), TINY_WITHIN); + assertThat(actual.getT4()) + .isCloseTo(expected.getT3().toInstant(), TINY_WITHIN); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TinyInt1isBitFalseTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TinyInt1isBitFalseTest.java new file mode 100644 index 000000000..9db19e414 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TinyInt1isBitFalseTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +class TinyInt1isBitFalseTest extends IntegrationTestSupport{ + TinyInt1isBitFalseTest() { + super(configuration(builder -> builder.tinyInt1isBit(false))); + } + + @Test + public void tinyInt1isBitFalse() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Byte.class))); + } + +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java new file mode 100644 index 000000000..df0e3c639 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; + +/** + * Integration tests for zstd compression. + */ +class ZlibCompressionIntegrationTest extends CompressionIntegrationTestSupport { + + ZlibCompressionIntegrationTest() { + super(CompressionAlgorithm.ZLIB); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java new file mode 100644 index 000000000..625fd6768 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; +import org.junit.jupiter.api.condition.EnabledIf; + +/** + * Integration tests for zstd compression. + */ +@EnabledIf("envIsZstdSupported") +class ZstdCompressionIntegrationTest extends CompressionIntegrationTestSupport { + + ZstdCompressionIntegrationTest() { + super(CompressionAlgorithm.ZSTD); + } + + static boolean envIsZstdSupported() { + if (TestServerUtil.isMariaDb()) { + return false; + } + + final ServerVersion ver = TestServerUtil.getServerVersion(); + return ver.isGreaterThanOrEqualTo(ServerVersion.create(8, 0, 18)); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinitionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinitionTest.java new file mode 100644 index 000000000..46fa69ef0 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinitionTest.java @@ -0,0 +1,172 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.asyncer.r2dbc.mysql.ConsistentSnapshotEngine; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.TransactionDefinition; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Duration; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link MySqlTransactionDefinition}. + */ +class MySqlTransactionDefinitionTest { + + @Test + void getAttribute() { + Duration lockWaitTimeout = Duration.ofSeconds(118); + long sessionId = 123456789L; + MySqlTransactionDefinition definition = MySqlTransactionDefinition.from(IsolationLevel.READ_COMMITTED) + .lockWaitTimeout(lockWaitTimeout) + .consistent("ROCKSDB", sessionId); + + assertThat(definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(IsolationLevel.READ_COMMITTED); + assertThat(definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isSameAs(lockWaitTimeout); + assertThat(definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT)) + .isTrue(); + assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE)) + .isEqualTo("ROCKSDB"); + assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_FROM_SESSION)) + .isEqualTo(sessionId); + } + + @Test + void isolationLevel() { + Duration lockWaitTimeout = Duration.ofSeconds(118); + MySqlTransactionDefinition def1 = MySqlTransactionDefinition.mutability(false) + .isolationLevel(IsolationLevel.SERIALIZABLE) + .lockWaitTimeout(lockWaitTimeout); + MySqlTransactionDefinition def2 = def1.isolationLevel(IsolationLevel.READ_COMMITTED); + + assertThat(def1).isNotEqualTo(def2); + assertThat(def1.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isSameAs(def2.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isSameAs(lockWaitTimeout); + assertThat(def1.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(IsolationLevel.SERIALIZABLE); + assertThat(def2.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(IsolationLevel.READ_COMMITTED); + assertThat(def1.getAttribute(TransactionDefinition.READ_ONLY)) + .isSameAs(def2.getAttribute(TransactionDefinition.READ_ONLY)) + .isEqualTo(true); + } + + @ParameterizedTest + @MethodSource + void withoutIsolationLevel(MySqlTransactionDefinition definition, IsolationLevel level) { + assertThat(definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(level); + assertThat(definition.withoutIsolationLevel().getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isNull(); + } + + @ParameterizedTest + @MethodSource + void withoutLockWaitTimeout(MySqlTransactionDefinition definition, @Nullable Duration lockWaitTimeout) { + assertThat(definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isEqualTo(lockWaitTimeout); + assertThat(definition.withoutLockWaitTimeout().getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isNull(); + } + + @ParameterizedTest + @MethodSource + void withoutMutability(MySqlTransactionDefinition definition, @Nullable Boolean readOnly) { + assertThat(definition.getAttribute(TransactionDefinition.READ_ONLY)) + .isEqualTo(readOnly); + assertThat(definition.withoutMutability().getAttribute(TransactionDefinition.READ_ONLY)) + .isNull(); + } + + @ParameterizedTest + @MethodSource + void withoutConsistent(MySqlTransactionDefinition definition) { + assertThat(definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT)) + .isTrue(); + assertThat(definition) + .isNotEqualTo(MySqlTransactionDefinition.empty()) + .extracting(MySqlTransactionDefinition::withoutConsistent) + .isEqualTo(MySqlTransactionDefinition.empty()); + } + + static Stream withoutIsolationLevel() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), null), + Arguments.of( + MySqlTransactionDefinition.from(IsolationLevel.READ_COMMITTED), + IsolationLevel.READ_COMMITTED + ), + Arguments.of( + MySqlTransactionDefinition.from(IsolationLevel.SERIALIZABLE) + .lockWaitTimeout(Duration.ofSeconds(118)), + IsolationLevel.SERIALIZABLE + ) + ); + } + + static Stream withoutLockWaitTimeout() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), null), + Arguments.of( + MySqlTransactionDefinition.empty() + .lockWaitTimeout(Duration.ofSeconds(118)), + Duration.ofSeconds(118) + ), + Arguments.of( + MySqlTransactionDefinition.empty() + .lockWaitTimeout(Duration.ofSeconds(123)) + .consistent("ROCKSDB", 123456789), + Duration.ofSeconds(123) + ) + ); + } + + static Stream withoutMutability() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), null), + Arguments.of(MySqlTransactionDefinition.mutability(true), false), + Arguments.of(MySqlTransactionDefinition.mutability(false), true), + Arguments.of(MySqlTransactionDefinition.mutability(true).consistent(), false), + Arguments.of(MySqlTransactionDefinition.mutability(false).consistent(), true), + Arguments.of( + MySqlTransactionDefinition.mutability(true) + .isolationLevel(IsolationLevel.SERIALIZABLE), + false + ) + ); + } + + static Stream withoutConsistent() { + return Stream.of( + MySqlTransactionDefinition.empty().consistent(), + MySqlTransactionDefinition.empty().consistent("ROCKSDB"), + MySqlTransactionDefinition.empty().consistent("ROCKSDB", 123456789), + MySqlTransactionDefinition.empty().consistentFromSession(123456789) + ); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java new file mode 100644 index 000000000..3c9a00baa --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java @@ -0,0 +1,99 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DecoderException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Stream; +import java.util.zip.DeflaterOutputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for {@link ZlibCompressor}. + */ +class ZlibCompressorTest { + + private final ZlibCompressor compressor = new ZlibCompressor(); + + @ParameterizedTest + @MethodSource("uncompressedData") + void compress(String input) throws IOException { + byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + ByteBuf compressed = compressor.compress(Unpooled.wrappedBuffer(bytes)); + byte[] nativeCompressed = nativeCompress(bytes); + + // It may return early if the compressed data is not smaller than the original. + assertThat(ByteBufUtil.getBytes(compressed)).hasSizeLessThanOrEqualTo(bytes.length) + .isEqualTo(Arrays.copyOf(nativeCompressed, compressed.readableBytes())); + } + + @ParameterizedTest + @MethodSource("uncompressedData") + void decompress(String input) throws IOException { + byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + ByteBuf compressed = Unpooled.wrappedBuffer(nativeCompress(bytes)); + ByteBuf decompressed = compressor.decompress(compressed, bytes.length); + + assertThat(ByteBufUtil.getBytes(decompressed)).isEqualTo(bytes); + } + + @Test + void badDecompress() { + ByteBuf compressed = Unpooled.wrappedBuffer( + new byte[] { 0x78, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }); + + assertThatExceptionOfType(DecoderException.class) + .isThrownBy(() -> compressor.decompress(compressed, compressed.readableBytes() << 1)); + } + + static Stream uncompressedData() { + return Stream.of( + "", " ", + "Hello, world!", + "1234567890", + "ユニコードテスト、유니코드 테스트,Unicode测试,тест Юникода", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " + + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + + "Ut enim ad minim veniam, quis exercitation ullamco nisi ut aliquip ea commodo consequat. " + + "Duis aute irure dolor in reprehenderit en voluptate esse cillum eu fugiat nulla pariatur." + ); + } + + private static byte[] nativeCompress(byte[] input) throws IOException { + try (ByteArrayOutputStream r = new ByteArrayOutputStream(); + DeflaterOutputStream s = new DeflaterOutputStream(r)) { + + s.write(input); + s.finish(); + s.flush(); + + return r.toByteArray(); + } + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java new file mode 100644 index 000000000..dbfd5c104 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java @@ -0,0 +1,173 @@ +/* + * Copyright 2023 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.codec; + +import io.asyncer.r2dbc.mysql.ConnectionContextTest; +import io.asyncer.r2dbc.mysql.constant.MySqlType; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.r2dbc.spi.R2dbcNonTransientException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.nio.charset.Charset; +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +/** + * Unit tests for {@link BooleanCodec}. + */ +class BooleanCodecTest implements CodecTestSupport { + + private final Boolean[] booleans = { Boolean.TRUE, Boolean.FALSE }; + + @Override + public BooleanCodec getCodec() { + return BooleanCodec.INSTANCE; + } + + @Override + public Boolean[] originParameters() { + return booleans; + } + + @Override + public Object[] stringifyParameters() { + return Arrays.stream(booleans).map(it -> it ? "1" : "0").toArray(); + } + + @Override + public ByteBuf[] binaryParameters(Charset charset) { + return Arrays.stream(booleans) + .map(it -> Unpooled.wrappedBuffer(it ? new byte[] { 1 } : new byte[] { 0 })) + .toArray(ByteBuf[]::new); + } + + @Override + public ByteBuf sized(ByteBuf value) { + return value; + } + + @Test + void decodeString() { + Codec codec = getCodec(); + Charset c = ConnectionContextTest.mock().getClientCollation().getCharset(); + byte[] bOne = new byte[]{(byte)1}; + byte[] bZero = new byte[]{(byte)0}; + ByteBuffer bitValOne = ByteBuffer.wrap(bOne); + ByteBuffer bitValZero = ByteBuffer.wrap(bZero); + Decoding d1 = new Decoding(Unpooled.copiedBuffer("true", c), "true", MySqlType.VARCHAR); + Decoding d2 = new Decoding(Unpooled.copiedBuffer("false", c), "false", MySqlType.VARCHAR); + Decoding d3 = new Decoding(Unpooled.copiedBuffer("1", c), "1", MySqlType.VARCHAR); + Decoding d4 = new Decoding(Unpooled.copiedBuffer("0", c), "0", MySqlType.VARCHAR); + Decoding d5 = new Decoding(Unpooled.copiedBuffer("Y", c), "Y", MySqlType.VARCHAR); + Decoding d6 = new Decoding(Unpooled.copiedBuffer("no", c), "no", MySqlType.VARCHAR); + Decoding d7 = new Decoding(Unpooled.copiedBuffer("26.57", c), "26.57", MySqlType.VARCHAR); + Decoding d8 = new Decoding(Unpooled.copiedBuffer("-57", c), "=57", MySqlType.VARCHAR); + Decoding d9 = new Decoding(Unpooled.copiedBuffer("100000", c), "100000", MySqlType.VARCHAR); + Decoding d10 = new Decoding(Unpooled.copiedBuffer("-12345678901234567890", c), + "-12345678901234567890", MySqlType.VARCHAR); + Decoding d11 = new Decoding(Unpooled.copiedBuffer("Banana", c), "Banana", MySqlType.VARCHAR); + Decoding d12 = new Decoding(Unpooled.copiedBuffer(bitValOne), bitValOne, MySqlType.BIT); + Decoding d13 = new Decoding(Unpooled.copiedBuffer(bitValZero), bitValZero, MySqlType.BIT); + Decoding d14 = new Decoding(Unpooled.copyDouble(26.57d), 26.57d, MySqlType.DOUBLE); + Decoding d15 = new Decoding(Unpooled.copiedBuffer(bOne), bOne, MySqlType.TINYINT); + Decoding d16 = new Decoding(Unpooled.copiedBuffer(bZero), bZero, MySqlType.TINYINT); + Decoding d17 = new Decoding(Unpooled.copiedBuffer("1e4", c), "1e4", MySqlType.VARCHAR); + Decoding d18 = new Decoding(Unpooled.copiedBuffer("-1.34e10", c), "-1.34e10", MySqlType.VARCHAR); + Decoding d19 = new Decoding(Unpooled.copiedBuffer("-0", c), "-0", MySqlType.VARCHAR); + + assertThat(codec.decode(d1.content(), d1.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d1) + .isEqualTo(true); + + assertThat(codec.decode(d2.content(), d2.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d2) + .isEqualTo(false); + + assertThat(codec.decode(d3.content(), d3.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d3) + .isEqualTo(true); + + assertThat(codec.decode(d4.content(), d4.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d4) + .isEqualTo(false); + + assertThat(codec.decode(d5.content(), d5.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d5) + .isEqualTo(true); + + assertThat(codec.decode(d6.content(), d6.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d6) + .isEqualTo(false); + + assertThat(codec.decode(d7.content(), d7.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d7) + .isEqualTo(true); + + assertThat(codec.decode(d8.content(), d8.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d8) + .isEqualTo(false); + + assertThat(codec.decode(d9.content(), d9.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d9) + .isEqualTo(true); + + assertThat(codec.decode(d10.content(), d10.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d10) + .isEqualTo(false); + + assertThatThrownBy(() -> {codec.decode(d11.content(), d11.metadata(), Boolean.class, false, ConnectionContextTest.mock());}) + .isInstanceOf(R2dbcNonTransientException.class); + + assertThat(codec.decode(d12.content(), d12.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d12) + .isEqualTo(true); + + assertThat(codec.decode(d13.content(), d13.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d13) + .isEqualTo(false); + + assertThat(codec.decode(d14.content(), d14.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d14) + .isEqualTo(true); + + assertThat(codec.decode(d15.content(), d15.metadata(), Boolean.class, true, ConnectionContextTest.mock())) + .as("Decode failed, %s", d15) + .isEqualTo(true); + + assertThat(codec.decode(d16.content(), d16.metadata(), Boolean.class, true, ConnectionContextTest.mock())) + .as("Decode failed, %s", d16) + .isEqualTo(false); + + assertThat(codec.decode(d17.content(), d17.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d17) + .isEqualTo(true); + + assertThat(codec.decode(d18.content(), d18.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d18) + .isEqualTo(false); + + assertThat(codec.decode(d19.content(), d19.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d19) + .isEqualTo(false); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodecTest.java new file mode 100644 index 000000000..8c7e8b847 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodecTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.testcontainers.shaded.org.bouncycastle.util.encoders.Hex; + +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Unit tests for {@link ByteArrayInputStreamCodec}. + */ +public class ByteArrayInputStreamCodecTest implements CodecTestSupport { + + private final byte[][] rawData = { + new byte[0], + new byte[] { 0x7F }, + new byte[] { 0x12, 34, 0x56, 78, (byte) 0x9A }, + "Hello world!".getBytes(StandardCharsets.US_ASCII), + new byte[] { (byte) 0xFE, (byte) 0xDC, (byte) 0xBA }, + }; + + private final ByteArrayInputStream[] data = Arrays.stream(rawData) + .map(ByteArrayInputStream::new) + .toArray(ByteArrayInputStream[]::new); + + @Override + public Codec getCodec() { + return ByteArrayInputStreamCodec.INSTANCE; + } + + @Override + public ByteArrayInputStream[] originParameters() { + return data; + } + + @Override + public Object[] stringifyParameters() { + return Arrays.stream(rawData) + .map(bytes -> String.format("x'%s'", Hex.toHexString(bytes))) + .toArray(); + } + + @Override + public ByteBuf[] binaryParameters(Charset charset) { + return Arrays.stream(rawData) + .map(Unpooled::wrappedBuffer) + .toArray(ByteBuf[]::new); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java similarity index 98% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java index 7a9471793..380d3c6de 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java @@ -91,7 +91,7 @@ default void encodeStringify() { Query query = Query.parse("?"); for (int i = 0; i < origin.length; ++i) { - ParameterWriter writer = ParameterWriterHelper.get(query); + ParameterWriter writer = ParameterWriterHelper.get(false, query); codec.encode(origin[i], context()) .publishText(writer) .as(StepVerifier::create) diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java similarity index 85% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java index 998907315..fefdb2415 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java @@ -17,12 +17,10 @@ package io.asyncer.r2dbc.mysql.codec; import io.asyncer.r2dbc.mysql.ConnectionContextTest; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; -import io.asyncer.r2dbc.mysql.MySqlTypeMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.message.FieldValue; -import io.netty.buffer.PooledByteBufAllocator; import io.r2dbc.spi.Nullability; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -34,7 +32,7 @@ */ class CodecsTest { - private static final Codecs CODECS = Codecs.builder(PooledByteBufAllocator.DEFAULT).build(); + private static final Codecs CODECS = Codecs.builder().build(); private static final CodecContext CONTEXT = ConnectionContextTest.mock(); @@ -74,21 +72,11 @@ public String getName() { return "mock"; } - @Override - public MySqlTypeMetadata getNativeTypeMetadata() { - return null; - } - @Override public CharCollation getCharCollation(CodecContext context) { return CharCollation.fromId(CharCollation.BINARY_ID, context); } - @Override - public long getNativePrecision() { - return 0; - } - @Override public Nullability getNullability() { return Nullability.NULLABLE; diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java similarity index 83% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java index 100ea09cf..8d4adf4b1 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java @@ -21,7 +21,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.SignStyle; @@ -29,14 +29,20 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; -import static java.time.temporal.ChronoField.*; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MICRO_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; /** * Base class considers codecs unit tests of date/time. */ abstract class DateTimeCodecTestSupport implements CodecTestSupport { - protected static final ZoneId ENCODE_SERVER_ZONE = ZoneId.of("+6"); + protected static final ZoneOffset ENCODE_SERVER_ZONE = ZoneOffset.ofHours(6); private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() .appendLiteral('\'') diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java similarity index 87% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java index 7faade837..bf17a6dc9 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java @@ -16,8 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; -import io.asyncer.r2dbc.mysql.MySqlTypeMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; @@ -71,11 +70,6 @@ public String getName() { return "mock"; } - @Override - public MySqlTypeMetadata getNativeTypeMetadata() { - return null; - } - @Override public Nullability getNullability() { return Nullability.NON_NULL; @@ -86,9 +80,5 @@ public CharCollation getCharCollation(CodecContext context) { return context.getClientCollation(); } - @Override - public long getNativePrecision() { - return 0; - } } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java similarity index 95% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java index d97feca07..87666ddac 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java @@ -27,7 +27,8 @@ /** * Unit tests for {@link EnumCodec}. */ -class EnumCodecTest implements CodecTestSupport> { +@SuppressWarnings("rawtypes") +class EnumCodecTest implements CodecTestSupport { private final Enum[] enums = { // Java has no way to create an element of enum with special character. diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java similarity index 88% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java index 97774b2a4..8485b1059 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java @@ -22,6 +22,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Arrays; @@ -58,6 +59,9 @@ public ByteBuf[] binaryParameters(Charset charset) { } private LocalDateTime convert(OffsetDateTime value) { - return value.withOffsetSameInstant((ZoneOffset) ENCODE_SERVER_ZONE).toLocalDateTime(); + ZoneOffset offset = context().isPreserveInstants() ? ENCODE_SERVER_ZONE : + ZoneId.systemDefault().getRules().getOffset(value.toLocalDateTime()); + + return value.withOffsetSameInstant(offset).toLocalDateTime(); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java similarity index 88% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java index 4aa3ecdee..b3d849745 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java @@ -19,8 +19,10 @@ import io.netty.buffer.ByteBuf; import java.nio.charset.Charset; +import java.time.Instant; import java.time.LocalTime; import java.time.OffsetTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Arrays; @@ -67,6 +69,9 @@ public ByteBuf[] binaryParameters(Charset charset) { } private LocalTime convert(OffsetTime value) { - return value.withOffsetSameInstant((ZoneOffset) ENCODE_SERVER_ZONE).toLocalTime(); + ZoneId zone = ZoneId.systemDefault().normalized(); + + return value.withOffsetSameInstant(zone instanceof ZoneOffset ? (ZoneOffset) zone : + zone.getRules().getStandardOffset(Instant.EPOCH)).toLocalTime(); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java similarity index 98% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java index ac50501a5..a2155a578 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java @@ -129,7 +129,7 @@ void stringifySet() { Query query = Query.parse("?"); for (int i = 0; i < sets.length; ++i) { - ParameterWriter writer = ParameterWriterHelper.get(query); + ParameterWriter writer = ParameterWriterHelper.get(false, query); codec.encode(sets[i], context()) .publishText(writer) .as(StepVerifier::create) diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java similarity index 92% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java index 66697a75a..10f729193 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java @@ -75,6 +75,10 @@ public ByteBuf[] binaryParameters(Charset charset) { } private LocalDateTime convert(ZonedDateTime value) { - return value.withZoneSameInstant(ENCODE_SERVER_ZONE).toLocalDateTime(); + if (context().isPreserveInstants()) { + return value.withZoneSameInstant(ENCODE_SERVER_ZONE).toLocalDateTime(); + } + + return value.toLocalDateTime(); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java similarity index 98% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java index 02e076a03..8c9d7325e 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.internal.util; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -262,7 +263,7 @@ void mergeIntegralWithLargeCrossIntegral() { } private Flux envelopes(Flux source, int envelopeSize) { - return new FluxEnvelope(source, allocator, envelopeSize, 0, true); + return new FluxEnvelope(source, allocator, envelopeSize, new AtomicInteger(0), true); } private Consumer> assertBuffers(String origin, int envelopeSize, int lastSize, @@ -273,7 +274,7 @@ private Consumer> assertBuffers(String origin, int envelopeSize, i for (int i = 0, n = originBuffers.size(); i < n; i += 2) { ByteBuf header = originBuffers.get(i); - assertThat(header.readableBytes()).isEqualTo(Envelopes.PART_HEADER_SIZE); + assertThat(header.readableBytes()).isEqualTo(Packets.NORMAL_HEADER_SIZE); int size = header.readMediumLE(); if (size > 0) { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestContainerExtension.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestContainerExtension.java new file mode 100644 index 000000000..e324d3cf9 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestContainerExtension.java @@ -0,0 +1,160 @@ +package io.asyncer.r2dbc.mysql.internal.util; + +import io.netty.util.internal.SystemPropertyUtil; +import org.jetbrains.annotations.VisibleForTesting; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; + +/** + * JUnit 5 extension to start a test container with MySQL or MariaDB. + * The extension starts a test container with MySQL or MariaDB based on the system properties: + *

    + *
  • {@code test.db.testcontainer} - whether to use a test container or not (default: {@code true})
  • + *
  • {@code test.db.type} - the test-container database type (default: {@code mysql})
  • + *
  • {@code test.db.version} - the test-container database version (default: {@code 5.7.44})
  • + *
  • {@code test.db.host} - the self-hosted database server host (default: {@code 127.0.0.1})
  • + *
  • {@code test.db.port} - the self-hosted database server port (default: {@code 3306})
  • + *
  • {@code test.db.database} - the self-hoseted database server database name (default: {@code test})
  • + *
  • {@code test.db.username} - the self-hosted database server username (default: {@code root})
  • + *
  • {@code test.db.password} - the self-hosted database server password (default: {@code root})
  • + *
+ */ +public final class TestContainerExtension implements BeforeAllCallback { + + static final Container server; + + static final boolean useTestContainer; + + static final String dbType; + + static final String dbVersion; + + static { + useTestContainer = SystemPropertyUtil.getBoolean("test.db.testcontainer", true); + dbType = SystemPropertyUtil.get("test.db.type", "mysql"); + dbVersion = SystemPropertyUtil.get("test.db.version", "5.7.44"); + if (useTestContainer) { + server = new TestContainer(dbType, dbVersion); + } else { + server = new SelfHostedContainer(); + } + server.start(); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + // NOOP - initialized in the static block + } + + private static final class TestContainer implements Container { + + private final JdbcDatabaseContainer container; + + @SuppressWarnings("resource") + private TestContainer(final String dbType, final String dbVersion) { + if ("mariadb".equalsIgnoreCase(dbType)) { + container = new MariaDBContainer<>(dbType + ':' + dbVersion) + .withUsername("root") + .withPassword("") + .withNetwork(Network.newNetwork()) + .withCommand("--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci"); + } else { + container = new MySQLContainer<>(dbType + ':' + dbVersion) + .withUsername("root") + .withNetwork(Network.newNetwork()) + .withCommand("--local-infile=true", + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci"); + if (dbVersion.startsWith("5.5")) { + // mysql 5.5.x does not support host_cache_size but latest test container utilizes it. + // so we need to remove host_cache_size option when mysql version is 5.5.x and lower + // ref: https://github.com/testcontainers/testcontainers-java/issues/8130 + ((MySQLContainer) container).withConfigurationOverride("testcontainer/mysql-5.5"); + } + } + } + + @Override + public void start() { + container.start(); + } + + @Override + public String getHost() { + return container.getHost(); + } + + @Override + public int getPort() { + return container.getMappedPort(3306); + } + + @Override + public String getDatabase() { + return container.getDatabaseName(); + } + + @Override + public String getUsername() { + return container.getUsername(); + } + + @Override + public String getPassword() { + return container.getPassword(); + } + } + + private static final class SelfHostedContainer implements Container { + + @Override + public void start() { + // NOOP + } + + @Override + public String getHost() { + return SystemPropertyUtil.get("test.db.host", "127.0.0.1"); + } + + @Override + public int getPort() { + return SystemPropertyUtil.getInt("test.db.port", 3306); + } + + @Override + public String getDatabase() { + return SystemPropertyUtil.get("test.db.database", "test"); + } + + @Override + public String getUsername() { + return SystemPropertyUtil.get("test.db.username", "root"); + } + + @Override + public String getPassword() { + return SystemPropertyUtil.get("test.db.password", "root"); + } + } + + interface Container { + + void start(); + + String getHost(); + + int getPort(); + + String getDatabase(); + + String getUsername(); + + String getPassword(); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtil.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtil.java new file mode 100644 index 000000000..4fbba5683 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtil.java @@ -0,0 +1,137 @@ +package io.asyncer.r2dbc.mysql.internal.util; + +import com.zaxxer.hikari.HikariDataSource; +import io.asyncer.r2dbc.mysql.ServerVersion; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Utility class that provides access to the test server configuration. + */ +public final class TestServerUtil { + + static final AtomicReference dataSource = new AtomicReference<>(); + + static final AtomicReference connection = new AtomicReference<>(); + + /** + * Returns the host of the test server. + * + * @return the host of the test server + */ + public static String getHost() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getHost(); + } + + /** + * Returns the port of the test server. + * @return the port of the test server + */ + public static int getPort() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getPort(); + } + + /** + * Returns the database name of the test server. + * @return the database name of the test server + */ + public static String getDatabase() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getDatabase(); + } + + /** + * Returns the username of the test server. + * @return the username of the test server + */ + public static String getUsername() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getUsername(); + } + + /** + * Returns the password of the test server. + * @return the password of the test server + */ + public static String getPassword() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getPassword(); + } + + /** + * Returns whether the test server is a MariaDB server. + * @return whether the test server is a MariaDB server + */ + public static boolean isMariaDb() { + ensureTestContainerIsRunning(); + try { + return !"MySQL".equalsIgnoreCase(getSharedConnection().getMetaData().getDatabaseProductName()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the server version of the test server. + * @return the server version of the test server + */ + public static ServerVersion getServerVersion() { + ensureTestContainerIsRunning(); + try { + return ServerVersion.parse(getSharedConnection().getMetaData().getDatabaseProductVersion()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a shared jdbc connection to the test server. + * @return a shared jdbc connection to the test server + */ + public static Connection getSharedConnection() { + ensureTestContainerIsRunning(); + if (connection.get() == null) { + connection.compareAndSet(null, getConnection0()); + } + return connection.get(); + } + + private static Connection getConnection0() { + ensureTestContainerIsRunning(); + DataSource source = dataSource.get(); + if (source == null) { + final String connectionString = String.format( + "jdbc:mariadb://%s:%s/%s?user=%s&password=%s", + // should use mariadb to get correct metadata.getDatabaseProductName() + getHost(), + getPort(), + getDatabase(), + getUsername(), + getPassword()); + HikariDataSource hikariDataSource = new HikariDataSource(); + hikariDataSource.setJdbcUrl(connectionString); + dataSource.compareAndSet(null, hikariDataSource); + } + try { + return dataSource.get().getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private static void ensureTestContainerIsRunning() { + if (TestContainerExtension.server == null) { + throw new IllegalStateException("Test server is not configured"); + } + TestContainerExtension.server.start(); // ensure running + } + + private TestServerUtil() { + // Utility class + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtilTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtilTest.java new file mode 100644 index 000000000..ab54aa168 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtilTest.java @@ -0,0 +1,32 @@ +package io.asyncer.r2dbc.mysql.internal.util; + +import io.asyncer.r2dbc.mysql.ServerVersion; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TestContainerExtension.class) +class TestServerUtilTest { + + @Test + void serverVersionTest() { + final boolean useTestContainer = TestContainerExtension.useTestContainer; + final String versionString = TestContainerExtension.dbVersion; + Assumptions.assumeTrue(useTestContainer && containsPatchVersion(versionString)); + Assertions.assertEquals(ServerVersion.parse(versionString), TestServerUtil.getServerVersion()); + } + + @Test + void serverTypeTest() { + final boolean useTestContainer = TestContainerExtension.useTestContainer; + final String dbType = TestContainerExtension.dbType; + Assumptions.assumeTrue(useTestContainer); + Assertions.assertEquals("mariadb".equalsIgnoreCase(dbType), TestServerUtil.isMariaDb()); + } + + private static boolean containsPatchVersion(final String versionString) { + return versionString.indexOf('.') != versionString.lastIndexOf('.'); + } + +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java similarity index 89% rename from src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java index dbaeeb928..ec9127cc7 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java @@ -17,11 +17,11 @@ package io.asyncer.r2dbc.mysql.json; import com.fasterxml.jackson.databind.ObjectMapper; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.CodecContext; -import io.asyncer.r2dbc.mysql.codec.ParametrizedCodec; +import io.asyncer.r2dbc.mysql.codec.ParameterizedCodec; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -41,7 +41,7 @@ /** * A JSON codec based on Jackson. */ -public final class JacksonCodec implements ParametrizedCodec { +public final class JacksonCodec implements ParameterizedCodec { private static final ObjectMapper MAPPER = new ObjectMapper(); @@ -52,7 +52,7 @@ public JacksonCodec(Mode mode) { } @Override - public Object decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Object decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { Charset charset = metadata.getCharCollation(context).getCharset(); @@ -64,7 +64,7 @@ public Object decode(ByteBuf value, MySqlColumnMetadata metadata, Class targe } @Override - public Object decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedType target, boolean binary, + public Object decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { Charset charset = metadata.getCharCollation(context).getCharset(); @@ -81,12 +81,12 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return doCanDecode(metadata); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { return doCanDecode(metadata); } @@ -95,7 +95,7 @@ public boolean canEncode(Object value) { return mode.isEncode(); } - private boolean doCanDecode(MySqlColumnMetadata metadata) { + private boolean doCanDecode(MySqlReadableMetadata metadata) { return mode.isDecode() && (metadata.getType() == MySqlType.JSON || metadata.getType() == MySqlType.TEXT); } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java similarity index 83% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java index 49f2dff65..ed05076fe 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java @@ -18,6 +18,8 @@ import io.asyncer.r2dbc.mysql.Query; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -84,42 +86,42 @@ void badFollowNull() { @Test void appendPart() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.append("define", 2, 5); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'fin'"); } @Test void writePart() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write("define", 2, 3); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'fin'"); } @Test void appendNull() { - assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(parameterOnly(1)).append(null))) + assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(false, parameterOnly(1)).append(null))) .isEqualTo("'null'"); - assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(parameterOnly(1)) + assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(false, parameterOnly(1)) .append(null, 1, 3))) .isEqualTo("'ul'"); } @Test void writeNull() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((String) null); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'null'"); - writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((String) null, 1, 2); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'ul'"); - writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((char[]) null); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'null'"); - writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((char[]) null, 1, 2); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'ul'"); } @@ -132,7 +134,7 @@ void publishSuccess() { values[i] = new MockMySqlParameter(true); } - Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values))) + Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values))) .as(StepVerifier::create) .expectNext(new String(new char[SIZE]).replace("\0", "''")) .verifyComplete(); @@ -154,7 +156,7 @@ void publishPartially() { values[i] = new MockMySqlParameter(false); } - Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values))) + Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values))) .as(StepVerifier::create) .verifyError(MockException.class); @@ -169,13 +171,30 @@ void publishNothing() { values[i] = new MockMySqlParameter(false); } - Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values))) + Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values))) .as(StepVerifier::create) .verifyError(MockException.class); assertThat(values).extracting(MockMySqlParameter::refCnt).containsOnly(0); } + @ParameterizedTest + @ValueSource(strings = { + "abc", + "a'b'c", + "a\nb\rc", + "a\"b\"c", + "a\\b\\c", + "a\0b\0c", + "a\u00a5b\u20a9c", + "a\032b\032c", + }) + void noBackslashEscapes(String value) { + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(true, parameterOnly(1)); + writer.write(value); + assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'" + value.replaceAll("'", "''") + "'"); + } + private static Query parameterOnly(int parameters) { char[] chars = new char[parameters]; Arrays.fill(chars, '?'); @@ -184,13 +203,13 @@ private static Query parameterOnly(int parameters) { } private static ParamWriter stringWriter() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write('0'); return writer; } private static ParamWriter nullWriter() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.writeNull(); return writer; } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java similarity index 93% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java index b532546cc..eacaa349a 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java @@ -45,10 +45,10 @@ public final class ParameterWriterHelper { ReflectionUtils.findMethod(ParamWriter.class, "toSql") .orElseThrow(RuntimeException::new); - public static ParameterWriter get(Query query) { + public static ParameterWriter get(boolean noBackslashEscapes, Query query) { assertThat(query.getPartSize()).isGreaterThan(1); - return ReflectionUtils.newInstance(CONSTRUCTOR, query); + return ReflectionUtils.newInstance(CONSTRUCTOR, noBackslashEscapes, query); } public static String toSql(ParameterWriter writer) { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java new file mode 100644 index 000000000..02a5fdd31 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.message.server; + +import io.asyncer.r2dbc.mysql.ConnectionContext; +import io.asyncer.r2dbc.mysql.ConnectionContextTest; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link OkMessage}. + */ +class OkMessageTest { + + @Test + void decodeSessionVariables() { + boolean isMariaDb = "mariadb".equalsIgnoreCase(System.getProperty("test.db.type")); + ConnectionContext context = ConnectionContextTest.mock(isMariaDb); + OkMessage message = OkMessage.decode(true, sessionVariablesOk(), context); + + assertThat(message.getAffectedRows()).isOne(); + assertThat(message.getLastInsertId()).isEqualTo(2); + assertThat(message.getServerStatuses()).isEqualTo((short) 0x4000); + assertThat(message.getWarnings()).isEqualTo(3); + assertThat(message.getSystemVariable("autocommit")).isEqualTo("OFF"); + } + + private static ByteBuf sessionVariablesOk() { + return Unpooled.wrappedBuffer(new byte[] { + 0, + 1, 2, 0, 0x40, 3, 0, 0, 0x11, 0, 0xf, 0xa, + 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x3, 0x4f, 0x46, 0x46, + }); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java similarity index 80% rename from src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java index c27dac59c..dd47e4678 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java @@ -21,7 +21,10 @@ import io.netty.buffer.Unpooled; import org.assertj.core.api.AbstractObjectAssert; import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -30,21 +33,21 @@ */ class ServerMessageDecoderTest { - @Test - void okAndPreparedOk() { - AbstractObjectAssert ok = assertThat(decode(okLike(), DecodeContext.command())) - .isExactlyInstanceOf(OkMessage.class) - .extracting(message -> (OkMessage) message); + @ParameterizedTest + @MethodSource(value = { "okLikePayload" }) + void okAndPreparedOk(byte[] okLike) { + AbstractObjectAssert ok = assertThat(decode( + Unpooled.wrappedBuffer(okLike), DecodeContext.command() + )).isExactlyInstanceOf(OkMessage.class).extracting(message -> (OkMessage) message); ok.extracting(OkMessage::getAffectedRows).isEqualTo(1L); ok.extracting(OkMessage::getLastInsertId).isEqualTo(0x10000L); // 65536 ok.extracting(OkMessage::getServerStatuses).isEqualTo((short) 0x100); // 256 ok.extracting(OkMessage::getWarnings).isEqualTo(0); - AbstractObjectAssert preparedOk = assertThat(decode(okLike(), - DecodeContext.prepareQuery())) - .isExactlyInstanceOf(PreparedOkMessage.class) - .extracting(message -> (PreparedOkMessage) message); + AbstractObjectAssert preparedOk = assertThat(decode( + Unpooled.wrappedBuffer(okLike), DecodeContext.prepareQuery() + )).isExactlyInstanceOf(PreparedOkMessage.class).extracting(message -> (PreparedOkMessage) message); preparedOk.extracting(PreparedOkMessage::getStatementId).isEqualTo(0xFD01); // 64769 preparedOk.extracting(PreparedOkMessage::getTotalColumns).isEqualTo(1); @@ -56,10 +59,8 @@ private static ServerMessage decode(ByteBuf buf, DecodeContext decodeContext) { return new ServerMessageDecoder().decode(buf, ConnectionContextTest.mock(), decodeContext); } - private static ByteBuf okLike() { - return Unpooled.wrappedBuffer(new byte[] { - 10, 0, 0, // envelope size - 1, // sequence ID + static Stream okLikePayload() { + return Stream.of(new byte[] { 0, // Heading both of OK and Prepared OK 1, // OK: affected rows, Prepared OK: first byte of statement ID (byte) 0xFD, diff --git a/src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension b/r2dbc-mysql/src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension similarity index 100% rename from src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension rename to r2dbc-mysql/src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension diff --git a/src/test/resources/local/stations.csv b/r2dbc-mysql/src/test/resources/local/stations.csv similarity index 100% rename from src/test/resources/local/stations.csv rename to r2dbc-mysql/src/test/resources/local/stations.csv diff --git a/src/test/resources/local/stations.json b/r2dbc-mysql/src/test/resources/local/stations.json similarity index 100% rename from src/test/resources/local/stations.json rename to r2dbc-mysql/src/test/resources/local/stations.json diff --git a/src/test/resources/local/stations.sql b/r2dbc-mysql/src/test/resources/local/stations.sql similarity index 100% rename from src/test/resources/local/stations.sql rename to r2dbc-mysql/src/test/resources/local/stations.sql diff --git a/src/test/resources/local/users.csv b/r2dbc-mysql/src/test/resources/local/users.csv similarity index 100% rename from src/test/resources/local/users.csv rename to r2dbc-mysql/src/test/resources/local/users.csv diff --git a/src/test/resources/local/users.json b/r2dbc-mysql/src/test/resources/local/users.json similarity index 100% rename from src/test/resources/local/users.json rename to r2dbc-mysql/src/test/resources/local/users.json diff --git a/src/test/resources/local/users.sql b/r2dbc-mysql/src/test/resources/local/users.sql similarity index 100% rename from src/test/resources/local/users.sql rename to r2dbc-mysql/src/test/resources/local/users.sql diff --git a/src/test/resources/logback-test.xml b/r2dbc-mysql/src/test/resources/logback-test.xml similarity index 95% rename from src/test/resources/logback-test.xml rename to r2dbc-mysql/src/test/resources/logback-test.xml index c4aa3b8ab..591762720 100644 --- a/src/test/resources/logback-test.xml +++ b/r2dbc-mysql/src/test/resources/logback-test.xml @@ -26,6 +26,7 @@ + diff --git a/r2dbc-mysql/src/test/resources/testcontainer/mysql-5.5/my.cnf b/r2dbc-mysql/src/test/resources/testcontainer/mysql-5.5/my.cnf new file mode 100644 index 000000000..07369ef5b --- /dev/null +++ b/r2dbc-mysql/src/test/resources/testcontainer/mysql-5.5/my.cnf @@ -0,0 +1,49 @@ +[mysqld] +user = mysql +datadir = /var/lib/mysql +port = 3306 +#socket = /tmp/mysql.sock +skip-external-locking +key_buffer_size = 16K +max_allowed_packet = 1M +table_open_cache = 4 +sort_buffer_size = 64K +read_buffer_size = 256K +read_rnd_buffer_size = 256K +net_buffer_length = 2K +skip-host-cache +skip-name-resolve + +# Don't listen on a TCP/IP port at all. This can be a security enhancement, +# if all processes that need to connect to mysqld run on the same host. +# All interaction with mysqld must be made via Unix sockets or named pipes. +# Note that using this option without enabling named pipes on Windows +# (using the "enable-named-pipe" option) will render mysqld useless! +# +#skip-networking +#server-id = 1 + +# Uncomment the following if you want to log updates +#log-bin=mysql-bin + +# binary logging format - mixed recommended +#binlog_format=mixed + +# Causes updates to non-transactional engines using statement format to be +# written directly to binary log. Before using this option make sure that +# there are no dependencies between transactional and non-transactional +# tables such as in the statement INSERT INTO t_myisam SELECT * FROM +# t_innodb; otherwise, slaves may diverge from the master. +#binlog_direct_non_transactional_updates=TRUE + +# Uncomment the following if you are using InnoDB tables +innodb_data_file_path = ibdata1:10M:autoextend +# You can set .._buffer_pool_size up to 50 - 80 % +# of RAM but beware of setting memory usage too high +innodb_buffer_pool_size = 16M +#innodb_additional_mem_pool_size = 2M +# Set .._log_file_size to 25 % of buffer pool size +innodb_log_file_size = 5M +innodb_log_buffer_size = 8M +innodb_flush_log_at_trx_commit = 1 +innodb_lock_wait_timeout = 50 diff --git a/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java b/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java deleted file mode 100644 index 351e47951..000000000 --- a/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import org.junit.platform.commons.annotation.Testable; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; -import org.openjdk.jmh.annotations.Timeout; - -/** - * Benchmark between {@link MySqlNames#compare} and {@link String#compareToIgnoreCase}. - */ -@State(Scope.Benchmark) -@Threads(1) -@Timeout(time = 1) -@Testable -public class MySqlNamesCompareBenchmark extends BenchmarkSupport { - - private static final String LEFT = "This is a test message for testing String compare " + - "with case insensitive or case sensitive"; - - private static final String RIGHT = LEFT.toUpperCase(); - - @Benchmark - @Testable - public int compareCi() { - return MySqlNames.compare(LEFT, RIGHT); - } - - @Benchmark - @Testable - public int nativeCompareCi() { - return String.CASE_INSENSITIVE_ORDER.compare(LEFT, RIGHT); - } - - @Benchmark - @Testable - public int compareCs() { - return MySqlNames.compare(LEFT, LEFT); - } - - @SuppressWarnings("EqualsWithItself") - @Benchmark - @Testable - public int nativeCompareCs() { - return String.CASE_INSENSITIVE_ORDER.compare(LEFT, LEFT); - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java b/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java deleted file mode 100644 index b15de4bc9..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; - -import java.util.AbstractSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Iterator; -import java.util.Objects; -import java.util.Set; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * An implementation of {@link Set}{@code <}{@link String}{@code >} for {@code RowMetadata.getColumnNames} - * results. - * - * @see MySqlNames column name searching rules. - */ -final class ColumnNameSet extends AbstractSet implements Set { - - static final Comparator NAME_COMPARATOR = (left, right) -> - MySqlNames.compare(left.getName(), right.getName()); - - private final String[] originNames; - - private final String[] sortedNames; - - /** - * Construct a {@link ColumnNameSet} by sorted {@code names} without array copy. - * - * @param originNames must be the original order. - * @param sortedNames must be sorted by {@link MySqlNames#compare(String, String)}. - */ - private ColumnNameSet(String[] originNames, String[] sortedNames) { - this.originNames = originNames; - this.sortedNames = sortedNames; - } - - @Override - public boolean contains(Object o) { - if (o instanceof String) { - return findIndex((String) o) >= 0; - } - - return false; - } - - @Override - public Iterator iterator() { - return InternalArrays.asIterator(originNames); - } - - @Override - public int size() { - return originNames.length; - } - - @Override - public boolean isEmpty() { - return originNames.length == 0; - } - - @Override - public Spliterator spliterator() { - return Spliterators.spliterator(this.originNames, - Spliterator.NONNULL | Spliterator.ORDERED | Spliterator.IMMUTABLE); - } - - @Override - public void forEach(Consumer action) { - Objects.requireNonNull(action); - - for (String name : this.originNames) { - action.accept(name); - } - } - - @Override - public String[] toArray() { - return Arrays.copyOf(originNames, originNames.length); - } - - @SuppressWarnings({ "unchecked", "SuspiciousSystemArraycopy" }) - @Override - public T[] toArray(T[] a) { - Objects.requireNonNull(a); - - int size = originNames.length; - - if (a.length < size) { - return (T[]) Arrays.copyOf(originNames, size, a.getClass()); - } else { - System.arraycopy(originNames, 0, a, 0, size); - - if (a.length > size) { - a[size] = null; - } - - return a; - } - } - - @Override - public boolean add(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean addAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.isEmpty()) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @Override - public boolean remove(Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeIf(Predicate filter) { - Objects.requireNonNull(filter); - - for (String name : this.originNames) { - if (filter.test(name)) { - throw new UnsupportedOperationException(); - } - } - - return false; - } - - @Override - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.isEmpty()) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @SuppressWarnings("SuspiciousMethodCalls") - @Override - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.containsAll(this)) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public String toString() { - return Arrays.toString(originNames); - } - - int findIndex(String name) { - return MySqlNames.nameSearch(this.sortedNames, name); - } - - String[] getSortedNames() { - return sortedNames; - } - - static ColumnNameSet of(String name) { - requireNonNull(name, "name must not be null"); - - String[] names = new String[] { name }; - return new ColumnNameSet(names, names); - } - - static ColumnNameSet of(String[] originNames, String[] sortedNames) { - requireNonNull(originNames, "originNames must not be null"); - requireNonNull(sortedNames, "sortedNames must not be null"); - require(originNames.length == sortedNames.length, - "The length of origin names the same as sorted names one"); - - return new ColumnNameSet(originNames, sortedNames); - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java deleted file mode 100644 index 3039ea19b..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.codec.CodecContext; -import io.asyncer.r2dbc.mysql.collation.CharCollation; -import io.asyncer.r2dbc.mysql.constant.ServerStatuses; -import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; -import org.jetbrains.annotations.Nullable; - -import java.nio.file.Path; -import java.time.ZoneId; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * The MySQL connection context considers the behavior of server or client. - *

- * WARNING: Do NOT change any data outside of this project, try configure {@code ConnectionFactoryOptions} or - * {@code MySqlConnectionConfiguration} to control connection context and client behavior. - */ -public final class ConnectionContext implements CodecContext { - - private static final ServerVersion NONE_VERSION = ServerVersion.create(0, 0, 0); - - private volatile int connectionId = -1; - - private volatile ServerVersion serverVersion = NONE_VERSION; - - private final ZeroDateOption zeroDateOption; - - @Nullable - private final Path localInfilePath; - - private final int localInfileBufferSize; - - @Nullable - private ZoneId serverZoneId; - - /** - * Assume that the auto commit is always turned on, it will be set after handshake V10 request message, or - * OK message which means handshake V9 completed. - */ - private volatile short serverStatuses = ServerStatuses.AUTO_COMMIT; - - private volatile Capability capability = null; - - ConnectionContext(ZeroDateOption zeroDateOption, @Nullable Path localInfilePath, - int localInfileBufferSize, @Nullable ZoneId serverZoneId) { - this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null"); - this.localInfilePath = localInfilePath; - this.localInfileBufferSize = localInfileBufferSize; - this.serverZoneId = serverZoneId; - } - - /** - * Get the connection identifier that is specified by server. - * - * @return the connection identifier. - */ - public int getConnectionId() { - return connectionId; - } - - /** - * Initializes this context. - * - * @param connectionId the connection identifier that is specified by server. - * @param version the server version. - * @param capability the connection capabilities. - */ - public void init(int connectionId, ServerVersion version, Capability capability) { - this.connectionId = connectionId; - this.serverVersion = version; - this.capability = capability; - } - - @Override - public ServerVersion getServerVersion() { - return serverVersion; - } - - @Override - public CharCollation getClientCollation() { - return CharCollation.clientCharCollation(); - } - - @Override - public ZoneId getServerZoneId() { - if (serverZoneId == null) { - throw new IllegalStateException("Server timezone have not initialization"); - } - return serverZoneId; - } - - @Override - public boolean isMariaDb() { - return capability.isMariaDb() || serverVersion.isMariaDb(); - } - - boolean shouldSetServerZoneId() { - return serverZoneId == null; - } - - void setServerZoneId(ZoneId serverZoneId) { - if (this.serverZoneId != null) { - throw new IllegalStateException("Server timezone have been initialized"); - } - this.serverZoneId = serverZoneId; - } - - @Override - public ZeroDateOption getZeroDateOption() { - return zeroDateOption; - } - - /** - * Gets the allowed local infile path. - * - * @return the path. - */ - @Nullable - public Path getLocalInfilePath() { - return localInfilePath; - } - - /** - * Gets the local infile buffer size. - * - * @return the buffer size. - */ - public int getLocalInfileBufferSize() { - return localInfileBufferSize; - } - - /** - * Get the bitmap of server statuses. - * - * @return the bitmap. - */ - public short getServerStatuses() { - return serverStatuses; - } - - /** - * Updates server statuses. - * - * @param serverStatuses the bitmap of server statuses. - */ - public void setServerStatuses(short serverStatuses) { - this.serverStatuses = serverStatuses; - } - - /** - * Get the connection capability. Should use it after this context initialized. - * - * @return the connection capability. - */ - public Capability getCapability() { - return capability; - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java b/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java deleted file mode 100644 index 33f8cf551..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.r2dbc.spi.IsolationLevel; - -/** - * An internal interface for check, set and reset connection states. - */ -interface ConnectionState { - - /** - * Sets current isolation level. - * - * @param level current level. - */ - void setIsolationLevel(IsolationLevel level); - - /** - * Reutrns session lock wait timeout. - * - * @return Session lock wait timeout. - */ - long getSessionLockWaitTimeout(); - - /** - * Sets current lock wait timeout. - * - * @param timeoutSeconds seconds of current lock wait timeout. - */ - void setCurrentLockWaitTimeout(long timeoutSeconds); - - /** - * Checks if lock wait timeout has been changed by {@link #setCurrentLockWaitTimeout(long)}. - * - * @return if lock wait timeout changed. - */ - boolean isLockWaitTimeoutChanged(); - - /** - * Resets current isolation level in initial state. - */ - void resetIsolationLevel(); - - /** - * Resets current isolation level in initial state. - */ - void resetCurrentLockWaitTimeout(); - - /** - * Checks if connection is processing a transaction. - * - * @return if in a transaction. - */ - boolean isInTransaction(); -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java deleted file mode 100644 index 65569c5d4..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java +++ /dev/null @@ -1,679 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.cache.PrepareCache; -import io.asyncer.r2dbc.mysql.cache.QueryCache; -import io.asyncer.r2dbc.mysql.client.Client; -import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.asyncer.r2dbc.mysql.constant.ServerStatuses; -import io.asyncer.r2dbc.mysql.internal.util.StringUtils; -import io.asyncer.r2dbc.mysql.message.client.InitDbMessage; -import io.asyncer.r2dbc.mysql.message.client.PingMessage; -import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; -import io.asyncer.r2dbc.mysql.message.server.ErrorMessage; -import io.asyncer.r2dbc.mysql.message.server.ServerMessage; -import io.netty.util.ReferenceCountUtil; -import io.netty.util.internal.logging.InternalLogger; -import io.netty.util.internal.logging.InternalLoggerFactory; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.IsolationLevel; -import io.r2dbc.spi.Lifecycle; -import io.r2dbc.spi.R2dbcNonTransientResourceException; -import io.r2dbc.spi.TransactionDefinition; -import io.r2dbc.spi.ValidationDepth; -import org.jetbrains.annotations.Nullable; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.SynchronousSink; - -import java.time.DateTimeException; -import java.time.Duration; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.function.Predicate; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty; -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * An implementation of {@link Connection} for connecting to the MySQL database. - */ -public final class MySqlConnection implements Connection, Lifecycle, ConnectionState { - - private static final InternalLogger logger = InternalLoggerFactory.getInstance(MySqlConnection.class); - - private static final int DEFAULT_LOCK_WAIT_TIMEOUT = 50; - - private static final String PING_MARKER = "/* ping */"; - - private static final String ZONE_PREFIX_POSIX = "posix/"; - - private static final String ZONE_PREFIX_RIGHT = "right/"; - - private static final int PREFIX_LENGTH = 6; - - private static final ServerVersion MARIA_11_1_1 = ServerVersion.create(11, 1, 1, true); - - private static final ServerVersion MYSQL_8_0_3 = ServerVersion.create(8, 0, 3); - - private static final ServerVersion MYSQL_5_7_20 = ServerVersion.create(5, 7, 20); - - private static final ServerVersion MYSQL_8 = ServerVersion.create(8, 0, 0); - - private static final ServerVersion MYSQL_5_7_4 = ServerVersion.create(5, 7, 4); - - private static final ServerVersion MARIA_10_1_1 = ServerVersion.create(10, 1, 1, true); - - private static final Function VALIDATE = message -> { - if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { - return true; - } - - if (message instanceof ErrorMessage) { - ErrorMessage msg = (ErrorMessage) message; - logger.debug("Remote validate failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), - msg.getMessage()); - } else { - ReferenceCountUtil.safeRelease(message); - } - - return false; - }; - - private static final BiConsumer> PING = (message, sink) -> { - if (message instanceof ErrorMessage) { - sink.next(message); - sink.complete(); - } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { - sink.next(message); - sink.complete(); - } else { - ReferenceCountUtil.safeRelease(message); - } - }; - - private static final BiConsumer> INIT_DB = (message, sink) -> { - if (message instanceof ErrorMessage) { - ErrorMessage msg = (ErrorMessage) message; - logger.debug("Use database failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), - msg.getMessage()); - sink.next(false); - sink.complete(); - } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { - sink.next(true); - sink.complete(); - } else { - ReferenceCountUtil.safeRelease(message); - } - }; - - private static final BiConsumer> INIT_DB_AFTER = (message, sink) -> { - if (message instanceof ErrorMessage) { - sink.error(((ErrorMessage) message).toException()); - } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { - sink.complete(); - } else { - ReferenceCountUtil.safeRelease(message); - } - }; - - private final Client client; - - private final Codecs codecs; - - private final boolean batchSupported; - - private final ConnectionContext context; - - private final MySqlConnectionMetadata metadata; - - private volatile IsolationLevel sessionLevel; - - private final QueryCache queryCache; - - private final PrepareCache prepareCache; - - @Nullable - private final Predicate prepare; - - /** - * Current isolation level inferred by past statements. - *

- * Inference rules: - *

  1. In the beginning, it is also {@link #sessionLevel}.
  2. - *
  3. After the user calls {@link #setTransactionIsolationLevel(IsolationLevel)}, it will change to - * the user-specified value.
  4. - *
  5. After the end of a transaction (commit or rollback), it will recover to {@link #sessionLevel}.
  6. - *
- */ - private volatile IsolationLevel currentLevel; - - /** - * Session lock wait timeout. - */ - private volatile long lockWaitTimeout; - - /** - * Current transaction lock wait timeout. - */ - private volatile long currentLockWaitTimeout; - - MySqlConnection(Client client, ConnectionContext context, Codecs codecs, IsolationLevel level, - long lockWaitTimeout, QueryCache queryCache, PrepareCache prepareCache, @Nullable String product, - @Nullable Predicate prepare) { - this.client = client; - this.context = context; - this.sessionLevel = level; - this.currentLevel = level; - this.codecs = codecs; - this.lockWaitTimeout = lockWaitTimeout; - this.currentLockWaitTimeout = lockWaitTimeout; - this.queryCache = queryCache; - this.prepareCache = prepareCache; - this.metadata = new MySqlConnectionMetadata(context.getServerVersion().toString(), product); - this.batchSupported = context.getCapability().isMultiStatementsAllowed(); - this.prepare = prepare; - - if (this.batchSupported) { - logger.debug("Batch is supported by server"); - } else { - logger.warn("The MySQL server does not support batch, fallback to executing one-by-one"); - } - } - - @Override - public Mono beginTransaction() { - return beginTransaction(MySqlTransactionDefinition.empty()); - } - - @Override - public Mono beginTransaction(TransactionDefinition definition) { - return Mono.defer(() -> QueryFlow.beginTransaction(client, this, batchSupported, definition)); - } - - @Override - public Mono close() { - Mono closer = client.close(); - - if (logger.isDebugEnabled()) { - return closer.doOnSubscribe(s -> logger.debug("Connection closing")) - .doOnSuccess(ignored -> logger.debug("Connection close succeed")); - } - - return closer; - } - - @Override - public Mono commitTransaction() { - return Mono.defer(() -> QueryFlow.doneTransaction(client, this, true, batchSupported)); - } - - @Override - public MySqlBatch createBatch() { - return batchSupported ? new MySqlBatchingBatch(client, codecs, context) : - new MySqlSyntheticBatch(client, codecs, context); - - } - - @Override - public Mono createSavepoint(String name) { - requireNonEmpty(name, "Savepoint name must not be empty"); - - return QueryFlow.createSavepoint(client, this, name, batchSupported); - } - - @Override - public MySqlStatement createStatement(String sql) { - requireNonNull(sql, "sql must not be null"); - - if (sql.startsWith(PING_MARKER)) { - return new PingStatement(this, codecs, context); - } - - Query query = queryCache.get(sql); - - if (query.isSimple()) { - if (prepare != null && prepare.test(sql)) { - logger.debug("Create a simple statement provided by prepare query"); - return new PrepareSimpleStatement(client, codecs, context, sql, prepareCache); - } - - logger.debug("Create a simple statement provided by text query"); - - return new TextSimpleStatement(client, codecs, context, sql); - } - - if (prepare == null) { - logger.debug("Create a parametrized statement provided by text query"); - return new TextParametrizedStatement(client, codecs, query, context); - } - - logger.debug("Create a parametrized statement provided by prepare query"); - - return new PrepareParametrizedStatement(client, codecs, query, context, prepareCache); - } - - @Override - public Mono postAllocate() { - return Mono.empty(); - } - - @Override - public Mono preRelease() { - // Rollback if the connection is in transaction. - return rollbackTransaction(); - } - - @Override - public Mono releaseSavepoint(String name) { - requireNonEmpty(name, "Savepoint name must not be empty"); - - return QueryFlow.executeVoid(client, "RELEASE SAVEPOINT " + StringUtils.quoteIdentifier(name)); - } - - @Override - public Mono rollbackTransaction() { - return Mono.defer(() -> QueryFlow.doneTransaction(client, this, false, batchSupported)); - } - - @Override - public Mono rollbackTransactionToSavepoint(String name) { - requireNonEmpty(name, "Savepoint name must not be empty"); - - return QueryFlow.executeVoid(client, "ROLLBACK TO SAVEPOINT " + StringUtils.quoteIdentifier(name)); - } - - @Override - public MySqlConnectionMetadata getMetadata() { - return metadata; - } - - /** - * MySQL does not have any way to query the isolation level of the current transaction, only inferred from - * past statements, so driver can not make sure the result is right. - *

- * See MySQL Bug 53341 - *

- * {@inheritDoc} - */ - @Override - public IsolationLevel getTransactionIsolationLevel() { - return currentLevel; - } - - /** - * Gets session transaction isolation level(Only for testing). - * - * @return session transaction isolation level. - */ - IsolationLevel getSessionTransactionIsolationLevel() { - return sessionLevel; - } - - @Override - public Mono setTransactionIsolationLevel(IsolationLevel isolationLevel) { - requireNonNull(isolationLevel, "isolationLevel must not be null"); - - // Set subsequent transaction isolation level. - return QueryFlow.executeVoid(client, "SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql()) - .doOnSuccess(ignored -> { - this.sessionLevel = isolationLevel; - if (!this.isInTransaction()) { - this.currentLevel = isolationLevel; - } - }); - } - - @Override - public Mono validate(ValidationDepth depth) { - requireNonNull(depth, "depth must not be null"); - - if (depth == ValidationDepth.LOCAL) { - return Mono.fromSupplier(client::isConnected); - } - - return Mono.defer(() -> { - if (!client.isConnected()) { - return Mono.just(false); - } - - return doPingInternal() - .last() - .map(VALIDATE) - .onErrorResume(e -> { - // `last` maybe emit a NoSuchElementException, exchange maybe emit exception by Netty. - // But should NEVER emit any exception, so logging exception and emit false. - logger.debug("Remote validate failed", e); - return Mono.just(false); - }); - }); - } - - @Override - public boolean isAutoCommit() { - // Within transaction, autocommit remains disabled until end the transaction with COMMIT or ROLLBACK. - // The autocommit mode then reverts to its previous state. - return !isInTransaction() && isSessionAutoCommit(); - } - - @Override - public Mono setAutoCommit(boolean autoCommit) { - return Mono.defer(() -> { - if (autoCommit == isSessionAutoCommit()) { - return Mono.empty(); - } - - return QueryFlow.executeVoid(client, "SET autocommit=" + (autoCommit ? 1 : 0)); - }); - } - - @Override - public void setIsolationLevel(IsolationLevel level) { - this.currentLevel = level; - } - - @Override - public long getSessionLockWaitTimeout() { - return lockWaitTimeout; - } - - @Override - public void setCurrentLockWaitTimeout(long timeoutSeconds) { - this.currentLockWaitTimeout = timeoutSeconds; - } - - @Override - public void resetIsolationLevel() { - this.currentLevel = this.sessionLevel; - } - - @Override - public boolean isLockWaitTimeoutChanged() { - return currentLockWaitTimeout != lockWaitTimeout; - } - - @Override - public void resetCurrentLockWaitTimeout() { - this.currentLockWaitTimeout = this.lockWaitTimeout; - } - - @Override - public boolean isInTransaction() { - return (context.getServerStatuses() & ServerStatuses.IN_TRANSACTION) != 0; - } - - @Override - public Mono setLockWaitTimeout(Duration timeout) { - requireNonNull(timeout, "timeout must not be null"); - - long timeoutSeconds = timeout.getSeconds(); - return QueryFlow.executeVoid(client, "SET innodb_lock_wait_timeout=" + timeoutSeconds) - .doOnSuccess(ignored -> this.lockWaitTimeout = this.currentLockWaitTimeout = timeoutSeconds); - } - - @Override - public Mono setStatementTimeout(Duration timeout) { - requireNonNull(timeout, "timeout must not be null"); - final boolean isMariaDb = context.isMariaDb(); - final ServerVersion serverVersion = context.getServerVersion(); - final long timeoutMs = timeout.toMillis(); - final String sql = isMariaDb ? "SET max_statement_time=" + timeoutMs / 1000.0 - : "SET SESSION MAX_EXECUTION_TIME=" + timeoutMs; - - // mariadb: https://mariadb.com/kb/en/aborting-statements/ - // mysql: https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/ - // ref: https://github.com/mariadb-corporation/mariadb-connector-r2dbc - if (isMariaDb && serverVersion.isGreaterThanOrEqualTo(MARIA_10_1_1) - || !isMariaDb && serverVersion.isGreaterThanOrEqualTo(MYSQL_5_7_4)) { - return QueryFlow.executeVoid(client, sql); - } - - return Mono.error( - new R2dbcNonTransientResourceException( - "Statement timeout is not supported by server version " + serverVersion, - "HY000", - -1, - sql - ) - ); - } - - Flux doPingInternal() { - return client.exchange(PingMessage.INSTANCE, PING); - } - - boolean isSessionAutoCommit() { - return (context.getServerStatuses() & ServerStatuses.AUTO_COMMIT) != 0; - } - - /** - * Initialize a {@link MySqlConnection} after login. - * - * @param client must be logged-in. - * @param codecs the {@link Codecs}. - * @param context must be initialized. - * @param database the database that should be lazy init. - * @param queryCache the cache of {@link Query}. - * @param prepareCache the cache of server-preparing result. - * @param prepare judging for prefer use prepare statement to execute simple query. - * @return a {@link Mono} will emit an initialized {@link MySqlConnection}. - */ - static Mono init( - Client client, Codecs codecs, ConnectionContext context, String database, - QueryCache queryCache, PrepareCache prepareCache, - @Nullable Predicate prepare - ) { - StringBuilder query = new StringBuilder(128) - .append("SELECT ") - .append(transactionIsolationColumn(context)) - .append(",@@innodb_lock_wait_timeout AS l,@@version_comment AS v"); - - Function> handler; - - if (context.shouldSetServerZoneId()) { - query.append(",@@system_time_zone AS s,@@time_zone AS t"); - handler = MySqlConnection::fullInit; - } else { - handler = MySqlConnection::init; - } - - Mono connection = new TextSimpleStatement(client, codecs, context, query.toString()) - .execute() - .flatMap(handler) - .last() - .map(data -> { - ZoneId serverZoneId = data.serverZoneId; - if (serverZoneId != null) { - logger.debug("Set server time zone to {} from init query", serverZoneId); - context.setServerZoneId(serverZoneId); - } - - return new MySqlConnection(client, context, codecs, data.level, data.lockWaitTimeout, - queryCache, prepareCache, data.product, prepare); - }); - - if (database.isEmpty()) { - return connection; - } - - requireNonEmpty(database, "database must not be empty"); - - return connection.flatMap(conn -> client.exchange(new InitDbMessage(database), INIT_DB) - .last() - .flatMap(success -> { - if (success) { - return Mono.just(conn); - } - - String sql = "CREATE DATABASE IF NOT EXISTS " + StringUtils.quoteIdentifier(database); - - return QueryFlow.executeVoid(client, sql) - .then(client.exchange(new InitDbMessage(database), INIT_DB_AFTER).then(Mono.just(conn))); - })); - } - - private static Publisher init(MySqlResult r) { - return r.map((row, meta) -> new InitData(convertIsolationLevel(row.get(0, String.class)), - convertLockWaitTimeout(row.get(1, Long.class)), - row.get(2, String.class), null)); - } - - private static Publisher fullInit(MySqlResult r) { - return r.map((row, meta) -> { - IsolationLevel level = convertIsolationLevel(row.get(0, String.class)); - long lockWaitTimeout = convertLockWaitTimeout(row.get(1, Long.class)); - String product = row.get(2, String.class); - String systemTimeZone = row.get(3, String.class); - String timeZone = row.get(4, String.class); - ZoneId zoneId; - - if (timeZone == null || timeZone.isEmpty() || "SYSTEM".equalsIgnoreCase(timeZone)) { - if (systemTimeZone == null || systemTimeZone.isEmpty()) { - logger.warn("MySQL does not return any timezone, trying to use system default timezone"); - zoneId = ZoneId.systemDefault(); - } else { - zoneId = convertZoneId(systemTimeZone); - } - } else { - zoneId = convertZoneId(timeZone); - } - - return new InitData(level, lockWaitTimeout, product, zoneId); - }); - } - - /** - * Creates a {@link ZoneId} from MySQL timezone result, or fallback to system default timezone if not - * found. - * - * @param id the ID/name of MySQL timezone. - * @return the {@link ZoneId}. - */ - private static ZoneId convertZoneId(String id) { - String realId; - - if (id.startsWith(ZONE_PREFIX_POSIX) || id.startsWith(ZONE_PREFIX_RIGHT)) { - realId = id.substring(PREFIX_LENGTH); - } else { - realId = id; - } - - try { - switch (realId) { - case "Factory": - // Looks like the "Factory" time zone is UTC. - return ZoneOffset.UTC; - case "America/Nuuk": - // They are same timezone including DST. - return ZoneId.of("America/Godthab"); - case "ROC": - // Republic of China, 1912-1949, very very old time zone. - // Even the ZoneId.SHORT_IDS does not support it. - // Is there anyone using this time zone, really? - // Don't think so, but should support it for compatible. - // Just use GMT+8, id is equal to +08:00. - return ZoneId.of("+8"); - } - - return ZoneId.of(realId, ZoneId.SHORT_IDS); - } catch (DateTimeException e) { - logger.warn("The server timezone is unknown <{}>, trying to use system default timezone", id, e); - - return ZoneId.systemDefault(); - } - } - - private static IsolationLevel convertIsolationLevel(@Nullable String name) { - if (name == null) { - logger.warn("Isolation level is null in current session, fallback to repeatable read"); - - return IsolationLevel.REPEATABLE_READ; - } - - switch (name) { - case "READ-UNCOMMITTED": - return IsolationLevel.READ_UNCOMMITTED; - case "READ-COMMITTED": - return IsolationLevel.READ_COMMITTED; - case "REPEATABLE-READ": - return IsolationLevel.REPEATABLE_READ; - case "SERIALIZABLE": - return IsolationLevel.SERIALIZABLE; - } - - logger.warn("Unknown isolation level {} in current session, fallback to repeatable read", name); - - return IsolationLevel.REPEATABLE_READ; - } - - private static long convertLockWaitTimeout(@Nullable Long timeout) { - if (timeout == null) { - logger.error("Lock wait timeout is null, fallback to " + DEFAULT_LOCK_WAIT_TIMEOUT + " seconds"); - - return DEFAULT_LOCK_WAIT_TIMEOUT; - } - - return timeout; - } - - /** - * Resolves the column of session isolation level, the {@literal @@tx_isolation} has been marked as - * deprecated. - *

- * If server is MariaDB, {@literal @@transaction_isolation} is used starting from {@literal 11.1.1}. - *

- * If the server is MySQL, use {@literal @@transaction_isolation} starting from {@literal 8.0.3}, or - * between {@literal 5.7.20} and {@literal 8.0.0} (exclusive). - */ - private static String transactionIsolationColumn(ConnectionContext context) { - ServerVersion version = context.getServerVersion(); - - if (context.isMariaDb()) { - return version.isGreaterThanOrEqualTo(MARIA_11_1_1) ? "@@transaction_isolation AS i" : - "@@tx_isolation AS i"; - } - - return version.isGreaterThanOrEqualTo(MYSQL_8_0_3) || - (version.isGreaterThanOrEqualTo(MYSQL_5_7_20) && version.isLessThan(MYSQL_8)) ? - "@@transaction_isolation AS i" : "@@tx_isolation AS i"; - } - - private static class InitData { - - private final IsolationLevel level; - - private final long lockWaitTimeout; - - @Nullable - private final String product; - - @Nullable - private final ZoneId serverZoneId; - - private InitData(IsolationLevel level, long lockWaitTimeout, @Nullable String product, - @Nullable ZoneId serverZoneId) { - this.level = level; - this.lockWaitTimeout = lockWaitTimeout; - this.product = product; - this.serverZoneId = serverZoneId; - } - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java deleted file mode 100644 index 6070d195c..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.cache.Caches; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; -import io.asyncer.r2dbc.mysql.cache.QueryCache; -import io.asyncer.r2dbc.mysql.client.Client; -import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.asyncer.r2dbc.mysql.codec.CodecsBuilder; -import io.asyncer.r2dbc.mysql.constant.SslMode; -import io.asyncer.r2dbc.mysql.extension.CodecRegistrar; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.unix.DomainSocketAddress; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.ConnectionFactoryMetadata; -import org.jetbrains.annotations.Nullable; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.util.Objects; -import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * An implementation of {@link ConnectionFactory} for creating connections to a MySQL database. - */ -public final class MySqlConnectionFactory implements ConnectionFactory { - - private final Mono client; - - private MySqlConnectionFactory(Mono client) { - this.client = client; - } - - @Override - public Mono create() { - return client; - } - - @Override - public ConnectionFactoryMetadata getMetadata() { - return MySqlConnectionFactoryMetadata.INSTANCE; - } - - /** - * Creates a {@link MySqlConnectionFactory} with a {@link MySqlConnectionConfiguration}. - * - * @param configuration the {@link MySqlConnectionConfiguration}. - * @return configured {@link MySqlConnectionFactory}. - */ - public static MySqlConnectionFactory from(MySqlConnectionConfiguration configuration) { - requireNonNull(configuration, "configuration must not be null"); - - LazyQueryCache queryCache = new LazyQueryCache(configuration.getQueryCacheSize()); - - return new MySqlConnectionFactory(Mono.defer(() -> { - MySqlSslConfiguration ssl; - SocketAddress address; - - if (configuration.isHost()) { - ssl = configuration.getSsl(); - address = InetSocketAddress.createUnresolved(configuration.getDomain(), - configuration.getPort()); - } else { - ssl = MySqlSslConfiguration.disabled(); - address = new DomainSocketAddress(configuration.getDomain()); - } - - String database = configuration.getDatabase(); - boolean createDbIfNotExist = configuration.isCreateDatabaseIfNotExist(); - String user = configuration.getUser(); - CharSequence password = configuration.getPassword(); - SslMode sslMode = ssl.getSslMode(); - ConnectionContext context = new ConnectionContext( - configuration.getZeroDateOption(), - configuration.getLoadLocalInfilePath(), - configuration.getLocalInfileBufferSize(), - configuration.getServerZoneId() - ); - Extensions extensions = configuration.getExtensions(); - Predicate prepare = configuration.getPreferPrepareStatement(); - int prepareCacheSize = configuration.getPrepareCacheSize(); - Publisher passwordPublisher = configuration.getPasswordPublisher(); - - if (Objects.nonNull(passwordPublisher)) { - return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( - configuration, queryCache, - ssl, address, - database, createDbIfNotExist, - user, sslMode, context, - extensions, prepare, - prepareCacheSize, token - )); - } - - return getMySqlConnection( - configuration, queryCache, - ssl, address, - database, createDbIfNotExist, - user, sslMode, context, - extensions, prepare, - prepareCacheSize, password - ); - })); - } - - private static Mono getMySqlConnection( - final MySqlConnectionConfiguration configuration, - final LazyQueryCache queryCache, - final MySqlSslConfiguration ssl, - final SocketAddress address, - final String database, - final boolean createDbIfNotExist, - final String user, - final SslMode sslMode, - final ConnectionContext context, - final Extensions extensions, - @Nullable final Predicate prepare, - final int prepareCacheSize, - @Nullable final CharSequence password) { - return Client.connect(ssl, address, configuration.isTcpKeepAlive(), configuration.isTcpNoDelay(), - context, configuration.getConnectTimeout()) - .flatMap(client -> { - // Lazy init database after handshake/login - String db = createDbIfNotExist ? "" : database; - return QueryFlow.login(client, sslMode, db, user, password, context); - }) - .flatMap(client -> { - ByteBufAllocator allocator = client.getByteBufAllocator(); - CodecsBuilder builder = Codecs.builder(allocator); - PrepareCache prepareCache = Caches.createPrepareCache(prepareCacheSize); - String db = createDbIfNotExist ? database : ""; - - extensions.forEach(CodecRegistrar.class, registrar -> - registrar.register(allocator, builder)); - - return MySqlConnection.init(client, builder.build(), context, db, queryCache.get(), - prepareCache, prepare); - }); - } - - private static final class LazyQueryCache { - - private final int capacity; - - private final ReentrantLock lock = new ReentrantLock(); - - @Nullable - private volatile QueryCache cache; - - private LazyQueryCache(int capacity) { - this.capacity = capacity; - } - - public QueryCache get() { - QueryCache cache = this.cache; - if (cache == null) { - lock.lock(); - try { - if ((cache = this.cache) == null) { - this.cache = cache = Caches.createQueryCache(capacity); - } - return cache; - } finally { - lock.unlock(); - } - } - return cache; - } - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java deleted file mode 100644 index eba16df09..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.constant.SslMode; -import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; -import io.netty.handler.ssl.SslContextBuilder; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.ConnectionFactoryOptions; -import io.r2dbc.spi.ConnectionFactoryProvider; -import io.r2dbc.spi.Option; -import org.reactivestreams.Publisher; - -import javax.net.ssl.HostnameVerifier; -import java.time.Duration; -import java.time.ZoneId; -import java.util.function.Function; -import java.util.function.Predicate; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; -import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT; -import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; -import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; -import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; -import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; -import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; -import static io.r2dbc.spi.ConnectionFactoryOptions.SSL; -import static io.r2dbc.spi.ConnectionFactoryOptions.USER; - -/** - * An implementation of {@link ConnectionFactoryProvider} for creating {@link MySqlConnectionFactory}s. - */ -public final class MySqlConnectionFactoryProvider implements ConnectionFactoryProvider { - - /** - * The name of the driver used for discovery, should not be changed. - */ - public static final String MYSQL_DRIVER = "mysql"; - - /** - * Option to set the Unix Domain Socket. - * - * @since 0.8.1 - */ - public static final Option UNIX_SOCKET = Option.valueOf("unixSocket"); - - /** - * Option to set {@link ZoneId} of server. If it is set, driver will ignore the real time zone of - * server-side. - * - * @since 0.8.2 - */ - public static final Option SERVER_ZONE_ID = Option.valueOf("serverZoneId"); - - /** - * Option to configure handling when MySQL server returning "zero date" (aka. "0000-00-00 00:00:00") - * - * @since 0.8.1 - */ - public static final Option ZERO_DATE = Option.valueOf("zeroDate"); - - /** - * Option to {@link SslMode}. - * - * @since 0.8.1 - */ - public static final Option SSL_MODE = Option.valueOf("sslMode"); - - /** - * Option to configure {@link HostnameVerifier}. It is available only if the {@link #SSL_MODE} set to - * {@link SslMode#VERIFY_IDENTITY}. It can be an implementation class name of {@link HostnameVerifier} - * with a public no-args constructor. - * - * @since 0.8.2 - */ - public static final Option SSL_HOSTNAME_VERIFIER = - Option.valueOf("sslHostnameVerifier"); - - /** - * Option to TLS versions for SslContext protocols, see also {@code TlsVersions}. Usually sorted from - * higher to lower. It can be a {@code Collection}. It can be a {@link String}, protocols will be - * split by {@code ,}. e.g. "TLSv1.2,TLSv1.1,TLSv1". - * - * @since 0.8.1 - */ - public static final Option TLS_VERSION = Option.valueOf("tlsVersion"); - - /** - * Option to set a PEM file of server SSL CA. It will be used to verify server certificates. And it will - * be used only if {@link #SSL_MODE} set to {@link SslMode#VERIFY_CA} or higher level. - * - * @since 0.8.1 - */ - public static final Option SSL_CA = Option.valueOf("sslCa"); - - /** - * Option to set a PEM file of client SSL key. - * - * @since 0.8.1 - */ - public static final Option SSL_KEY = Option.valueOf("sslKey"); - - /** - * Option to set a PEM file password of client SSL key. It will be used only if {@link #SSL_KEY} and - * {@link #SSL_CERT} set. - * - * @since 0.8.1 - */ - public static final Option SSL_KEY_PASSWORD = Option.sensitiveValueOf("sslKeyPassword"); - - /** - * Option to set a PEM file of client SSL cert. - * - * @since 0.8.1 - */ - public static final Option SSL_CERT = Option.valueOf("sslCert"); - - /** - * Option to custom {@link SslContextBuilder}. It can be an implementation class name of {@link Function} - * with a public no-args constructor. - * - * @since 0.8.2 - */ - public static final Option> - SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer"); - - /** - * Enable/Disable TCP KeepAlive. - * - * @since 0.8.2 - */ - public static final Option TCP_KEEP_ALIVE = Option.valueOf("tcpKeepAlive"); - - /** - * Enable/Disable TCP NoDelay. - * - * @since 0.8.2 - */ - public static final Option TCP_NO_DELAY = Option.valueOf("tcpNoDelay"); - - /** - * Enable/Disable database creation if not exist. - * - * @since 1.0.6 - */ - public static final Option CREATE_DATABASE_IF_NOT_EXIST = - Option.valueOf("createDatabaseIfNotExist"); - - /** - * Enable server preparing for parametrized statements and prefer server preparing simple statements. - *

- * The value can be a {@link Boolean}. If it is {@code true}, driver will use server preparing for - * parametrized statements and text query for simple statements. If it is {@code false}, driver will use - * client preparing for parametrized statements and text query for simple statements. - *

- * The value can be a {@link Predicate}{@code <}{@link String}{@code >}. If it is set, driver will server - * preparing for parametrized statements, it configures whether to prefer prepare execution on a - * statement-by-statement basis (simple statements). The {@link Predicate}{@code <}{@link String}{@code >} - * accepts the simple SQL query string and returns a {@code boolean} flag indicating preference. - *

- * The value can be a {@link String}. If it is set, driver will try to convert it to {@link Boolean} or an - * instance of {@link Predicate}{@code <}{@link String}{@code >} which use reflection with a public - * no-args constructor. - * - * @since 0.8.1 - */ - public static final Option USE_SERVER_PREPARE_STATEMENT = - Option.valueOf("useServerPrepareStatement"); - - public static final Option ALLOW_LOAD_LOCAL_INFILE_IN_PATH = - Option.valueOf("allowLoadLocalInfileInPath"); - - /** - * Option to set the maximum size of the {@link Query} parsing cache. Default to {@code 256}. - * - * @since 0.8.3 - */ - public static final Option PREPARE_CACHE_SIZE = Option.valueOf("prepareCacheSize"); - - /** - * Option to set the maximum size of the server-preparing cache. Default to {@code 0}. - * - * @since 0.8.3 - */ - public static final Option QUERY_CACHE_SIZE = Option.valueOf("queryCacheSize"); - - /** - * Enable/Disable auto-detect driver extensions. - * - * @since 0.8.2 - */ - public static final Option AUTODETECT_EXTENSIONS = Option.valueOf("autodetectExtensions"); - - /** - * Password Publisher function can be used to retrieve password before creating a connection. - * This can be used with Amazon RDS Aurora IAM authentication, wherein it requires token to be generated. - * The token is valid for 15 minutes, and this token will be used as password. - * - */ - public static final Option> PASSWORD_PUBLISHER = Option.valueOf("passwordPublisher"); - - @Override - public ConnectionFactory create(ConnectionFactoryOptions options) { - requireNonNull(options, "connectionFactoryOptions must not be null"); - - return MySqlConnectionFactory.from(setup(options)); - } - - @Override - public boolean supports(ConnectionFactoryOptions options) { - requireNonNull(options, "connectionFactoryOptions must not be null"); - return MYSQL_DRIVER.equals(options.getValue(DRIVER)); - } - - @Override - public String getDriver() { - return MYSQL_DRIVER; - } - - /** - * Visible for unit tests. - * - * @param options the {@link ConnectionFactoryOptions} for setup {@link MySqlConnectionConfiguration}. - * @return completed {@link MySqlConnectionConfiguration}. - */ - static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { - OptionMapper mapper = new OptionMapper(options); - MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder(); - - mapper.requires(USER).asString() - .to(builder::user); - mapper.optional(PASSWORD).asPassword() - .to(builder::password); - mapper.optional(UNIX_SOCKET).asString() - .to(builder::unixSocket) - .otherwise(() -> setupHost(builder, mapper)); - mapper.optional(SERVER_ZONE_ID).as(ZoneId.class, id -> ZoneId.of(id, ZoneId.SHORT_IDS)) - .to(builder::serverZoneId); - mapper.optional(TCP_KEEP_ALIVE).asBoolean() - .to(builder::tcpKeepAlive); - mapper.optional(TCP_NO_DELAY).asBoolean() - .to(builder::tcpNoDelay); - mapper.optional(ZERO_DATE) - .as(ZeroDateOption.class, id -> ZeroDateOption.valueOf(id.toUpperCase())) - .to(builder::zeroDateOption); - mapper.optional(USE_SERVER_PREPARE_STATEMENT).prepare(builder::useClientPrepareStatement, - builder::useServerPrepareStatement, builder::useServerPrepareStatement); - mapper.optional(ALLOW_LOAD_LOCAL_INFILE_IN_PATH).asString() - .to(builder::allowLoadLocalInfileInPath); - mapper.optional(QUERY_CACHE_SIZE).asInt() - .to(builder::queryCacheSize); - mapper.optional(PREPARE_CACHE_SIZE).asInt() - .to(builder::prepareCacheSize); - mapper.optional(AUTODETECT_EXTENSIONS).asBoolean() - .to(builder::autodetectExtensions); - mapper.optional(CONNECT_TIMEOUT).as(Duration.class, Duration::parse) - .to(builder::connectTimeout); - mapper.optional(DATABASE).asString() - .to(builder::database); - mapper.optional(CREATE_DATABASE_IF_NOT_EXIST).asBoolean() - .to(builder::createDatabaseIfNotExist); - mapper.optional(PASSWORD_PUBLISHER).as(Publisher.class) - .to(builder::passwordPublisher); - - return builder.build(); - } - - /** - * Set builder of {@link MySqlConnectionConfiguration} for hostname-based address with SSL - * configurations. - * - * @param builder the builder of {@link MySqlConnectionConfiguration}. - * @param mapper the {@link OptionMapper} of {@code options}. - */ - private static void setupHost(MySqlConnectionConfiguration.Builder builder, OptionMapper mapper) { - mapper.requires(HOST).asString() - .to(builder::host); - mapper.optional(PORT).asInt() - .to(builder::port); - mapper.optional(SSL).asBoolean() - .to(isSsl -> builder.sslMode(isSsl ? SslMode.REQUIRED : SslMode.DISABLED)); - mapper.optional(SSL_MODE).as(SslMode.class, id -> SslMode.valueOf(id.toUpperCase())) - .to(builder::sslMode); - mapper.optional(TLS_VERSION).asStrings() - .to(builder::tlsVersion); - mapper.optional(SSL_HOSTNAME_VERIFIER).as(HostnameVerifier.class) - .to(builder::sslHostnameVerifier); - mapper.optional(SSL_CERT).asString() - .to(builder::sslCert); - mapper.optional(SSL_KEY).asString() - .to(builder::sslKey); - mapper.optional(SSL_KEY_PASSWORD).asPassword() - .to(builder::sslKeyPassword); - mapper.optional(SSL_CONTEXT_BUILDER_CUSTOMIZER).as(Function.class) - .to(builder::sslContextBuilderCustomizer); - mapper.optional(SSL_CA).asString() - .to(builder::sslCa); - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java deleted file mode 100644 index 40027ea04..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -/** - * A utility considers column names searching logic which use a special compare rule for sort and special - * binary search. - * - *
  • Sort: compare with case insensitive first, then compare with case sensitive when they equals by - * case insensitive.
  • - *
  • Search: find with case sensitive first, then find with case insensitive when not found in case - * sensitive.
- *

- * For example: - * Sort first: abc AB a Abc Ab ABC A ab b B -> A a B b AB Ab ab ABC Abc abc - * Then find "aB" use the same compare rule, - * - * @see #compare(String, String) - */ -final class MySqlNames { - - /** - * Find the best match of target string. This means that if it cannot find case-sensitive content, it will - * try to find with case-insensitive. If the target string is enclosed by {@literal `} and contains at - * least 1 character in quotes, it will find with case-sensitive only. - * - * @param names column names ordered by {@link #compare}. - * @param name the target string. - * @return found index, or a negative integer means not found. - */ - static int nameSearch(String[] names, String name) { - int size = name.length(); - return binarySearch(names, name, size <= 2 || name.charAt(0) != '`' || name.charAt(size - 1) != '`'); - } - - private static int binarySearch(String[] names, String name, boolean ignoreCase) { - int left = 0, right = names.length - 1, middle, compared; - int nameStart = ignoreCase ? 0 : 1, nameEnd = ignoreCase ? name.length() : name.length() - 1; - int ciResult = -1; - String value; - - while (left <= right) { - // `left + (right - left) / 2` for ensure no overflow, - // `left + (right - left) / 2` = `(left + right) >>> 1` - // when `left` and `right` is not negative integer. - // And `left` must greater or equals than 0, - // `right` greater then or equals to `left`. - middle = (left + right) >>> 1; - value = names[middle]; - compared = compare0(value, name, nameStart, nameEnd); - - if (compared < 0) { - left = middle + 1; - - if (compared == -2) { - // Match succeed if case insensitive, always use last - // matched result that will be closer to `name`. - ciResult = middle; - } - } else if (compared > 0) { - right = middle - 1; - - if (compared == 2) { - // Match succeed if case insensitive, always use last - // matched result that will be closer to `name`. - ciResult = middle; - } - } else { - // Match succeed when case sensitive, just return. - return middle; - } - } - - return ignoreCase ? ciResult : -1; - } - - /** - * Compares double strings and return an integer of both difference. If the integer is {@code 0} means - * both strings equals even case sensitive, absolute value is {@code 2} means it is equals by case - * insensitive but not equals when case sensitive, absolute value is {@code 4} means it is not equals even - * case insensitive. - *

- * Note: visible for unit tests. - * - * @param left the {@link String} of left. - * @param right the {@link String} of right. - * @return an integer of both difference. - */ - static int compare(String left, String right) { - return compare0(left, right, 0, right.length()); - } - - private static int compare0(String left, String right, int start, int end) { - int leftSize = left.length(), rightSize = end - start; - int minSize = Math.min(leftSize, rightSize); - // Case sensitive comparator result. - int csCompared = 0; - char leftCh, rightCh; - - for (int i = 0; i < minSize; i++) { - leftCh = left.charAt(i); - rightCh = right.charAt(i + start); - - if (leftCh != rightCh) { - if (csCompared == 0) { - // Compare end if is case sensitive comparator. - csCompared = leftCh - rightCh; - } - - // Use `Character.toLowerCase` for all latin alphabets, not just ASCII. - leftCh = Character.toLowerCase(leftCh); - rightCh = Character.toLowerCase(rightCh); - - if (leftCh != rightCh) { - // Not equals even case insensitive. - return leftCh < rightCh ? -4 : 4; - } - } - } - - // Length not equals means both strings not equals even case insensitive. - if (leftSize != rightSize) { - return leftSize < rightSize ? -4 : 4; - } - - // Equals when case insensitive, use case sensitive. - return csCompared < 0 ? -2 : (csCompared > 0 ? 2 : 0); - } - - private MySqlNames() { } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java deleted file mode 100644 index a228681f1..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.r2dbc.spi.Statement; -import reactor.core.publisher.Flux; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * A strongly typed implementation of {@link Statement} for the MySQL database. - */ -public interface MySqlStatement extends Statement { - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement add(); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bind(int index, Object value); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bind(String name, Object value); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bindNull(int index, Class type); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bindNull(String name, Class type); - - /** - * {@inheritDoc} - */ - @Override - Flux execute(); - - /** - * {@inheritDoc} - */ - @Override - default MySqlStatement returnGeneratedValues(String... columns) { - requireNonNull(columns, "columns must not be null"); - return this; - } - - /** - * {@inheritDoc} - */ - @Override - default MySqlStatement fetchSize(int rows) { - require(rows >= 0, "Fetch size must be greater or equal to zero"); - return this; - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java deleted file mode 100644 index 4ef11772c..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -/** - * A metadata descriptor considers MySQL types. - */ -public final class MySqlTypeMetadata { - - private final int id; - - private final ColumnDefinition definition; - - MySqlTypeMetadata(int id, ColumnDefinition definition) { - this.id = id; - this.definition = definition; - } - - /** - * Get the native type identifier. - * - * @return the native type identifier. - */ - public int getId() { - return id; - } - - /** - * Get the {@link ColumnDefinition} that potentially exposes more type differences. - * - * @return the column definitions. - */ - public ColumnDefinition getDefinition() { - return definition; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof MySqlTypeMetadata)) { - return false; - } - - MySqlTypeMetadata that = (MySqlTypeMetadata) o; - - return id == that.id && definition.equals(that.definition); - } - - @Override - public int hashCode() { - return 31 * id + definition.hashCode(); - } - - @Override - public String toString() { - return "MySqlTypeMetadata(" + id + ", " + definition + ')'; - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/EnvelopeSlicer.java b/src/main/java/io/asyncer/r2dbc/mysql/client/EnvelopeSlicer.java deleted file mode 100644 index 5e6c2bb51..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/EnvelopeSlicer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.client; - -import io.asyncer.r2dbc.mysql.constant.Envelopes; -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.codec.LengthFieldBasedFrameDecoder; - -import java.nio.ByteOrder; - -/** - * Slice server message envelope of MySQL protocol. - */ -final class EnvelopeSlicer extends LengthFieldBasedFrameDecoder { - - static final String NAME = "R2dbcMySqlEnvelopeSlicer"; - - EnvelopeSlicer() { - super(ByteOrder.LITTLE_ENDIAN, Envelopes.MAX_ENVELOPE_SIZE + Envelopes.PART_HEADER_SIZE, 0, - Envelopes.SIZE_FIELD_SIZE, - 1, // byte size of sequence Id field - 0, // do NOT strip header - true - ); - } - - /** - * Override this method because {@code ByteBuf.order(order)} will create temporary {@code SwappedByteBuf}, - * and {@code ByteBuf.order(order)} has also been deprecated. - *

- * {@inheritDoc} - */ - @Override - protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) { - if (length != Envelopes.SIZE_FIELD_SIZE || order != ByteOrder.LITTLE_ENDIAN) { - // impossible length or order, only BUG or hack of reflect - throw new DecoderException("Unsupported lengthFieldLength: " + length + - " (only 3) or byteOrder: " + order + " (only LITTLE_ENDIAN)"); - } - - return buf.getUnsignedMediumLE(offset); - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java b/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java deleted file mode 100644 index c5bfb2408..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.codec; - -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; -import io.netty.buffer.ByteBuf; - -/** - * Base class considers primitive class for {@link Codec} implementations. It should be an internal - * abstraction. - *

- * Primitive types should never return {@code null} when decoding. - * - * @param the boxed type that is handled by this codec. - */ -interface PrimitiveCodec extends Codec { - - /** - * Decode a {@link ByteBuf} as specified {@link Class}. - * - * @param value the {@link ByteBuf}. - * @param metadata the metadata of the column. - * @param target the specified {@link Class}, which can be a primitive type. - * @param binary if the value should be decoded by binary protocol. - * @param context the codec context. - * @return the decoded data that is boxed. - */ - @Override - T decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, - CodecContext context); - - /** - * Check if can decode the field value as a primitive data. - * - * @param metadata the metadata of the column. - * @return if can decode. - */ - boolean canPrimitiveDecode(MySqlColumnMetadata metadata); - - /** - * Get the primitive {@link Class}, such as {@link Integer#TYPE}, etc. - * - * @return the primitive {@link Class}. - */ - Class getPrimitiveClass(); -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/ColumnDefinitions.java b/src/main/java/io/asyncer/r2dbc/mysql/constant/ColumnDefinitions.java deleted file mode 100644 index a5e98d257..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/constant/ColumnDefinitions.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.constant; - -/** - * Column definition flags. - */ -public final class ColumnDefinitions { - - /** - * The data is not null. - */ - public static final int NOT_NULL = 1; - -// public static final int PRIMARY_PART = 1 << 1; // This field is a part of the primary key -// public static final int UNIQUE_PART = 1 << 2; // This field is a part of a unique key -// public static final int KEY_PART = 1 << 3; // This field is a part of a normal key -// public static final int BLOB = 1 << 4; - - /** - * The data is an unsigned number. Only applicable to numeric types, like BIGINT UNSIGNED, INT UNSIGNED, - * etc. - *

- * Note: IEEE-754 floating types (e.g. DOUBLE/FLOAT) do not supports it in MySQL 8.0+. When creating a - * column as an unsigned floating type, the server may report a warning. - */ - public static final int UNSIGNED = 1 << 5; - -// public static final int ZEROFILL = 1 << 6; -// public static final int BINARY = 1 << 7; - - /** - * The real type of this field is ENUMERABLE. - *

- * Note: in order to be compatible with older drivers, MySQL server will send type as VARCHAR for type - * ENUMERABLE. If this flag is enabled, change data type to ENUMERABLE. - */ - public static final int ENUMERABLE = 1 << 8; - -// public static final int AUTO_INCREMENT = 1 << 9; -// public static final int TIMESTAMP = 1 << 10; - - /** - * The real type of this field is SET. - *

- * Note: in order to be compatible with older drivers, MySQL server will send type as VARCHAR for type - * SET. If this flag is enabled, change data type to SET. - */ - public static final int SET = 1 << 11; // type is set - -// public static final int NO_DEFAULT = 1 << 12; // column has no default value -// public static final int ON_UPDATE_NOW = 1 << 13; // field will be set to NOW() in UPDATE statement - - private ColumnDefinitions() { } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java deleted file mode 100644 index 34f6bacd8..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2024 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.internal.util; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty; - -/** - * A utility for processing {@link String} in MySQL/MariaDB. - */ -public final class StringUtils { - - private static final char QUOTE = '`'; - - /** - * Quotes identifier with backticks, it will escape backticks in the identifier. - * - * @param identifier the identifier - * @return quoted identifier - */ - public static String quoteIdentifier(String identifier) { - requireNonEmpty(identifier, "identifier must not be empty"); - - int index = identifier.indexOf(QUOTE); - - if (index == -1) { - return QUOTE + identifier + QUOTE; - } - - int len = identifier.length(); - StringBuilder builder = new StringBuilder(len + 10).append(QUOTE); - int fromIndex = 0; - - while (index != -1) { - builder.append(identifier, fromIndex, index) - .append(QUOTE) - .append(QUOTE); - fromIndex = index + 1; - index = identifier.indexOf(QUOTE, fromIndex); - } - - if (fromIndex < len) { - builder.append(identifier, fromIndex, len); - } - - return builder.append(QUOTE).toString(); - } - - /** - * Extends a SQL statement with {@code RETURNING} clause. - * - * @param sql the original SQL statement. - * @param returning quoted column identifiers. - * @return the SQL statement with {@code RETURNING} clause. - */ - public static String extendReturning(String sql, String returning) { - return returning.isEmpty() ? sql : sql + " RETURNING " + returning; - } - - private StringUtils() { - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java deleted file mode 100644 index eb926fe41..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.asyncer.r2dbc.mysql.message.client; - -import io.asyncer.r2dbc.mysql.ConnectionContext; -import io.netty.buffer.ByteBuf; - -public final class InitDbMessage extends ScalarClientMessage { - - private static final byte FLAG = 0x02; - - private final String database; - - public InitDbMessage(String database) { this.database = database; } - - @Override - protected void writeTo(ByteBuf buf, ConnectionContext context) { - // RestOfPacketString, no need terminal or length - buf.writeByte(FLAG).writeCharSequence(database, context.getClientCollation().getCharset()); - } -} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java b/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java deleted file mode 100644 index 4b84d09ba..000000000 --- a/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.collation.CharCollation; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ColumnDefinition}. - */ -class ColumnDefinitionTest { - - @Test - void allSet() { - ColumnDefinition definition = ColumnDefinition.of(-1); - - assertThat(definition.isBinary()).isTrue(); - assertThat(definition.isSet()).isTrue(); - assertThat(definition.isUnsigned()).isTrue(); - assertThat(definition.isEnum()).isTrue(); - assertThat(definition.isNotNull()).isTrue(); - } - - @Test - void noSet() { - ColumnDefinition definition = ColumnDefinition.of(0); - - assertThat(definition.isBinary()).isFalse(); - assertThat(definition.isSet()).isFalse(); - assertThat(definition.isUnsigned()).isFalse(); - assertThat(definition.isEnum()).isFalse(); - assertThat(definition.isNotNull()).isFalse(); - - } - - @Test - void isBinaryUsesCollationId() { - ColumnDefinition definition = ColumnDefinition.of(-1, CharCollation.BINARY_ID); - - assertThat(definition.isBinary()).isTrue(); - - definition = ColumnDefinition.of(-1, ~CharCollation.BINARY_ID); - assertThat(definition.isBinary()).isFalse(); - } -} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java deleted file mode 100644 index 2cec69c8f..000000000 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.cache.Caches; -import io.asyncer.r2dbc.mysql.client.Client; -import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.asyncer.r2dbc.mysql.message.client.ClientMessage; -import io.asyncer.r2dbc.mysql.message.client.TextQueryMessage; -import io.r2dbc.spi.IsolationLevel; -import org.assertj.core.api.ThrowableTypeAssert; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Unit tests for {@link MySqlConnection}. - */ -class MySqlConnectionTest { - - private final Client client = mock(Client.class); - - private final Codecs codecs = mock(Codecs.class); - - private final IsolationLevel level = IsolationLevel.REPEATABLE_READ; - - private final String product = "MockConnection"; - - private final MySqlConnection noPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, null); - - @Test - void createStatement() { - String condition = "SELECT * FROM test"; - MySqlConnection allPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, sql -> true); - MySqlConnection halfPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, sql -> false); - MySqlConnection conditionPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, sql -> sql.equals(condition)); - - assertThat(noPrepare.createStatement("SELECT * FROM test WHERE id=1")) - .isExactlyInstanceOf(TextSimpleStatement.class); - assertThat(noPrepare.createStatement(condition)) - .isExactlyInstanceOf(TextSimpleStatement.class); - assertThat(noPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(TextParametrizedStatement.class); - - assertThat(allPrepare.createStatement("SELECT * FROM test WHERE id=1")) - .isExactlyInstanceOf(PrepareSimpleStatement.class); - assertThat(allPrepare.createStatement(condition)) - .isExactlyInstanceOf(PrepareSimpleStatement.class); - assertThat(allPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(PrepareParametrizedStatement.class); - - assertThat(halfPrepare.createStatement("SELECT * FROM test WHERE id=1")) - .isExactlyInstanceOf(TextSimpleStatement.class); - assertThat(halfPrepare.createStatement(condition)) - .isExactlyInstanceOf(TextSimpleStatement.class); - assertThat(halfPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(PrepareParametrizedStatement.class); - - assertThat(conditionPrepare.createStatement("SELECT * FROM test WHERE id=1")) - .isExactlyInstanceOf(TextSimpleStatement.class); - assertThat(conditionPrepare.createStatement(condition)) - .isExactlyInstanceOf(PrepareSimpleStatement.class); - assertThat(conditionPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(PrepareParametrizedStatement.class); - } - - @SuppressWarnings("ConstantConditions") - @Test - void badCreateStatement() { - assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.createStatement(null)); - } - - @SuppressWarnings("ConstantConditions") - @Test - void badCreateSavepoint() { - ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); - - asserted.isThrownBy(() -> noPrepare.createSavepoint("")); - asserted.isThrownBy(() -> noPrepare.createSavepoint(null)); - } - - @SuppressWarnings("ConstantConditions") - @Test - void badReleaseSavepoint() { - ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); - - asserted.isThrownBy(() -> noPrepare.releaseSavepoint("")); - asserted.isThrownBy(() -> noPrepare.releaseSavepoint(null)); - } - - @SuppressWarnings("ConstantConditions") - @Test - void badRollbackTransactionToSavepoint() { - ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); - - asserted.isThrownBy(() -> noPrepare.rollbackTransactionToSavepoint("")); - asserted.isThrownBy(() -> noPrepare.rollbackTransactionToSavepoint(null)); - } - - @SuppressWarnings("ConstantConditions") - @Test - void badSetTransactionIsolationLevel() { - assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.setTransactionIsolationLevel(null)); - } - - @Test - void shouldSetTransactionIsolationLevelSuccessfully() { - ClientMessage message = new TextQueryMessage("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE"); - when(client.exchange(eq(message), any())).thenReturn(Flux.empty()); - - noPrepare.setTransactionIsolationLevel(IsolationLevel.SERIALIZABLE) - .as(StepVerifier::create) - .verifyComplete(); - - assertThat(noPrepare.getSessionTransactionIsolationLevel()).isEqualTo(IsolationLevel.SERIALIZABLE); - } - - @SuppressWarnings("ConstantConditions") - @Test - void badValidate() { - assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.validate(null)); - } -} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java deleted file mode 100644 index ce4f27387..000000000 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.TreeSet; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Unit tests for {@link MySqlNames}. - */ -class MySqlNamesTest { - - private static final String[] NAMES = { "c", "dD", "cBc", "Dca", "ADC", "DcA", "abc", "b", "B", "dA", - "AB", "a", "Abc", "ABC", "A", "ab", "cc", "Da", "CbC" }; - - private static final String[] SINGLETON = { "name" }; - - private static final Set CS_NAME_SET = new HashSet<>(); - - private static final Set CI_NAME_SET = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - - static { - CS_NAME_SET.addAll(Arrays.asList(NAMES)); - CI_NAME_SET.addAll(Arrays.asList(NAMES)); - Arrays.sort(NAMES, MySqlNames::compare); - } - - @Test - void nameSearch() { - Consumer judge = name -> { - if (CS_NAME_SET.contains(name)) { - assertEquals(NAMES[MySqlNames.nameSearch(NAMES, name)], name); - assertEquals(NAMES[MySqlNames.nameSearch(NAMES, String.format("`%s`", name))], name); - } else if (CI_NAME_SET.contains(name)) { - assertTrue(NAMES[MySqlNames.nameSearch(NAMES, name)].equalsIgnoreCase(name)); - assertEquals(MySqlNames.nameSearch(NAMES, String.format("`%s`", name)), -1); - } else { - assertEquals(MySqlNames.nameSearch(NAMES, name), -1); - assertEquals(MySqlNames.nameSearch(NAMES, String.format("`%s`", name)), -1); - } - }; - nameGenerate(1, judge); - nameGenerate(2, judge); - nameGenerate(3, judge); - - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "name")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "Name")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "nAMe")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "`name`")], "name"); - assertEquals(MySqlNames.nameSearch(SINGLETON, "`Name`"), -1); - assertEquals(MySqlNames.nameSearch(SINGLETON, "`nAMe`"), -1); - } - - /** - * A full-arrangement of repeatable selections is generated in 'a' - 'd' and 'A' - 'D' of fixed length - * String. - *

- * e.g. input: 2, publish: aa ab ac ad aA aB aC ... DB DC DD - */ - private static void nameGenerate(int length, Consumer nameConsumer) { - nameGen0(length, null, nameConsumer); - } - - private static void nameGen0(int length, @Nullable String prefix, Consumer nameConsumer) { - if (length <= 1) { - for (char c = 'a'; c < 'e'; ++c) { - if (prefix == null) { - nameConsumer.accept(Character.toString(c)); - } else { - nameConsumer.accept(prefix + c); - } - } - for (char c = 'A'; c < 'E'; ++c) { - if (prefix == null) { - nameConsumer.accept(Character.toString(c)); - } else { - nameConsumer.accept(prefix + c); - } - } - } else { - for (char c = 'a'; c < 'e'; ++c) { - if (prefix == null) { - nameGen0(length - 1, Character.toString(c), nameConsumer); - } else { - nameGen0(length - 1, prefix + c, nameConsumer); - } - } - for (char c = 'A'; c < 'E'; ++c) { - if (prefix == null) { - nameGen0(length - 1, Character.toString(c), nameConsumer); - } else { - nameGen0(length - 1, prefix + c, nameConsumer); - } - } - } - } -} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java deleted file mode 100644 index 7ff20cb9d..000000000 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.r2dbc.spi.IsolationLevel; -import io.r2dbc.spi.TransactionDefinition; -import org.junit.jupiter.api.Test; - -import java.time.Duration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link MySqlTransactionDefinition}. - */ -class MySqlTransactionDefinitionTest { - - @Test - void builder() { - Duration lockWaitTimeout = Duration.ofSeconds(118); - Long sessionId = 123456789L; - MySqlTransactionDefinition definition = MySqlTransactionDefinition.builder() - .isolationLevel(IsolationLevel.READ_COMMITTED) - .lockWaitTimeout(lockWaitTimeout) - .withConsistentSnapshot(true) - .consistentSnapshotEngine(ConsistentSnapshotEngine.ROCKSDB) - .consistentSnapshotFromSession(sessionId) - .build(); - - assertThat(definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) - .isSameAs(IsolationLevel.READ_COMMITTED); - assertThat(definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) - .isSameAs(lockWaitTimeout); - assertThat(definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT)) - .isTrue(); - assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE)) - .isSameAs(ConsistentSnapshotEngine.ROCKSDB); - assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_FROM_SESSION)) - .isSameAs(sessionId); - } - - @Test - void mutate() { - Duration lockWaitTimeout = Duration.ofSeconds(118); - MySqlTransactionDefinition def1 = MySqlTransactionDefinition.builder() - .isolationLevel(IsolationLevel.SERIALIZABLE) - .lockWaitTimeout(lockWaitTimeout) - .readOnly(true) - .build(); - MySqlTransactionDefinition def2 = def1.mutate() - .isolationLevel(IsolationLevel.READ_COMMITTED) - .build(); - - assertThat(def1).isNotEqualTo(def2); - assertThat(def1.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) - .isSameAs(def2.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) - .isSameAs(lockWaitTimeout); - assertThat(def1.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) - .isSameAs(IsolationLevel.SERIALIZABLE); - assertThat(def2.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) - .isSameAs(IsolationLevel.READ_COMMITTED); - assertThat(def1.getAttribute(TransactionDefinition.READ_ONLY)) - .isSameAs(def2.getAttribute(TransactionDefinition.READ_ONLY)) - .isEqualTo(true); - } -} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java b/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java deleted file mode 100644 index 999111de5..000000000 --- a/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.codec; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; - -import java.nio.charset.Charset; -import java.util.Arrays; - -/** - * Unit tests for {@link BooleanCodec}. - */ -class BooleanCodecTest implements CodecTestSupport { - - private final Boolean[] booleans = { Boolean.TRUE, Boolean.FALSE }; - - @Override - public BooleanCodec getCodec() { - return BooleanCodec.INSTANCE; - } - - @Override - public Boolean[] originParameters() { - return booleans; - } - - @Override - public Object[] stringifyParameters() { - return Arrays.stream(booleans).map(it -> it ? "1" : "0").toArray(); - } - - @Override - public ByteBuf[] binaryParameters(Charset charset) { - return Arrays.stream(booleans) - .map(it -> Unpooled.wrappedBuffer(it ? new byte[] { 1 } : new byte[] { 0 })) - .toArray(ByteBuf[]::new); - } - - @Override - public ByteBuf sized(ByteBuf value) { - return value; - } -} diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml new file mode 100644 index 000000000..36b13f6b0 --- /dev/null +++ b/test-native-image/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + io.asyncer + test-native-image + 1.4.2-SNAPSHOT + + + UTF-8 + UTF-8 + 1.8 + 8 + 8 + true + + 2024.0.3 + 1.0.0.RELEASE + 20.3.17 + + + + + ${project.groupId} + r2dbc-mysql + ${project.version} + + + io.r2dbc + r2dbc-spi + ${r2dbc-spi.version} + + + io.projectreactor + reactor-core + + + + + + + io.projectreactor + reactor-bom + ${reactor.version} + pom + import + + + + + + + + org.graalvm.nativeimage + native-image-maven-plugin + ${graalvm.version} + + + + native-image + + package + + + + ${skipNativeImage} + ${project.artifactId} + io.asyncer.Main + + --report-unsupported-elements-at-runtime + --allow-incomplete-classpath + --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils + --initialize-at-run-time=io.netty.handler.ssl.JdkSslServerContext + + + + + + + + + graalvm + + + ${java.home}/bin/gu + + + + false + + + + diff --git a/test-native-image/src/main/java/io/asyncer/Main.java b/test-native-image/src/main/java/io/asyncer/Main.java new file mode 100644 index 000000000..1a85dccb2 --- /dev/null +++ b/test-native-image/src/main/java/io/asyncer/Main.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer; + +import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import reactor.core.publisher.Mono; + +public final class Main { + + private Main() { + // Native Image Entry Point + } + + public static void main(String[] args) { + ConnectionFactory connectionFactory = ConnectionFactories.get(ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, "mysql") + .option(ConnectionFactoryOptions.HOST, "127.0.0.1") + .option(ConnectionFactoryOptions.PORT, 3306) + .option(ConnectionFactoryOptions.USER, "root") + .option(ConnectionFactoryOptions.PASSWORD, "root") + .option(ConnectionFactoryOptions.DATABASE, "test") + .option(MySqlConnectionFactoryProvider.CREATE_DATABASE_IF_NOT_EXIST, true) + .build()); + + Mono.from(connectionFactory.create()) + .flatMapMany(connection -> connection.createStatement("SELECT 1").execute()) + .flatMap((result) -> result.map((row, rowMetadata) -> row.get(0, Integer.class))) + .doOnNext(System.out::println) + .blockLast(); + } +} diff --git a/test-native-image/src/main/java/io/asyncer/package-info.java b/test-native-image/src/main/java/io/asyncer/package-info.java new file mode 100644 index 000000000..fb9d7aed7 --- /dev/null +++ b/test-native-image/src/main/java/io/asyncer/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Native image build test classes. + */ +@NotNullByDefault +package io.asyncer; + +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault;