diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index a1fb8bde9..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: | @@ -63,10 +63,12 @@ jobs: 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() }} @@ -77,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/ @@ -88,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: | @@ -116,14 +118,14 @@ 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 @@ -131,16 +133,14 @@ jobs: - name: Import GPG & Deploy Local Staging working-directory: ./prepare-workspace/ run: | - 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 }}" + 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 -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 trunk - diff --git a/.github/workflows/cd-snapshot.yml b/.github/workflows/cd-snapshot.yml index a04976b28..8cd96d40c 100644 --- a/.github/workflows/cd-snapshot.yml +++ b/.github/workflows/cd-snapshot.yml @@ -46,9 +46,9 @@ 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 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 index ee5f0d03d..54db266ff 100644 --- a/.github/workflows/ci-graalvm-tests.yml +++ b/.github/workflows/ci-graalvm-tests.yml @@ -16,18 +16,18 @@ name: Native Image Build Test on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: graalvm-build-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1 with: java-version: 21 - distribution: 'graalvm' + distribution: "graalvm" native-image-job-reports: true github-token: ${{ secrets.GITHUB_TOKEN }} @@ -43,7 +43,7 @@ jobs: - 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 + 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 6f0e151ff..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.45, 5.6, 5.7.28, 5.7, 8.0, 8.1, 8.2, 8.3] + 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 diff --git a/.github/workflows/ci-mariadb-intergration-tests.yml b/.github/workflows/ci-mariadb-intergration-tests.yml index d20bba01a..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.0, 10.1, 10.2.15, 10.2, 10.3.7, 10.3, 10.5.1, 10.5, 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 diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml index fdf48ce0d..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,7 +20,7 @@ jobs: java-version: ${{ matrix.java-version }} cache: maven - name: Unit test with Maven - run: | + run: | set -o pipefail ./mvnw -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ -Dio.netty.leakDetectionLevel=paranoid \ 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 4ddab2f62..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) @@ -10,15 +11,17 @@ 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.1.0 | +| 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. @@ -37,16 +40,19 @@ This driver provides the following features: - [x] MariaDB `RETURNING` clause. ## 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. @@ -58,7 +64,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so io.asyncer r2dbc-mysql - 1.1.0 + 1.4.0 ``` @@ -68,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.1.0' + implementation 'io.asyncer:r2dbc-mysql:1.4.0' } ``` @@ -77,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.1.0") + implementation("io.asyncer:r2dbc-mysql:1.4.0") } ``` @@ -110,7 +116,7 @@ See [Usage](https://github.com/asyncer-io/r2dbc-mysql/wiki/usage) wiki for more ## 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. @@ -149,7 +155,7 @@ 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 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/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 7c23bd139..a903116a8 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.1.3 + 1.4.2-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ 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.3 + HEAD @@ -73,8 +73,8 @@ false 1.0.0.RELEASE - 2022.0.16 - 4.1.106.Final + 2024.0.3 + 4.1.118.Final 3.25.3 1.37 5.10.2 @@ -82,7 +82,7 @@ 4.11.0 8.3.0 3.3.3 - 1.19.7 + 1.21.0 4.0.3 5.3.32 2.16.1 @@ -448,6 +448,14 @@ + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + + central + + @@ -558,16 +566,9 @@ - - - false - - - true - - ossrh-snapshots - Sonatype Nexus Snapshots - https://s01.oss.sonatype.org/content/repositories/snapshots/ - + + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + 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 index cc5aeb2a9..26ec660c4 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -51,6 +51,8 @@ public final class ConnectionContext implements CodecContext { private final int localInfileBufferSize; + private final boolean tinyInt1isBit; + private final boolean preserveInstants; private int connectionId = -1; @@ -107,12 +109,14 @@ public final class ConnectionContext implements CodecContext { 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; } @@ -216,6 +220,11 @@ public boolean isMariaDb() { return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb(); } + @Override + public boolean isTinyInt1isBit() { + return tinyInt1isBit; + } + public boolean isNoBackslashEscapes() { return (serverStatuses & ServerStatuses.NO_BACKSLASH_ESCAPES) != 0; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 3856b58bd..39fb91eb6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -22,8 +22,10 @@ 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; @@ -127,22 +129,31 @@ public final class MySqlConnectionConfiguration { @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, - 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 - ) { + 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; @@ -171,6 +182,9 @@ private MySqlConnectionConfiguration( this.loopResources = loopResources == null ? TcpResources.get() : loopResources; this.extensions = extensions; this.passwordPublisher = passwordPublisher; + this.resolver = resolver; + this.metrics = metrics; + this.tinyInt1isBit = tinyInt1isBit; } /** @@ -301,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) { @@ -337,7 +364,10 @@ public boolean equals(Object o) { 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 @@ -352,19 +382,26 @@ public int hashCode() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, - loopResources, extensions, passwordPublisher); + 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 + + 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 + + ", zeroDateOption=" + zeroDateOption + + ", user='" + user + "', password=" + password + ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + ", sessionVariables=" + sessionVariables + @@ -372,32 +409,16 @@ public String toString() { ", statementTimeout=" + statementTimeout + ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + - ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize + + ", queryCacheSize=" + queryCacheSize + + ", prepareCacheSize=" + prepareCacheSize + ", compressionAlgorithms=" + compressionAlgorithms + ", zstdCompressionLevel=" + zstdCompressionLevel + ", loopResources=" + loopResources + - ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; - } - - return "MySqlConnectionConfiguration{unixSocket='" + domain + - "', 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 + - ", compressionAlgorithms=" + compressionAlgorithms + - ", zstdCompressionLevel=" + zstdCompressionLevel + - ", loopResources=" + loopResources + - ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; + ", extensions=" + extensions + + ", passwordPublisher=" + passwordPublisher + + ", resolver=" + resolver + + ", metrics=" + metrics + + ", tinyInt1isBit=" + tinyInt1isBit; } /** @@ -494,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. * @@ -528,11 +556,11 @@ public MySqlConnectionConfiguration build() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, - Extensions.from(extensions, autodetectExtensions), passwordPublisher); + Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit); } /** - * Configures 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}. @@ -1156,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 index d003db2b0..094674f2a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -42,68 +42,65 @@ */ public final class MySqlConnectionFactory implements ConnectionFactory { - private final Mono client; + private final MySqlConnectionConfiguration configuration; + private final LazyQueryCache queryCache; - private MySqlConnectionFactory(Mono client) { - this.client = client; + private MySqlConnectionFactory(MySqlConnectionConfiguration configuration) { + this.configuration = configuration; + this.queryCache = new LazyQueryCache(configuration.getQueryCacheSize()); } @Override public Mono create() { - return client; - } - - @Override - public ConnectionFactoryMetadata getMetadata() { - return MySqlConnectionFactoryMetadata.INSTANCE; - } + MySqlSslConfiguration ssl; + SocketAddress address; - /** - * 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(), + if (configuration.isHost()) { + ssl = configuration.getSsl(); + address = InetSocketAddress.createUnresolved(configuration.getDomain(), configuration.getPort()); - } else { - ssl = MySqlSslConfiguration.disabled(); - address = new DomainSocketAddress(configuration.getDomain()); - } + } else { + ssl = MySqlSslConfiguration.disabled(); + address = new DomainSocketAddress(configuration.getDomain()); + } - String user = configuration.getUser(); - CharSequence password = configuration.getPassword(); - Publisher passwordPublisher = configuration.getPasswordPublisher(); + String user = configuration.getUser(); + CharSequence password = configuration.getPassword(); + Publisher passwordPublisher = configuration.getPasswordPublisher(); - if (Objects.nonNull(passwordPublisher)) { - return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( + if (Objects.nonNull(passwordPublisher)) { + return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( configuration, ssl, queryCache, address, user, token - )); - } + )); + } - return getMySqlConnection( + 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); } /** @@ -137,6 +134,7 @@ private static Mono getMySqlConnection( configuration.getZeroDateOption(), configuration.getLoadLocalInfilePath(), configuration.getLocalInfileBufferSize(), + configuration.isTinyInt1isBit(), configuration.isPreserveInstants(), connectionTimeZone ); @@ -147,7 +145,9 @@ private static Mono getMySqlConnection( configuration.isTcpNoDelay(), context, configuration.getConnectTimeout(), - configuration.getLoopResources() + configuration.getLoopResources(), + configuration.getResolver(), + configuration.isMetrics() )).flatMap(client -> { // Lazy init database after handshake/login boolean deferDatabase = configuration.isCreateDatabaseIfNotExist(); 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 index f6dc1a57a..5905c56ca 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -20,6 +20,7 @@ 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; @@ -308,6 +309,38 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr */ 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"); @@ -389,6 +422,8 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { .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(), @@ -399,6 +434,10 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { .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(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java index d7c3ac28a..316d90999 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java @@ -22,6 +22,7 @@ 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; @@ -126,19 +127,21 @@ public interface Client { * @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, - LoopResources loopResources) { + 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() - .runOn(loopResources); + .runOn(loopResources) + .metrics(metrics); if (connectTimeout != null) { tcpClient = tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, @@ -150,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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index 81cb5f21e..5054f3631 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -118,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()); } }) @@ -131,6 +131,14 @@ 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); } @@ -213,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 -> { @@ -378,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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java index ce3361fd4..22038bb43 100644 --- a/r2dbc-mysql/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; @@ -151,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(); @@ -195,7 +201,7 @@ 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; @@ -203,16 +209,6 @@ 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; - } - - @Override public SslContext sslContext() throws SSLException { return builder.build(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index f546ba751..8fb98c273 100644 --- a/r2dbc-mysql/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 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() { @@ -38,7 +43,35 @@ private BooleanCodec() { @Override 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 @@ -54,8 +87,20 @@ public MySqlParameter encode(Object value, CodecContext context) { @Override public boolean doCanDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); - return (type == MySqlType.BIT || type == MySqlType.TINYINT) && - Integer.valueOf(1).equals(metadata.getPrecision()); + 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/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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java index 8eda9c985..5b58fa5f6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java @@ -69,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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index d76b398e2..01e47348c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -18,6 +18,7 @@ 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; @@ -80,7 +81,8 @@ final class DefaultCodecs implements Codecs { BlobCodec.INSTANCE, ByteBufferCodec.INSTANCE, - ByteArrayCodec.INSTANCE + ByteArrayCodec.INSTANCE, + ByteArrayInputStreamCodec.INSTANCE ); private final List> codecs; @@ -136,6 +138,7 @@ private DefaultCodecs(List> codecs) { * 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, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { @@ -150,7 +153,7 @@ public T decode(FieldValue value, MySqlReadableMetadata metadata, Class t return null; } - Class target = chooseClass(metadata, type); + Class target = chooseClass(metadata, type, context); if (value instanceof NormalFieldValue) { return decodeNormal((NormalFieldValue) value, metadata, target, binary, context); @@ -161,6 +164,7 @@ public T decode(FieldValue value, MySqlReadableMetadata metadata, Class t throw new IllegalArgumentException("Unknown value " + value.getClass().getSimpleName()); } + @Nullable @Override public T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { @@ -358,11 +362,34 @@ private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat * @param type the {@link Class} specified by the user. * @return the {@link Class} to use for decoding. */ - private static Class chooseClass(MySqlReadableMetadata metadata, Class type) { - Class javaType = metadata.getType().getJavaType(); + 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 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(); + } + + private static Class getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) { + final MySqlType type = metadata.getType(); + final Integer precision = metadata.getPrecision(); + + if (shouldBeTreatedAsBoolean(precision, type, codecContext)) { + return Boolean.class; + } + + return type.getJavaType(); + } + static final class Builder implements CodecsBuilder { @GuardedBy("lock") diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java index 5d0635412..eb4a3ea3c 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java @@ -39,7 +39,7 @@ void getTimeZone() { String id = i < 0 ? "UTC" + i : "UTC+" + i; ConnectionContext context = new ConnectionContext( ZeroDateOption.USE_NULL, null, - 8192, true, ZoneId.of(id)); + 8192, true, true, ZoneId.of(id)); assertThat(context.getTimeZone()).isEqualTo(ZoneId.of(id)); } @@ -48,7 +48,7 @@ void getTimeZone() { @Test void setTwiceTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, true, null); + 8192, true, true, null); context.initSession( Caches.createPrepareCache(0), @@ -70,7 +70,7 @@ void setTwiceTimeZone() { @Test void badSetTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, true, ZoneId.systemDefault()); + 8192, true, true, ZoneId.systemDefault()); assertThatIllegalStateException().isThrownBy(() -> context.initSession( Caches.createPrepareCache(0), IsolationLevel.REPEATABLE_READ, @@ -91,7 +91,7 @@ public static ConnectionContext mock(boolean isMariaDB) { public static ConnectionContext mock(boolean isMariaDB, ZoneId zoneId) { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, true, zoneId); + 8192, true, true, zoneId); context.initHandshake(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"), Capability.of(~(isMariaDB ? 1 : 0))); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index b65b3b447..4e4fa34ae 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -579,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 diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java index f050f4e4a..e62fea190 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java @@ -22,6 +22,8 @@ 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; @@ -207,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) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index ab75161c1..be48a2255 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -20,6 +20,8 @@ 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; @@ -49,7 +51,9 @@ 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; @@ -453,6 +457,31 @@ 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( 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/codec/BooleanCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java index 999111de5..dbfd5c104 100644 --- 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 @@ -16,12 +16,22 @@ 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}. */ @@ -55,4 +65,109 @@ public ByteBuf[] binaryParameters(Charset charset) { 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/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/test-native-image/pom.xml b/test-native-image/pom.xml index e18ad93bb..36b13f6b0 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.1.4-SNAPSHOT + 1.4.2-SNAPSHOT UTF-8 @@ -15,9 +15,9 @@ 8 true - 2022.0.16 + 2024.0.3 1.0.0.RELEASE - 20.3.13 + 20.3.17 @@ -67,7 +67,12 @@ ${skipNativeImage} ${project.artifactId} io.asyncer.Main - --report-unsupported-elements-at-runtime --allow-incomplete-classpath --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils + + --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 +