+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/pom.xml b/pom.xml
index 9b034e46f..305230922 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,460 +17,14 @@
4.0.0
-
io.asyncer
- r2dbc-mysql
- 1.1.0
- jar
-
- Reactive Relational Database Connectivity - MySQL
- https://github.com/asyncer-io/r2dbc-mysql
- R2DBC MySQL Implementation
-
-
-
- The Apache License, Version 2.0
- https://www.apache.org/licenses/LICENSE-2.0.txt
-
-
-
-
- asyncer.io
- https://github.com/asyncer-io/r2dbc-mysql
-
-
-
-
- jchrys
- jchrys
- jchrys@me.com
-
- Project Lead
-
-
-
-
- 2018
-
- scm:git:git://github.com/asyncer-io/r2dbc-mysql.git
- scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git
- https://github.com/asyncer-io/r2dbc-mysql
- r2dbc-mysql-1.1.0
-
-
-
- UTF-8
- UTF-8
- 1.8
- false
-
- 1.0.0.RELEASE
- 2022.0.9
- 3.24.2
- 1.37
- 5.10.1
- 1.4.14
- 4.11.0
- 8.2.0
- 1.19.3
- 4.0.3
- 5.3.31
- 2.16.0
- 0.3.0.RELEASE
- 3.0.2
- 24.1.0
-
-
-
-
-
- io.projectreactor
- reactor-bom
- ${reactor.version}
- pom
- import
-
-
- org.junit
- junit-bom
- ${junit.version}
- pom
- import
-
-
- org.testcontainers
- testcontainers-bom
- ${testcontainers.version}
- pom
- import
-
-
- com.fasterxml.jackson
- jackson-bom
- ${jackson.version}
- pom
- import
-
-
- org.jetbrains
- annotations
- ${java-annotations.version}
- provided
-
-
-
-
-
-
- io.projectreactor
- reactor-core
-
-
- io.projectreactor.netty
- reactor-netty
-
-
- io.r2dbc
- r2dbc-spi
- ${r2dbc-spi.version}
-
-
- org.jetbrains
- annotations
-
-
-
- com.google.code.findbugs
- jsr305
- ${jsr305.version}
- provided
-
-
-
- ch.qos.logback
- logback-classic
- ${logback.version}
- test
-
-
- io.projectreactor
- reactor-test
- test
-
-
- io.r2dbc
- r2dbc-spi-test
- ${r2dbc-spi.version}
- test
-
-
- org.assertj
- assertj-core
- ${assertj.version}
- test
-
-
- org.junit.jupiter
- junit-jupiter-api
- test
-
-
- org.junit.jupiter
- junit-jupiter-engine
- test
-
-
- org.junit.jupiter
- junit-jupiter-params
- test
-
-
- org.mockito
- mockito-core
- ${mockito.version}
- test
-
-
- com.mysql
- mysql-connector-j
- ${mysql.version}
- test
-
-
- com.zaxxer
- HikariCP
- ${hikari-cp.version}
- test
-
-
- org.slf4j
- slf4j-api
-
-
-
-
- org.springframework
- spring-jdbc
- ${spring-framework.version}
- test
-
-
- org.testcontainers
- mysql
- test
-
-
- org.slf4j
- slf4j-api
-
-
-
-
- com.fasterxml.jackson.core
- jackson-core
- test
-
-
- com.fasterxml.jackson.core
- jackson-databind
- test
-
-
- com.fasterxml.jackson.core
- jackson-annotations
- test
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
- 3.11.0
-
-
- -Xlint:all
- -Xlint:-options
- -Xlint:-processing
- -Xlint:-serial
-
- true
- ${java.version}
- ${java.version}
-
-
-
- org.apache.maven.plugins
- maven-jar-plugin
- 3.3.0
-
-
-
- true
- true
-
-
-
-
-
- org.apache.maven.plugins
- maven-deploy-plugin
- 3.1.1
-
-
- org.apache.maven.plugins
- maven-javadoc-plugin
- 3.6.3
-
-
- io.asyncer.r2dbc.mysql.authentication,io.asyncer.r2dbc.mysql.client,io.asyncer.r2dbc.mysql.util,io.asyncer.r2dbc.mysql.codec.lob,io.asyncer.r2dbc.mysql.message
-
-
- https://r2dbc.io/spec/${r2dbc-spi.version}/api/
- https://projectreactor.io/docs/core/release/api/
- https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/
-
- en_US
-
-
-
- attach-javadocs
-
- jar
-
-
-
-
-
- org.apache.maven.plugins
- maven-source-plugin
- 3.3.0
-
-
- attach-javadocs
-
- jar
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
- 3.2.3
-
- random
-
- **/*Test.java
-
-
- **/*TestKit.java
- **/*IntegrationTest.java
-
- ${maven.surefire.skip}
-
-
-
- org.apache.maven.plugins
- maven-failsafe-plugin
- 3.2.3
-
-
-
- integration-test
- verify
-
-
-
-
- random
-
- **/*TestKit.java
- **/*IntegrationTest.java
-
-
-
-
-
-
- ${project.basedir}
-
- LICENSE
-
- META-INF
-
-
- ${project.basedir}/src/main/resources
-
-
-
-
-
-
- jmh
-
-
- com.github.mp911de.microbenchmark-runner
- microbenchmark-runner-junit5
- ${mbr.version}
- test
-
-
- org.openjdk.jmh
- jmh-core
- ${jmh.version}
- test
-
-
- org.openjdk.jmh
- jmh-generator-annprocess
- ${jmh.version}
- test
-
-
-
-
-
- org.codehaus.mojo
- build-helper-maven-plugin
- 3.5.0
-
-
- add-source
- generate-sources
-
- add-test-source
-
-
-
- src/jmh/java
-
-
-
-
-
-
- org.apache.maven.plugins
- maven-surefire-plugin
-
- true
-
-
-
- org.apache.maven.plugins
- maven-failsafe-plugin
-
- true
-
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 3.1.1
-
-
- run-benchmarks
- pre-integration-test
-
- exec
-
-
- test
- java
-
- -classpath
-
- org.openjdk.jmh.Main
- .*
-
-
-
-
-
-
-
-
-
-
-
-
- jitpack.io
- https://jitpack.io
-
-
-
-
-
-
- false
-
-
- true
-
- ossrh-snapshots
- Sonatype Nexus Snapshots
- https://s01.oss.sonatype.org/content/repositories/snapshots/
-
-
+ r2dbc-mysql-parent
+ INTERNAL
+ pom
+
+
+ r2dbc-mysql
+ test-native-image
+ build-tools
+
diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml
new file mode 100644
index 000000000..a903116a8
--- /dev/null
+++ b/r2dbc-mysql/pom.xml
@@ -0,0 +1,574 @@
+
+
+
+ 4.0.0
+
+ io.asyncer
+ r2dbc-mysql
+ 1.4.2-SNAPSHOT
+
+ Reactive Relational Database Connectivity - MySQL
+ https://github.com/asyncer-io/r2dbc-mysql
+ R2DBC MySQL Implementation
+
+
+
+ The Apache License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+ asyncer.io
+ https://github.com/asyncer-io/r2dbc-mysql
+
+
+
+
+ jchrys
+ jchrys
+ jchrys@me.com
+
+ Maintainer
+
+
+
+ mirromutth
+ mirromutth
+ mirromutth@gmail.com
+
+ Maintainer
+
+
+
+
+ 2018
+
+ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git
+ scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git
+ https://github.com/asyncer-io/r2dbc-mysql
+ HEAD
+
+
+
+ UTF-8
+ UTF-8
+ 1.8
+ 8
+ 8
+ false
+
+ 1.0.0.RELEASE
+ 2024.0.3
+ 4.1.118.Final
+ 3.25.3
+ 1.37
+ 5.10.2
+ 1.5.3
+ 4.11.0
+ 8.3.0
+ 3.3.3
+ 1.21.0
+ 4.0.3
+ 5.3.32
+ 2.16.1
+ 0.4.0.RELEASE
+ 3.0.2
+ 1.5.5-11
+ 24.1.0
+ 1.77
+
+
+
+
+
+ io.projectreactor
+ reactor-bom
+ ${reactor.version}
+ pom
+ import
+
+
+ io.netty
+ netty-bom
+ ${netty.version}
+ pom
+ import
+
+
+ org.junit
+ junit-bom
+ ${junit.version}
+ pom
+ import
+
+
+ org.testcontainers
+ testcontainers-bom
+ ${testcontainers.version}
+ pom
+ import
+
+
+ com.fasterxml.jackson
+ jackson-bom
+ ${jackson.version}
+ pom
+ import
+
+
+ org.jetbrains
+ annotations
+ ${java-annotations.version}
+ provided
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ ${bouncy-castle.version}
+ test
+
+
+
+
+
+
+ io.projectreactor
+ reactor-core
+
+
+ io.projectreactor.netty
+ reactor-netty-core
+
+
+ io.netty
+ netty-transport-native-epoll
+ linux-x86_64
+ true
+
+
+ io.netty
+ netty-transport-native-kqueue
+ osx-x86_64
+ true
+
+
+ io.r2dbc
+ r2dbc-spi
+ ${r2dbc-spi.version}
+
+
+ org.jetbrains
+ annotations
+
+
+
+ com.google.code.findbugs
+ jsr305
+ ${jsr305.version}
+ provided
+
+
+
+ com.github.luben
+ zstd-jni
+ ${zstd-jni.version}
+ true
+
+
+
+ ch.qos.logback
+ logback-classic
+ ${logback.version}
+ test
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+ io.r2dbc
+ r2dbc-spi-test
+ ${r2dbc-spi.version}
+ test
+
+
+ org.assertj
+ assertj-core
+ ${assertj.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
+
+ com.mysql
+ mysql-connector-j
+ ${mysql.version}
+ test
+
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ ${mariadb.version}
+ test
+
+
+ com.zaxxer
+ HikariCP
+ ${hikari-cp.version}
+ test
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.springframework
+ spring-jdbc
+ ${spring-framework.version}
+ test
+
+
+ org.testcontainers
+ mysql
+ test
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ org.testcontainers
+ mariadb
+ test
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ test
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ test
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ test
+
+
+ org.bouncycastle
+ bcpkix-jdk18on
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.3.1
+
+
+ validate
+ validate
+
+ io/asyncer/checkstyle.xml
+ io/asyncer/checkstyle-suppressions.xml
+ true
+ true
+ true
+
+
+ check
+
+
+
+
+
+ io.asyncer
+ build-tools
+ INTERNAL
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.12.1
+
+
+ -Xlint:all
+ -Xlint:-options
+ -Xlint:-processing
+ -Xlint:-serial
+
+ true
+ ${java.version}
+ ${java.version}
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ true
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-release-plugin
+ 3.0.1
+
+ r2dbc-mysql-@{project.version}
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.3
+
+
+ io.asyncer.r2dbc.mysql.authentication,io.asyncer.r2dbc.mysql.client,io.asyncer.r2dbc.mysql.util,io.asyncer.r2dbc.mysql.codec.lob,io.asyncer.r2dbc.mysql.message
+
+
+ https://r2dbc.io/spec/${r2dbc-spi.version}/api/
+ https://projectreactor.io/docs/core/release/api/
+ https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/
+
+ en_US
+
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+ random
+
+ **/*Test.java
+
+
+ **/*TestKit.java
+ **/*IntegrationTest.java
+
+ ${maven.surefire.skip}
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+ 3.2.5
+
+
+
+ integration-test
+ verify
+
+
+
+
+ random
+
+ **/*TestKit.java
+ **/*IntegrationTest.java
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.7.0
+
+ central
+
+
+
+
+
+ ${project.basedir}
+
+ LICENSE
+
+ META-INF
+
+
+ ${project.basedir}/src/main/resources
+
+
+
+
+
+
+ jmh
+
+
+ com.github.mp911de.microbenchmark-runner
+ microbenchmark-runner-junit5
+ ${mbr.version}
+ test
+
+
+ org.openjdk.jmh
+ jmh-core
+ ${jmh.version}
+ test
+
+
+ org.openjdk.jmh
+ jmh-generator-annprocess
+ ${jmh.version}
+ test
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.5.0
+
+
+ add-source
+ generate-sources
+
+ add-test-source
+
+
+
+ src/jmh/java
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-failsafe-plugin
+
+ true
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.2.0
+
+
+ run-benchmarks
+ pre-integration-test
+
+ exec
+
+
+ test
+ java
+
+ -classpath
+
+ org.openjdk.jmh.Main
+ .*
+
+
+
+
+
+
+
+
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+
+
+ central-portal-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+
+
+
diff --git a/src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java
similarity index 100%
rename from src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java
rename to r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java
diff --git a/src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java
similarity index 100%
rename from src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java
rename to r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java
diff --git a/src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java
similarity index 100%
rename from src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java
rename to r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Binding.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java
similarity index 97%
rename from src/main/java/io/asyncer/r2dbc/mysql/Binding.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java
index a98b612d0..fc0166a09 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/Binding.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java
@@ -22,9 +22,9 @@
import java.util.Arrays;
/**
- * A collection of {@link MySqlParameter} for one bind invocation of a parametrized statement.
+ * A collection of {@link MySqlParameter} for one bind invocation of a parameterized statement.
*
- * @see ParametrizedStatementSupport
+ * @see ParameterizedStatementSupport
*/
final class Binding {
@@ -40,7 +40,7 @@ final class Binding {
* Add a {@link MySqlParameter} to the binding.
*
* @param index the index of the {@link MySqlParameter}
- * @param value the {@link MySqlParameter} from {@link PrepareParametrizedStatement}
+ * @param value the {@link MySqlParameter} from {@link PrepareParameterizedStatement}
*/
void add(int index, MySqlParameter value) {
if (index < 0 || index >= this.values.length) {
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java
similarity index 86%
rename from src/main/java/io/asyncer/r2dbc/mysql/Capability.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java
index 133d52e48..26299a08b 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java
@@ -144,7 +144,11 @@ public final class Capability {
private static final long VAR_INT_SIZED_AUTH = 1L << 21;
// private static final long HANDLE_EXPIRED_PASSWORD = 1L << 22; // Client can handle expired passwords.
-// private static final long SESSION_TRACK = 1L << 23;
+
+ /**
+ * Server can send session state information in the OK packet.
+ */
+ private static final long SESSION_TRACK = 1L << 23;
/**
* The MySQL server marks the EOF message as deprecated and use OK message instead.
@@ -154,7 +158,7 @@ public final class Capability {
// Allow the server not to send column metadata in result set,
// should NEVER enable this option.
// private static final long OPTIONAL_RESULT_SET_METADATA = 1L << 25;
-// private static final long Z_STD_COMPRESSION = 1L << 26;
+ private static final long ZSTD_COMPRESS = 1L << 26;
// A reserved flag, used to extend the 32-bits capability bitmap to 64-bits.
// There is no available MySql server version/edition to support it.
@@ -171,7 +175,12 @@ public final class Capability {
private static final long ALL_SUPPORTED = CLIENT_MYSQL | FOUND_ROWS | LONG_FLAG | CONNECT_WITH_DB |
NO_SCHEMA | COMPRESS | LOCAL_FILES | IGNORE_SPACE | PROTOCOL_41 | INTERACTIVE | SSL |
TRANSACTIONS | SECURE_SALT | MULTI_STATEMENTS | MULTI_RESULTS | PS_MULTI_RESULTS |
- PLUGIN_AUTH | CONNECT_ATTRS | VAR_INT_SIZED_AUTH | DEPRECATE_EOF;
+ PLUGIN_AUTH | CONNECT_ATTRS | VAR_INT_SIZED_AUTH | SESSION_TRACK | DEPRECATE_EOF | ZSTD_COMPRESS;
+
+ /**
+ * The default capabilities for a MySQL connection. It contains all client supported capabilities.
+ */
+ public static final Capability DEFAULT = new Capability(ALL_SUPPORTED);
private final long bitmap;
@@ -212,9 +221,9 @@ public boolean isProtocol41() {
}
/**
- * Checks if can use var-integer sized bytes to encode client authentication.
+ * Checks if allow to use var-integer sized bytes to encode client authentication.
*
- * @return if can use var-integer sized authentication.
+ * @return if allow to use var-integer sized authentication.
*/
public boolean isVarIntSizedAuthAllowed() {
return (bitmap & VAR_INT_SIZED_AUTH) != 0;
@@ -232,7 +241,7 @@ public boolean isPluginAuthAllowed() {
/**
* Checks if the connection contains connection attributes.
*
- * @return if has connection attributes.
+ * @return if connection attributes exists.
*/
public boolean isConnectionAttributesAllowed() {
return (bitmap & CONNECT_ATTRS) != 0;
@@ -274,6 +283,33 @@ public boolean isTransactionAllowed() {
return (bitmap & TRANSACTIONS) != 0;
}
+ /**
+ * Checks if any compression enabled.
+ *
+ * @return if any compression enabled.
+ */
+ public boolean isCompression() {
+ return (bitmap & (COMPRESS | ZSTD_COMPRESS)) != 0;
+ }
+
+ /**
+ * Checks if zlib compression enabled.
+ *
+ * @return if zlib compression enabled.
+ */
+ public boolean isZlibCompression() {
+ return (bitmap & COMPRESS) != 0;
+ }
+
+ /**
+ * Checks if zstd compression enabled.
+ *
+ * @return if zstd compression enabled.
+ */
+ public boolean isZstdCompression() {
+ return (bitmap & ZSTD_COMPRESS) != 0;
+ }
+
/**
* Extends MariaDB capabilities.
*
@@ -342,7 +378,8 @@ private Capability(long bitmap) {
* @return the {@link Capability} without unknown flags.
*/
public static Capability of(long capabilities) {
- return new Capability(capabilities & ALL_SUPPORTED);
+ long c = capabilities & ALL_SUPPORTED;
+ return c == ALL_SUPPORTED ? DEFAULT : new Capability(c);
}
static final class Builder {
@@ -358,9 +395,17 @@ void disableDatabasePinned() {
}
void disableCompression() {
+ this.bitmap &= ~(COMPRESS | ZSTD_COMPRESS);
+ }
+
+ void disableZlibCompression() {
this.bitmap &= ~COMPRESS;
}
+ void disableZstdCompression() {
+ this.bitmap &= ~ZSTD_COMPRESS;
+ }
+
void disableLoadDataLocalInfile() {
this.bitmap &= ~LOCAL_FILES;
}
@@ -377,6 +422,10 @@ void disableSsl() {
this.bitmap &= ~SSL;
}
+ void disableSessionTrack() {
+ this.bitmap &= ~SESSION_TRACK;
+ }
+
void disableConnectAttributes() {
this.bitmap &= ~CONNECT_ATTRS;
}
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java
new file mode 100644
index 000000000..26ec660c4
--- /dev/null
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2023 asyncer.io projects
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.asyncer.r2dbc.mysql;
+
+import io.asyncer.r2dbc.mysql.cache.PrepareCache;
+import io.asyncer.r2dbc.mysql.codec.CodecContext;
+import io.asyncer.r2dbc.mysql.collation.CharCollation;
+import io.asyncer.r2dbc.mysql.constant.ServerStatuses;
+import io.asyncer.r2dbc.mysql.constant.ZeroDateOption;
+import io.r2dbc.spi.IsolationLevel;
+import org.jetbrains.annotations.Nullable;
+
+import java.nio.file.Path;
+import java.time.Duration;
+import java.time.ZoneId;
+
+import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
+
+/**
+ * The MySQL connection context considers the behavior of server or client.
+ *
+ * WARNING: Do NOT change any data outside of this project, try to configure {@code ConnectionFactoryOptions} or
+ * {@code MySqlConnectionConfiguration} to control connection context and client behavior.
+ */
+public final class ConnectionContext implements CodecContext {
+
+ private static final ServerVersion NONE_VERSION = ServerVersion.create(0, 0, 0);
+
+ private static final ServerVersion MYSQL_5_7_4 = ServerVersion.create(5, 7, 4);
+
+ private static final ServerVersion MARIA_10_1_1 = ServerVersion.create(10, 1, 1, true);
+
+ private final ZeroDateOption zeroDateOption;
+
+ @Nullable
+ private final Path localInfilePath;
+
+ private final int localInfileBufferSize;
+
+ private final boolean tinyInt1isBit;
+
+ private final boolean preserveInstants;
+
+ private int connectionId = -1;
+
+ private ServerVersion serverVersion = NONE_VERSION;
+
+ private Capability capability = Capability.DEFAULT;
+
+ private PrepareCache prepareCache;
+
+ @Nullable
+ private ZoneId timeZone;
+
+ private String product = "Unknown";
+
+ /**
+ * Current isolation level inferred by past statements.
+ *
+ * Inference rules:
+ *
In the beginning, it is also {@link #sessionIsolationLevel}.
+ * A transaction has began with a {@link IsolationLevel}, it will be changed to the value
+ * The transaction end (commit or rollback), it will recover to {@link #sessionIsolationLevel}.
+ */
+ private volatile IsolationLevel currentIsolationLevel;
+
+ /**
+ * Session isolation level.
+ *
+ * It is applied to all subsequent transactions performed within the current session.
+ * Calls {@link io.r2dbc.spi.Connection#setTransactionIsolationLevel}, it will change to the value.
+ * It can be changed within transactions, but does not affect the current ongoing transaction.
+ */
+ private volatile IsolationLevel sessionIsolationLevel;
+
+ private boolean lockWaitTimeoutSupported = false;
+
+ /**
+ * Current lock wait timeout in seconds.
+ */
+ private volatile Duration currentLockWaitTimeout;
+
+ /**
+ * Session lock wait timeout in seconds.
+ */
+ private volatile Duration sessionLockWaitTimeout;
+
+ /**
+ * Assume that the auto commit is always turned on, it will be set after handshake V10 request message, or OK
+ * message which means handshake V9 completed.
+ */
+ private volatile short serverStatuses = ServerStatuses.AUTO_COMMIT;
+
+ ConnectionContext(
+ ZeroDateOption zeroDateOption,
+ @Nullable Path localInfilePath,
+ int localInfileBufferSize,
+ boolean tinyInt1isBit,
+ boolean preserveInstants,
+ @Nullable ZoneId timeZone
+ ) {
+ this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null");
+ this.localInfilePath = localInfilePath;
+ this.localInfileBufferSize = localInfileBufferSize;
+ this.tinyInt1isBit = tinyInt1isBit;
+ this.preserveInstants = preserveInstants;
+ this.timeZone = timeZone;
+ }
+
+ /**
+ * Initializes handshake information after connection is established.
+ *
+ * @param connectionId the connection identifier that is specified by server.
+ * @param version the server version.
+ * @param capability the connection capabilities.
+ */
+ void initHandshake(int connectionId, ServerVersion version, Capability capability) {
+ this.connectionId = connectionId;
+ this.serverVersion = version;
+ this.capability = capability;
+ }
+
+ /**
+ * Initializes session information after logged-in.
+ *
+ * @param prepareCache the prepare cache.
+ * @param isolationLevel the session isolation level.
+ * @param lockWaitTimeoutSupported if the server supports lock wait timeout.
+ * @param lockWaitTimeout the lock wait timeout.
+ * @param product the server product name.
+ * @param timeZone the server timezone.
+ */
+ void initSession(
+ PrepareCache prepareCache,
+ IsolationLevel isolationLevel,
+ boolean lockWaitTimeoutSupported,
+ Duration lockWaitTimeout,
+ @Nullable String product,
+ @Nullable ZoneId timeZone
+ ) {
+ this.prepareCache = prepareCache;
+ this.currentIsolationLevel = this.sessionIsolationLevel = isolationLevel;
+ this.lockWaitTimeoutSupported = lockWaitTimeoutSupported;
+ this.currentLockWaitTimeout = this.sessionLockWaitTimeout = lockWaitTimeout;
+ this.product = product == null ? "Unknown" : product;
+
+ if (timeZone != null) {
+ if (isTimeZoneInitialized()) {
+ throw new IllegalStateException("Connection timezone have been initialized");
+ }
+ this.timeZone = timeZone;
+ }
+ }
+
+ /**
+ * Get the connection identifier that is specified by server.
+ *
+ * @return the connection identifier.
+ */
+ public int getConnectionId() {
+ return connectionId;
+ }
+
+ @Override
+ public ServerVersion getServerVersion() {
+ return serverVersion;
+ }
+
+ public Capability getCapability() {
+ return capability;
+ }
+
+ @Override
+ public CharCollation getClientCollation() {
+ return CharCollation.clientCharCollation();
+ }
+
+ @Override
+ public boolean isPreserveInstants() {
+ return preserveInstants;
+ }
+
+ @Override
+ public ZoneId getTimeZone() {
+ if (timeZone == null) {
+ throw new IllegalStateException("Server timezone have not initialization");
+ }
+ return timeZone;
+ }
+
+ String getProduct() {
+ return product;
+ }
+
+ PrepareCache getPrepareCache() {
+ return prepareCache;
+ }
+
+ boolean isTimeZoneInitialized() {
+ return timeZone != null;
+ }
+
+ @Override
+ public boolean isMariaDb() {
+ Capability capability = this.capability;
+ return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb();
+ }
+
+ @Override
+ public boolean isTinyInt1isBit() {
+ return tinyInt1isBit;
+ }
+
+ public boolean isNoBackslashEscapes() {
+ return (serverStatuses & ServerStatuses.NO_BACKSLASH_ESCAPES) != 0;
+ }
+
+ @Override
+ public ZeroDateOption getZeroDateOption() {
+ return zeroDateOption;
+ }
+
+ /**
+ * Gets the allowed local infile path.
+ *
+ * @return the path.
+ */
+ @Nullable
+ public Path getLocalInfilePath() {
+ return localInfilePath;
+ }
+
+ /**
+ * Gets the local infile buffer size.
+ *
+ * @return the buffer size.
+ */
+ public int getLocalInfileBufferSize() {
+ return localInfileBufferSize;
+ }
+
+ /**
+ * Checks if the server supports InnoDB lock wait timeout.
+ *
+ * @return if the server supports InnoDB lock wait timeout.
+ */
+ public boolean isLockWaitTimeoutSupported() {
+ return lockWaitTimeoutSupported;
+ }
+
+ /**
+ * Checks if the server supports statement timeout.
+ *
+ * @return if the server supports statement timeout.
+ */
+ public boolean isStatementTimeoutSupported() {
+ boolean isMariaDb = isMariaDb();
+ return (isMariaDb && serverVersion.isGreaterThanOrEqualTo(MARIA_10_1_1)) ||
+ (!isMariaDb && serverVersion.isGreaterThanOrEqualTo(MYSQL_5_7_4));
+ }
+
+ /**
+ * Get the bitmap of server statuses.
+ *
+ * @return the bitmap.
+ */
+ public short getServerStatuses() {
+ return serverStatuses;
+ }
+
+ /**
+ * Updates server statuses.
+ *
+ * @param serverStatuses the bitmap of server statuses.
+ */
+ public void setServerStatuses(short serverStatuses) {
+ this.serverStatuses = serverStatuses;
+ }
+
+ IsolationLevel getCurrentIsolationLevel() {
+ return currentIsolationLevel;
+ }
+
+ void setCurrentIsolationLevel(IsolationLevel isolationLevel) {
+ this.currentIsolationLevel = isolationLevel;
+ }
+
+ void resetCurrentIsolationLevel() {
+ this.currentIsolationLevel = this.sessionIsolationLevel;
+ }
+
+ IsolationLevel getSessionIsolationLevel() {
+ return sessionIsolationLevel;
+ }
+
+ void setSessionIsolationLevel(IsolationLevel isolationLevel) {
+ this.sessionIsolationLevel = isolationLevel;
+ }
+
+ void setCurrentLockWaitTimeout(Duration timeoutSeconds) {
+ this.currentLockWaitTimeout = timeoutSeconds;
+ }
+
+ void resetCurrentLockWaitTimeout() {
+ this.currentLockWaitTimeout = this.sessionLockWaitTimeout;
+ }
+
+ boolean isLockWaitTimeoutChanged() {
+ return currentLockWaitTimeout != sessionLockWaitTimeout;
+ }
+
+ Duration getSessionLockWaitTimeout() {
+ return sessionLockWaitTimeout;
+ }
+
+ void setAllLockWaitTimeout(Duration timeoutSeconds) {
+ this.currentLockWaitTimeout = this.sessionLockWaitTimeout = timeoutSeconds;
+ }
+
+ boolean isInTransaction() {
+ return (serverStatuses & ServerStatuses.IN_TRANSACTION) != 0;
+ }
+
+ boolean isAutoCommit() {
+ // Within transaction, autocommit remains disabled until end the transaction with COMMIT or ROLLBACK.
+ // The autocommit mode then reverts to its previous state.
+ short serverStatuses = this.serverStatuses;
+ return (serverStatuses & ServerStatuses.IN_TRANSACTION) == 0 &&
+ (serverStatuses & ServerStatuses.AUTO_COMMIT) != 0;
+ }
+}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java
similarity index 73%
rename from src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java
index 716b65a37..a501d1887 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 asyncer.io projects
+ * Copyright 2024 asyncer.io projects
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,12 @@
/**
* The engine of {@code START TRANSACTION WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar
* syntax.
+ *
+ * @deprecated since 1.1.3, use directly {@link String} instead, e.g. {@code "ROCKSDB"}
+ * @see io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition#consistent(String)
+ * @see io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition#consistent(String, long)
*/
+@Deprecated
public enum ConsistentSnapshotEngine {
ROCKSDB,
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Extensions.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Extensions.java
similarity index 100%
rename from src/main/java/io/asyncer/r2dbc/mysql/Extensions.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Extensions.java
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java
new file mode 100644
index 000000000..a7c13c596
--- /dev/null
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java
@@ -0,0 +1,754 @@
+/*
+ * Copyright 2024 asyncer.io projects
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.asyncer.r2dbc.mysql;
+
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
+import io.asyncer.r2dbc.mysql.authentication.MySqlAuthProvider;
+import io.asyncer.r2dbc.mysql.cache.Caches;
+import io.asyncer.r2dbc.mysql.cache.PrepareCache;
+import io.asyncer.r2dbc.mysql.client.Client;
+import io.asyncer.r2dbc.mysql.client.FluxExchangeable;
+import io.asyncer.r2dbc.mysql.codec.Codecs;
+import io.asyncer.r2dbc.mysql.codec.CodecsBuilder;
+import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm;
+import io.asyncer.r2dbc.mysql.constant.SslMode;
+import io.asyncer.r2dbc.mysql.extension.CodecRegistrar;
+import io.asyncer.r2dbc.mysql.internal.util.StringUtils;
+import io.asyncer.r2dbc.mysql.message.client.AuthResponse;
+import io.asyncer.r2dbc.mysql.message.client.ClientMessage;
+import io.asyncer.r2dbc.mysql.message.client.HandshakeResponse;
+import io.asyncer.r2dbc.mysql.message.client.InitDbMessage;
+import io.asyncer.r2dbc.mysql.message.client.SslRequest;
+import io.asyncer.r2dbc.mysql.message.client.SubsequenceClientMessage;
+import io.asyncer.r2dbc.mysql.message.server.AuthMoreDataMessage;
+import io.asyncer.r2dbc.mysql.message.server.ChangeAuthMessage;
+import io.asyncer.r2dbc.mysql.message.server.CompleteMessage;
+import io.asyncer.r2dbc.mysql.message.server.ErrorMessage;
+import io.asyncer.r2dbc.mysql.message.server.HandshakeHeader;
+import io.asyncer.r2dbc.mysql.message.server.HandshakeRequest;
+import io.asyncer.r2dbc.mysql.message.server.OkMessage;
+import io.asyncer.r2dbc.mysql.message.server.ServerMessage;
+import io.asyncer.r2dbc.mysql.message.server.SyntheticSslResponseMessage;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+import io.r2dbc.spi.IsolationLevel;
+import io.r2dbc.spi.R2dbcNonTransientResourceException;
+import io.r2dbc.spi.R2dbcPermissionDeniedException;
+import io.r2dbc.spi.Readable;
+import org.jetbrains.annotations.Nullable;
+import reactor.core.CoreSubscriber;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.Sinks;
+import reactor.core.publisher.SynchronousSink;
+import reactor.util.concurrent.Queues;
+
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.time.DateTimeException;
+import java.time.Duration;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+
+/**
+ * A message flow utility that can initializes the session of {@link Client}.
+ *
+ * It should not use server-side prepared statements, because {@link PrepareCache} will be initialized after the session
+ * is initialized.
+ */
+final class InitFlow {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(InitFlow.class);
+
+ private static final ServerVersion MARIA_11_1_1 = ServerVersion.create(11, 1, 1, true);
+
+ private static final ServerVersion MYSQL_8_0_3 = ServerVersion.create(8, 0, 3);
+
+ private static final ServerVersion MYSQL_5_7_20 = ServerVersion.create(5, 7, 20);
+
+ private static final ServerVersion MYSQL_8 = ServerVersion.create(8, 0, 0);
+
+ private static final BiConsumer> INIT_DB = (message, sink) -> {
+ if (message instanceof ErrorMessage) {
+ ErrorMessage msg = (ErrorMessage) message;
+ logger.debug("Use database failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), msg.getMessage());
+ sink.next(false);
+ sink.complete();
+ } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) {
+ sink.next(true);
+ sink.complete();
+ } else {
+ ReferenceCountUtil.safeRelease(message);
+ }
+ };
+
+ private static final BiConsumer> INIT_DB_AFTER = (message, sink) -> {
+ if (message instanceof ErrorMessage) {
+ sink.error(((ErrorMessage) message).toException());
+ } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) {
+ sink.complete();
+ } else {
+ ReferenceCountUtil.safeRelease(message);
+ }
+ };
+
+ /**
+ * Initializes handshake and login a {@link Client}.
+ *
+ * @param client the {@link Client} to exchange messages with.
+ * @param sslMode the {@link SslMode} defines SSL capability and behavior.
+ * @param database the database that will be connected.
+ * @param user the user that will be login.
+ * @param password the password of the {@code user}.
+ * @param compressionAlgorithms the list of compression algorithms.
+ * @param zstdCompressionLevel the zstd compression level.
+ * @return a {@link Mono} that indicates the initialization is done, or an error if the initialization failed.
+ */
+ static Mono initHandshake(Client client, SslMode sslMode, String database, String user,
+ @Nullable CharSequence password, Set compressionAlgorithms, int zstdCompressionLevel) {
+ return client.exchange(new HandshakeExchangeable(
+ client,
+ sslMode,
+ database,
+ user,
+ password,
+ compressionAlgorithms,
+ zstdCompressionLevel
+ )).then();
+ }
+
+ /**
+ * Initializes the session and {@link Codecs} of a {@link Client}.
+ *
+ * @param client the client
+ * @param database the database to use after session initialization
+ * @param prepareCacheSize the size of prepare cache
+ * @param sessionVariables the session variables to set
+ * @param forceTimeZone if the timezone should be set to session
+ * @param lockWaitTimeout the lock wait timeout that should be set to session
+ * @param statementTimeout the statement timeout that should be set to session
+ * @return a {@link Mono} that indicates the {@link Codecs}, or an error if the initialization failed
+ */
+ static Mono initSession(
+ Client client,
+ String database,
+ int prepareCacheSize,
+ List sessionVariables,
+ boolean forceTimeZone,
+ @Nullable Duration lockWaitTimeout,
+ @Nullable Duration statementTimeout,
+ Extensions extensions
+ ) {
+ return Mono.defer(() -> {
+ ByteBufAllocator allocator = client.getByteBufAllocator();
+ CodecsBuilder builder = Codecs.builder();
+
+ extensions.forEach(CodecRegistrar.class, registrar ->
+ registrar.register(allocator, builder));
+
+ Codecs codecs = builder.build();
+
+ List variables = mergeSessionVariables(client, sessionVariables, forceTimeZone, statementTimeout);
+
+ logger.debug("Initializing client session: {}", variables);
+
+ return QueryFlow.setSessionVariables(client, variables)
+ .then(loadSessionVariables(client, codecs))
+ .flatMap(data -> loadAndInitInnoDbEngineStatus(data, client, codecs, lockWaitTimeout))
+ .flatMap(data -> {
+ ConnectionContext context = client.getContext();
+
+ logger.debug("Initializing connection {} context: {}", context.getConnectionId(), data);
+ context.initSession(
+ Caches.createPrepareCache(prepareCacheSize),
+ data.level,
+ data.lockWaitTimeoutSupported,
+ data.lockWaitTimeout,
+ data.product,
+ data.timeZone
+ );
+
+ if (!data.lockWaitTimeoutSupported) {
+ logger.info(
+ "Lock wait timeout is not supported by server, all related operations will be ignored");
+ }
+
+ return database.isEmpty() ? Mono.just(codecs) :
+ initDatabase(client, database).then(Mono.just(codecs));
+ });
+ });
+ }
+
+ private static Mono loadAndInitInnoDbEngineStatus(
+ SessionState data,
+ Client client,
+ Codecs codecs,
+ @Nullable Duration lockWaitTimeout
+ ) {
+ return new TextSimpleStatement(
+ client,
+ codecs,
+ "SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'"
+ ).execute().flatMap(r -> r.map(readable -> {
+ String value = readable.get(1, String.class);
+
+ if (value == null || value.isEmpty()) {
+ return data;
+ } else {
+ return data.lockWaitTimeout(Duration.ofSeconds(Long.parseLong(value)));
+ }
+ })).single(data).flatMap(d -> {
+ if (lockWaitTimeout != null) {
+ // Do not use context.isLockWaitTimeoutSupported() here, because its session variable is not set
+ if (d.lockWaitTimeoutSupported) {
+ return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(lockWaitTimeout))
+ .then(Mono.fromSupplier(() -> d.lockWaitTimeout(lockWaitTimeout)));
+ }
+
+ logger.warn("Lock wait timeout is not supported by server, ignore initial setting");
+ return Mono.just(d);
+ }
+ return Mono.just(d);
+ });
+ }
+
+ private static Mono loadSessionVariables(Client client, Codecs codecs) {
+ ConnectionContext context = client.getContext();
+ StringBuilder query = new StringBuilder(128)
+ .append("SELECT ")
+ .append(transactionIsolationColumn(context))
+ .append(",@@version_comment AS v");
+
+ Function> handler;
+
+ if (context.isTimeZoneInitialized()) {
+ handler = r -> convertSessionData(r, false);
+ } else {
+ query.append(",@@system_time_zone AS s,@@time_zone AS t");
+ handler = r -> convertSessionData(r, true);
+ }
+
+ return new TextSimpleStatement(client, codecs, query.toString())
+ .execute()
+ .flatMap(handler)
+ .last();
+ }
+
+ private static Mono initDatabase(Client client, String database) {
+ return client.exchange(new InitDbMessage(database), INIT_DB)
+ .last()
+ .flatMap(success -> {
+ if (success) {
+ return Mono.empty();
+ }
+
+ String sql = "CREATE DATABASE IF NOT EXISTS " + StringUtils.quoteIdentifier(database);
+
+ return QueryFlow.executeVoid(client, sql)
+ .then(client.exchange(new InitDbMessage(database), INIT_DB_AFTER).then());
+ });
+ }
+
+ private static List mergeSessionVariables(
+ Client client,
+ List sessionVariables,
+ boolean forceTimeZone,
+ @Nullable Duration statementTimeout
+ ) {
+ ConnectionContext context = client.getContext();
+
+ if ((!forceTimeZone || !context.isTimeZoneInitialized()) && statementTimeout == null) {
+ return sessionVariables;
+ }
+
+ List variables = new ArrayList<>(sessionVariables.size() + 2);
+
+ variables.addAll(sessionVariables);
+
+ if (forceTimeZone && context.isTimeZoneInitialized()) {
+ variables.add(timeZoneVariable(context.getTimeZone()));
+ }
+
+ if (statementTimeout != null) {
+ if (context.isStatementTimeoutSupported()) {
+ variables.add(StringUtils.statementTimeoutVariable(statementTimeout, context.isMariaDb()));
+ } else {
+ logger.warn("Statement timeout is not supported in {}, ignore initial setting",
+ context.getServerVersion());
+ }
+ }
+
+ return variables;
+ }
+
+ private static String timeZoneVariable(ZoneId timeZone) {
+ String offerStr = timeZone instanceof ZoneOffset && "Z".equalsIgnoreCase(timeZone.getId()) ?
+ "+00:00" : timeZone.getId();
+
+ return "time_zone='" + offerStr + "'";
+ }
+
+ private static Flux convertSessionData(MySqlResult r, boolean timeZone) {
+ return r.map(readable -> {
+ IsolationLevel level = convertIsolationLevel(readable.get(0, String.class));
+ String product = readable.get(1, String.class);
+
+ return new SessionState(level, product, timeZone ? readZoneId(readable) : null);
+ });
+ }
+
+ /**
+ * Resolves the column of session isolation level, the {@literal @@tx_isolation} has been marked as deprecated.
+ *
+ * If server is MariaDB, {@literal @@transaction_isolation} is used starting from {@literal 11.1.1}.
+ *
+ * If the server is MySQL, use {@literal @@transaction_isolation} starting from {@literal 8.0.3}, or between
+ * {@literal 5.7.20} and {@literal 8.0.0} (exclusive).
+ */
+ private static String transactionIsolationColumn(ConnectionContext context) {
+ ServerVersion version = context.getServerVersion();
+
+ if (context.isMariaDb()) {
+ return version.isGreaterThanOrEqualTo(MARIA_11_1_1) ? "@@transaction_isolation AS i" :
+ "@@tx_isolation AS i";
+ }
+
+ return version.isGreaterThanOrEqualTo(MYSQL_8_0_3) ||
+ (version.isGreaterThanOrEqualTo(MYSQL_5_7_20) && version.isLessThan(MYSQL_8)) ?
+ "@@transaction_isolation AS i" : "@@tx_isolation AS i";
+ }
+
+ private static ZoneId readZoneId(Readable readable) {
+ String systemTimeZone = readable.get(2, String.class);
+ String timeZone = readable.get(3, String.class);
+
+ if (timeZone == null || timeZone.isEmpty() || "SYSTEM".equalsIgnoreCase(timeZone)) {
+ if (systemTimeZone == null || systemTimeZone.isEmpty()) {
+ logger.warn("MySQL does not return any timezone, trying to use system default timezone");
+ return ZoneId.systemDefault().normalized();
+ } else {
+ return convertZoneId(systemTimeZone);
+ }
+ } else {
+ return convertZoneId(timeZone);
+ }
+ }
+
+ private static ZoneId convertZoneId(String id) {
+ try {
+ return StringUtils.parseZoneId(id);
+ } catch (DateTimeException e) {
+ logger.warn("The server timezone is unknown <{}>, trying to use system default timezone", id, e);
+
+ return ZoneId.systemDefault().normalized();
+ }
+ }
+
+ private static IsolationLevel convertIsolationLevel(@Nullable String name) {
+ if (name == null) {
+ logger.warn("Isolation level is null in current session, fallback to repeatable read");
+
+ return IsolationLevel.REPEATABLE_READ;
+ }
+
+ switch (name) {
+ case "READ-UNCOMMITTED":
+ return IsolationLevel.READ_UNCOMMITTED;
+ case "READ-COMMITTED":
+ return IsolationLevel.READ_COMMITTED;
+ case "REPEATABLE-READ":
+ return IsolationLevel.REPEATABLE_READ;
+ case "SERIALIZABLE":
+ return IsolationLevel.SERIALIZABLE;
+ }
+
+ logger.warn("Unknown isolation level {} in current session, fallback to repeatable read", name);
+
+ return IsolationLevel.REPEATABLE_READ;
+ }
+
+ private InitFlow() {
+ }
+
+ private static final class SessionState {
+
+ private final IsolationLevel level;
+
+ @Nullable
+ private final String product;
+
+ @Nullable
+ private final ZoneId timeZone;
+
+ private final Duration lockWaitTimeout;
+
+ private final boolean lockWaitTimeoutSupported;
+
+ SessionState(IsolationLevel level, @Nullable String product, @Nullable ZoneId timeZone) {
+ this(level, product, timeZone, Duration.ZERO, false);
+ }
+
+ private SessionState(
+ IsolationLevel level,
+ @Nullable String product,
+ @Nullable ZoneId timeZone,
+ Duration lockWaitTimeout,
+ boolean lockWaitTimeoutSupported
+ ) {
+ this.level = level;
+ this.product = product;
+ this.timeZone = timeZone;
+ this.lockWaitTimeout = lockWaitTimeout;
+ this.lockWaitTimeoutSupported = lockWaitTimeoutSupported;
+ }
+
+ SessionState lockWaitTimeout(Duration timeout) {
+ return new SessionState(level, product, timeZone, timeout, true);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SessionState)) {
+ return false;
+ }
+
+ SessionState that = (SessionState) o;
+
+ return lockWaitTimeoutSupported == that.lockWaitTimeoutSupported &&
+ level.equals(that.level) &&
+ Objects.equals(product, that.product) &&
+ Objects.equals(timeZone, that.timeZone) &&
+ lockWaitTimeout.equals(that.lockWaitTimeout);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = level.hashCode();
+ result = 31 * result + (product != null ? product.hashCode() : 0);
+ result = 31 * result + (timeZone != null ? timeZone.hashCode() : 0);
+ result = 31 * result + lockWaitTimeout.hashCode();
+ return 31 * result + (lockWaitTimeoutSupported ? 1 : 0);
+ }
+
+ @Override
+ public String toString() {
+ return "SessionState{level=" + level +
+ ", product='" + product +
+ "', timeZone=" + timeZone +
+ ", lockWaitTimeout=" + lockWaitTimeout +
+ ", lockWaitTimeoutSupported=" + lockWaitTimeoutSupported +
+ '}';
+ }
+ }
+}
+
+/**
+ * An implementation of {@link FluxExchangeable} that considers login to the database.
+ *
+ * Not like other {@link FluxExchangeable}s, it is started by a server-side message, which should be an implementation
+ * of {@link HandshakeRequest}.
+ */
+final class HandshakeExchangeable extends FluxExchangeable {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(HandshakeExchangeable.class);
+
+ private static final Map ATTRIBUTES = Collections.emptyMap();
+
+ private static final String CLI_SPECIFIC = "HY000";
+
+ private static final int HANDSHAKE_VERSION = 10;
+
+ private final Sinks.Many requests = Sinks.many().unicast()
+ .onBackpressureBuffer(Queues.one().get());
+
+ private final Client client;
+
+ private final SslMode sslMode;
+
+ private final String database;
+
+ private final String user;
+
+ @Nullable
+ private final CharSequence password;
+
+ private final Set compressions;
+
+ private final int zstdCompressionLevel;
+
+ private boolean handshake = true;
+
+ private MySqlAuthProvider authProvider;
+
+ private byte[] salt;
+
+ private boolean sslCompleted;
+
+ HandshakeExchangeable(Client client, SslMode sslMode, String database, String user,
+ @Nullable CharSequence password, Set compressions,
+ int zstdCompressionLevel) {
+ this.client = client;
+ this.sslMode = sslMode;
+ this.database = database;
+ this.user = user;
+ this.password = password;
+ this.compressions = compressions;
+ this.zstdCompressionLevel = zstdCompressionLevel;
+ this.sslCompleted = sslMode == SslMode.TUNNEL;
+ }
+
+ @Override
+ public void subscribe(CoreSubscriber super ClientMessage> actual) {
+ requests.asFlux().subscribe(actual);
+ }
+
+ @Override
+ public void accept(ServerMessage message, SynchronousSink sink) {
+ if (message instanceof ErrorMessage) {
+ sink.error(((ErrorMessage) message).toException());
+ return;
+ }
+
+ // Ensures it will be initialized only once.
+ if (handshake) {
+ handshake = false;
+ if (message instanceof HandshakeRequest) {
+ HandshakeRequest request = (HandshakeRequest) message;
+ Capability capability = initHandshake(request);
+
+ if (capability.isSslEnabled()) {
+ emitNext(SslRequest.from(capability, client.getContext().getClientCollation().getId()), sink);
+ } else {
+ emitNext(createHandshakeResponse(capability), sink);
+ }
+ } else {
+ sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" +
+ message.getClass().getSimpleName() + "' in init phase"));
+ }
+
+ return;
+ }
+
+ if (message instanceof OkMessage) {
+ logger.trace("Connection (id {}) login success", client.getContext().getConnectionId());
+ client.loginSuccess();
+ sink.complete();
+ } else if (message instanceof SyntheticSslResponseMessage) {
+ sslCompleted = true;
+ emitNext(createHandshakeResponse(client.getContext().getCapability()), sink);
+ } else if (message instanceof AuthMoreDataMessage) {
+ AuthMoreDataMessage msg = (AuthMoreDataMessage) message;
+
+ if (msg.isFailed()) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Connection (id {}) fast authentication failed, use full authentication",
+ client.getContext().getConnectionId());
+ }
+
+ emitNext(createAuthResponse("full authentication"), sink);
+ }
+ // Otherwise success, wait until OK message or Error message.
+ } else if (message instanceof ChangeAuthMessage) {
+ ChangeAuthMessage msg = (ChangeAuthMessage) message;
+
+ authProvider = MySqlAuthProvider.build(msg.getAuthType());
+ salt = msg.getSalt();
+ emitNext(createAuthResponse("change authentication"), sink);
+ } else {
+ sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" +
+ message.getClass().getSimpleName() + "' in login phase"));
+ }
+ }
+
+ @Override
+ public void dispose() {
+ // No particular error condition handling for complete signal.
+ this.requests.tryEmitComplete();
+ }
+
+ private void emitNext(SubsequenceClientMessage message, SynchronousSink sink) {
+ Sinks.EmitResult result = requests.tryEmitNext(message);
+
+ if (result != Sinks.EmitResult.OK) {
+ sink.error(new IllegalStateException("Fail to emit a login request due to " + result));
+ }
+ }
+
+ private AuthResponse createAuthResponse(String phase) {
+ MySqlAuthProvider authProvider = getAndNextProvider();
+
+ if (authProvider.isSslNecessary() && !sslCompleted) {
+ throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), phase), CLI_SPECIFIC);
+ }
+
+ return new AuthResponse(authProvider.authentication(password, salt, client.getContext().getClientCollation()));
+ }
+
+ private Capability clientCapability(Capability serverCapability) {
+ Capability.Builder builder = serverCapability.mutate();
+
+ builder.disableSessionTrack();
+ builder.disableDatabasePinned();
+ builder.disableIgnoreAmbiguitySpace();
+ builder.disableInteractiveTimeout();
+
+ if (sslMode == SslMode.TUNNEL) {
+ // Tunnel does not use MySQL SSL protocol, disable it.
+ builder.disableSsl();
+ } else if (!serverCapability.isSslEnabled()) {
+ // Server unsupported SSL.
+ if (sslMode.requireSsl()) {
+ // Before handshake, Client.context does not be initialized
+ throw new R2dbcPermissionDeniedException("Server does not support SSL but mode '" + sslMode +
+ "' requires SSL", CLI_SPECIFIC);
+ } else if (sslMode.startSsl()) {
+ // SSL has start yet, and client can disable SSL, disable now.
+ client.sslUnsupported();
+ }
+ } else {
+ // The server supports SSL, but the user does not want to use SSL, disable it.
+ if (!sslMode.startSsl()) {
+ builder.disableSsl();
+ }
+ }
+
+ if (isZstdAllowed(serverCapability)) {
+ if (isZstdSupported()) {
+ builder.disableZlibCompression();
+ } else {
+ logger.warn("Server supports zstd, but zstd-jni dependency is missing");
+
+ if (isZlibAllowed(serverCapability)) {
+ builder.disableZstdCompression();
+ } else if (compressions.contains(CompressionAlgorithm.UNCOMPRESSED)) {
+ builder.disableCompression();
+ } else {
+ throw new R2dbcNonTransientResourceException(
+ "Environment does not support a compression algorithm in " + compressions +
+ ", config does not allow uncompressed mode", CLI_SPECIFIC);
+ }
+ }
+ } else if (isZlibAllowed(serverCapability)) {
+ builder.disableZstdCompression();
+ } else if (compressions.contains(CompressionAlgorithm.UNCOMPRESSED)) {
+ builder.disableCompression();
+ } else {
+ throw new R2dbcPermissionDeniedException(
+ "Environment does not support a compression algorithm in " + compressions +
+ ", config does not allow uncompressed mode", CLI_SPECIFIC);
+ }
+
+ if (database.isEmpty()) {
+ builder.disableConnectWithDatabase();
+ }
+
+ if (client.getContext().getLocalInfilePath() == null) {
+ builder.disableLoadDataLocalInfile();
+ }
+
+ if (ATTRIBUTES.isEmpty()) {
+ builder.disableConnectAttributes();
+ }
+
+ return builder.build();
+ }
+
+ private Capability initHandshake(HandshakeRequest message) {
+ HandshakeHeader header = message.getHeader();
+ int handshakeVersion = header.getProtocolVersion();
+ ServerVersion serverVersion = header.getServerVersion();
+
+ if (handshakeVersion < HANDSHAKE_VERSION) {
+ logger.warn("MySQL use handshake V{}, server version is {}, maybe most features are unavailable",
+ handshakeVersion, serverVersion);
+ }
+
+ Capability capability = clientCapability(message.getServerCapability());
+
+ // No need initialize server statuses because it has initialized by read filter.
+ this.client.getContext().initHandshake(header.getConnectionId(), serverVersion, capability);
+ this.authProvider = MySqlAuthProvider.build(message.getAuthType());
+ this.salt = message.getSalt();
+
+ return capability;
+ }
+
+ private MySqlAuthProvider getAndNextProvider() {
+ MySqlAuthProvider authProvider = this.authProvider;
+ this.authProvider = authProvider.next();
+ return authProvider;
+ }
+
+ private HandshakeResponse createHandshakeResponse(Capability capability) {
+ MySqlAuthProvider authProvider = getAndNextProvider();
+
+ if (authProvider.isSslNecessary() && !sslCompleted) {
+ throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), "handshake"),
+ CLI_SPECIFIC);
+ }
+
+ byte[] authorization = authProvider.authentication(password, salt, client.getContext().getClientCollation());
+ String authType = authProvider.getType();
+
+ if (MySqlAuthProvider.NO_AUTH_PROVIDER.equals(authType)) {
+ // Authentication type is not matter because of it has no authentication type.
+ // Server need send a Change Authentication Message after handshake response.
+ authType = MySqlAuthProvider.CACHING_SHA2_PASSWORD;
+ }
+
+ return HandshakeResponse.from(capability, client.getContext().getClientCollation().getId(), user, authorization,
+ authType, database, ATTRIBUTES, zstdCompressionLevel);
+ }
+
+ private boolean isZstdAllowed(Capability capability) {
+ return capability.isZstdCompression() && compressions.contains(CompressionAlgorithm.ZSTD);
+ }
+
+ private boolean isZlibAllowed(Capability capability) {
+ return capability.isZlibCompression() && compressions.contains(CompressionAlgorithm.ZLIB);
+ }
+
+ private static String authFails(String authType, String phase) {
+ return "Authentication type '" + authType + "' must require SSL in " + phase + " phase";
+ }
+
+ private static boolean isZstdSupported() {
+ try {
+ ClassLoader loader = AccessController.doPrivileged((PrivilegedAction) () -> {
+ ClassLoader cl = Thread.currentThread().getContextClassLoader();
+ return cl == null ? ClassLoader.getSystemClassLoader() : cl;
+ });
+ Class.forName("com.github.luben.zstd.Zstd", false, loader);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java
similarity index 71%
rename from src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java
index eab13e88c..bba48841c 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java
@@ -16,13 +16,20 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata;
+import io.asyncer.r2dbc.mysql.api.MySqlRow;
+import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata;
+import io.asyncer.r2dbc.mysql.api.MySqlStatement;
+import io.asyncer.r2dbc.mysql.codec.CodecContext;
import io.asyncer.r2dbc.mysql.codec.Codecs;
+import io.asyncer.r2dbc.mysql.collation.CharCollation;
import io.asyncer.r2dbc.mysql.constant.MySqlType;
import io.r2dbc.spi.ColumnMetadata;
import io.r2dbc.spi.Nullability;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
+import java.lang.reflect.ParameterizedType;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
@@ -37,7 +44,7 @@
*
* @see MySqlStatement#returnGeneratedValues(String...) reading last inserted ID.
*/
-final class InsertSyntheticRow implements Row, RowMetadata, ColumnMetadata {
+final class InsertSyntheticRow implements MySqlRow, MySqlRowMetadata, MySqlColumnMetadata {
private final Codecs codecs;
@@ -45,15 +52,11 @@ final class InsertSyntheticRow implements Row, RowMetadata, ColumnMetadata {
private final long lastInsertId;
- private final ColumnNameSet nameSet;
-
InsertSyntheticRow(Codecs codecs, String keyName, long lastInsertId) {
this.codecs = requireNonNull(codecs, "codecs must not be null");
this.keyName = requireNonNull(keyName, "keyName must not be null");
// lastInsertId may be negative if key is BIGINT UNSIGNED and value overflow than signed int64.
this.lastInsertId = lastInsertId;
- // Singleton name must be sorted.
- this.nameSet = ColumnNameSet.of(keyName);
}
@Override
@@ -96,19 +99,19 @@ public boolean contains(String name) {
}
@Override
- public RowMetadata getMetadata() {
+ public MySqlRowMetadata getMetadata() {
return this;
}
@Override
- public ColumnMetadata getColumnMetadata(int index) {
+ public MySqlColumnMetadata getColumnMetadata(int index) {
assertValidIndex(index);
return this;
}
@Override
- public ColumnMetadata getColumnMetadata(String name) {
+ public MySqlColumnMetadata getColumnMetadata(String name) {
requireNonNull(name, "name must not be null");
assertValidName(name);
@@ -116,7 +119,7 @@ public ColumnMetadata getColumnMetadata(String name) {
}
@Override
- public List getColumnMetadatas() {
+ public List getColumnMetadatas() {
return Collections.singletonList(this);
}
@@ -125,6 +128,11 @@ public MySqlType getType() {
return lastInsertId < 0 ? MySqlType.BIGINT_UNSIGNED : MySqlType.BIGINT;
}
+ @Override
+ public CharCollation getCharCollation(CodecContext context) {
+ return context.getClientCollation();
+ }
+
@Override
public String getName() {
return keyName;
@@ -140,14 +148,26 @@ public Nullability getNullability() {
return Nullability.NON_NULL;
}
- private void assertValidName(String name) {
- if (!contains0(name)) {
- throw new NoSuchElementException("Column name '" + name + "' does not exist in " + this.nameSet);
- }
+ @Override
+ public T get(int index, ParameterizedType type) {
+ throw new IllegalArgumentException(String.format("Cannot decode %s with last inserted ID %s", type,
+ lastInsertId < 0 ? Long.toUnsignedString(lastInsertId) : lastInsertId));
}
- private boolean contains0(String name) {
- return nameSet.contains(name);
+ @Override
+ public T get(String name, ParameterizedType type) {
+ throw new IllegalArgumentException(String.format("Cannot decode %s with last inserted ID %s", type,
+ lastInsertId < 0 ? Long.toUnsignedString(lastInsertId) : lastInsertId));
+ }
+
+ private boolean contains0(final String name) {
+ return keyName.equalsIgnoreCase(name);
+ }
+
+ private void assertValidName(final String name) {
+ if (!contains0(name)) {
+ throw new NoSuchElementException("Column name '" + name + "' does not exist in {" + name + '}');
+ }
}
private T get0(Class> type) {
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java
similarity index 87%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java
index 6d74cf4d0..a71c31986 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java
@@ -16,6 +16,8 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlBatch;
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import reactor.core.publisher.Flux;
@@ -28,20 +30,17 @@
* An implementation of {@link MySqlBatch} for executing a collection of statements in a batch against the
* MySQL database.
*/
-final class MySqlBatchingBatch extends MySqlBatch {
+final class MySqlBatchingBatch implements MySqlBatch {
private final Client client;
private final Codecs codecs;
- private final ConnectionContext context;
-
private final StringJoiner queries = new StringJoiner(";");
- MySqlBatchingBatch(Client client, Codecs codecs, ConnectionContext context) {
+ MySqlBatchingBatch(Client client, Codecs codecs) {
this.client = requireNonNull(client, "client must not be null");
this.codecs = requireNonNull(codecs, "codecs must not be null");
- this.context = requireNonNull(context, "context must not be null");
}
@Override
@@ -63,7 +62,7 @@ public MySqlBatch add(String sql) {
@Override
public Flux execute() {
return QueryFlow.execute(client, getSql())
- .map(messages -> MySqlResult.toResult(false, codecs, context, null, messages));
+ .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, null, messages));
}
@Override
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java
similarity index 60%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java
index af40495f5..61cb1d0b8 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java
@@ -16,32 +16,32 @@
package io.asyncer.r2dbc.mysql;
-import io.r2dbc.spi.ConnectionMetadata;
-import org.jetbrains.annotations.Nullable;
-
-import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
+import io.asyncer.r2dbc.mysql.api.MySqlConnectionMetadata;
+import io.asyncer.r2dbc.mysql.client.Client;
/**
* Connection metadata for a connection connected to MySQL database.
*/
-public final class MySqlConnectionMetadata implements ConnectionMetadata {
-
- private final String version;
+final class MySqlClientConnectionMetadata implements MySqlConnectionMetadata {
- private final String product;
+ private final Client client;
- MySqlConnectionMetadata(String version, @Nullable String product) {
- this.version = requireNonNull(version, "version must not be null");
- this.product = product == null ? "Unknown" : product;
+ MySqlClientConnectionMetadata(Client client) {
+ this.client = client;
}
@Override
public String getDatabaseVersion() {
- return version;
+ return client.getContext().getServerVersion().toString();
+ }
+
+ @Override
+ public boolean isMariaDb() {
+ return client.getContext().isMariaDb();
}
@Override
public String getDatabaseProductName() {
- return product;
+ return client.getContext().getProduct();
}
}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java
similarity index 83%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java
index 4bc9aaca4..8f8d2e9e0 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java
@@ -16,11 +16,14 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata;
+import io.asyncer.r2dbc.mysql.api.MySqlNativeTypeMetadata;
import io.asyncer.r2dbc.mysql.codec.CodecContext;
import io.asyncer.r2dbc.mysql.collation.CharCollation;
import io.asyncer.r2dbc.mysql.constant.MySqlType;
import io.asyncer.r2dbc.mysql.message.server.DefinitionMetadataMessage;
import io.r2dbc.spi.Nullability;
+import org.jetbrains.annotations.VisibleForTesting;
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require;
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
@@ -48,28 +51,29 @@ final class MySqlColumnDescriptor implements MySqlColumnMetadata {
private final int collationId;
- private MySqlColumnDescriptor(int index, short typeId, String name, ColumnDefinition definition,
+ @VisibleForTesting
+ MySqlColumnDescriptor(int index, short typeId, String name, int definitions,
long size, int decimals, int collationId) {
require(index >= 0, "index must not be a negative integer");
require(size >= 0, "size must not be a negative integer");
require(decimals >= 0, "decimals must not be a negative integer");
requireNonNull(name, "name must not be null");
- require(collationId > 0, "collationId must be a positive integer");
- requireNonNull(definition, "definition must not be null");
+
+ MySqlTypeMetadata typeMetadata = new MySqlTypeMetadata(typeId, definitions, collationId);
this.index = index;
- this.typeMetadata = new MySqlTypeMetadata(typeId, definition);
- this.type = MySqlType.of(typeId, definition);
+ this.typeMetadata = typeMetadata;
+ this.type = MySqlType.of(typeMetadata);
this.name = name;
- this.nullability = definition.isNotNull() ? Nullability.NON_NULL : Nullability.NULLABLE;
+ this.nullability = typeMetadata.isNotNull() ? Nullability.NON_NULL : Nullability.NULLABLE;
this.size = size;
this.decimals = decimals;
this.collationId = collationId;
}
static MySqlColumnDescriptor create(int index, DefinitionMetadataMessage message) {
- ColumnDefinition definition = message.getDefinition();
- return new MySqlColumnDescriptor(index, message.getTypeId(), message.getColumn(), definition,
+ int definitions = message.getDefinitions();
+ return new MySqlColumnDescriptor(index, message.getTypeId(), message.getColumn(), definitions,
message.getSize(), message.getDecimals(), message.getCollationId());
}
@@ -88,7 +92,7 @@ public String getName() {
}
@Override
- public MySqlTypeMetadata getNativeTypeMetadata() {
+ public MySqlNativeTypeMetadata getNativeTypeMetadata() {
return typeMetadata;
}
@@ -99,14 +103,13 @@ public Nullability getNullability() {
@Override
public Integer getPrecision() {
+ // FIXME: NEW_DECIMAL and DECIMAL are "exact" fixed-point number.
+ // So the `size` have to subtract:
+ // 1. if signed, 1 byte for the sign
+ // 2. if decimals > 0, 1 byte for the dot
return (int) size;
}
- @Override
- public long getNativePrecision() {
- return size;
- }
-
@Override
public Integer getScale() {
// 0x00 means it is an integer or a static string.
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
similarity index 57%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
index 0eec8645c..39fb91eb6 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java
@@ -16,12 +16,18 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm;
import io.asyncer.r2dbc.mysql.constant.SslMode;
import io.asyncer.r2dbc.mysql.constant.ZeroDateOption;
import io.asyncer.r2dbc.mysql.extension.Extension;
+import io.asyncer.r2dbc.mysql.internal.util.InternalArrays;
import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.resolver.AddressResolverGroup;
import org.jetbrains.annotations.Nullable;
import org.reactivestreams.Publisher;
+import reactor.netty.internal.util.Metrics;
+import reactor.netty.resources.LoopResources;
+import reactor.netty.tcp.TcpResources;
import javax.net.ssl.HostnameVerifier;
import java.net.Socket;
@@ -30,18 +36,22 @@
import java.time.Duration;
import java.time.ZoneId;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.ServiceLoader;
+import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require;
+import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty;
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_STRINGS;
/**
- * MySQL configuration of connection.
+ * A configuration of MySQL connection.
*/
public final class MySqlConnectionConfiguration {
@@ -71,8 +81,11 @@ public final class MySqlConnectionConfiguration {
@Nullable
private final Duration connectTimeout;
- @Nullable
- private final ZoneId serverZoneId;
+ private final boolean preserveInstants;
+
+ private final String connectionTimeZone;
+
+ private final boolean forceConnectionTimeZoneToSession;
private final ZeroDateOption zeroDateOption;
@@ -88,6 +101,14 @@ public final class MySqlConnectionConfiguration {
@Nullable
private final Predicate preferPrepareStatement;
+ private final List sessionVariables;
+
+ @Nullable
+ private final Duration lockWaitTimeout;
+
+ @Nullable
+ private final Duration statementTimeout;
+
@Nullable
private final Path loadLocalInfilePath;
@@ -97,21 +118,42 @@ public final class MySqlConnectionConfiguration {
private final int prepareCacheSize;
+ private final Set compressionAlgorithms;
+
+ private final int zstdCompressionLevel;
+
+ private final LoopResources loopResources;
+
private final Extensions extensions;
@Nullable
private final Publisher passwordPublisher;
+ @Nullable
+ private final AddressResolverGroup> resolver;
+
+ private final boolean metrics;
+
+ private final boolean tinyInt1isBit;
+
private MySqlConnectionConfiguration(
- boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
- boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
- ZeroDateOption zeroDateOption, @Nullable ZoneId serverZoneId,
- String user, @Nullable CharSequence password, @Nullable String database,
- boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement,
- @Nullable Path loadLocalInfilePath, int localInfileBufferSize,
- int queryCacheSize, int prepareCacheSize, Extensions extensions,
- @Nullable Publisher passwordPublisher
- ) {
+ boolean isHost, String domain, int port, MySqlSslConfiguration ssl,
+ boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout,
+ ZeroDateOption zeroDateOption,
+ boolean preserveInstants,
+ String connectionTimeZone,
+ boolean forceConnectionTimeZoneToSession,
+ String user, @Nullable CharSequence password, @Nullable String database,
+ boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement,
+ List sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout,
+ @Nullable Path loadLocalInfilePath, int localInfileBufferSize,
+ int queryCacheSize, int prepareCacheSize,
+ Set compressionAlgorithms, int zstdCompressionLevel,
+ @Nullable LoopResources loopResources,
+ Extensions extensions, @Nullable Publisher passwordPublisher,
+ @Nullable AddressResolverGroup> resolver,
+ boolean metrics,
+ boolean tinyInt1isBit) {
this.isHost = isHost;
this.domain = domain;
this.port = port;
@@ -119,19 +161,30 @@ private MySqlConnectionConfiguration(
this.tcpNoDelay = tcpNoDelay;
this.connectTimeout = connectTimeout;
this.ssl = ssl;
- this.serverZoneId = serverZoneId;
+ this.preserveInstants = preserveInstants;
+ this.connectionTimeZone = requireNonNull(connectionTimeZone, "connectionTimeZone must not be null");
+ this.forceConnectionTimeZoneToSession = forceConnectionTimeZoneToSession;
this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null");
this.user = requireNonNull(user, "user must not be null");
this.password = password;
this.database = database == null || database.isEmpty() ? "" : database;
this.createDatabaseIfNotExist = createDatabaseIfNotExist;
this.preferPrepareStatement = preferPrepareStatement;
+ this.sessionVariables = sessionVariables;
+ this.lockWaitTimeout = lockWaitTimeout;
+ this.statementTimeout = statementTimeout;
this.loadLocalInfilePath = loadLocalInfilePath;
this.localInfileBufferSize = localInfileBufferSize;
this.queryCacheSize = queryCacheSize;
this.prepareCacheSize = prepareCacheSize;
+ this.compressionAlgorithms = compressionAlgorithms;
+ this.zstdCompressionLevel = zstdCompressionLevel;
+ this.loopResources = loopResources == null ? TcpResources.get() : loopResources;
this.extensions = extensions;
this.passwordPublisher = passwordPublisher;
+ this.resolver = resolver;
+ this.metrics = metrics;
+ this.tinyInt1isBit = tinyInt1isBit;
}
/**
@@ -176,9 +229,16 @@ ZeroDateOption getZeroDateOption() {
return zeroDateOption;
}
- @Nullable
- ZoneId getServerZoneId() {
- return serverZoneId;
+ boolean isPreserveInstants() {
+ return preserveInstants;
+ }
+
+ String getConnectionTimeZone() {
+ return connectionTimeZone;
+ }
+
+ boolean isForceConnectionTimeZoneToSession() {
+ return forceConnectionTimeZoneToSession;
}
String getUser() {
@@ -203,6 +263,20 @@ Predicate getPreferPrepareStatement() {
return preferPrepareStatement;
}
+ List getSessionVariables() {
+ return sessionVariables;
+ }
+
+ @Nullable
+ Duration getLockWaitTimeout() {
+ return lockWaitTimeout;
+ }
+
+ @Nullable
+ Duration getStatementTimeout() {
+ return statementTimeout;
+ }
+
@Nullable
Path getLoadLocalInfilePath() {
return loadLocalInfilePath;
@@ -220,6 +294,18 @@ int getPrepareCacheSize() {
return prepareCacheSize;
}
+ Set getCompressionAlgorithms() {
+ return compressionAlgorithms;
+ }
+
+ int getZstdCompressionLevel() {
+ return zstdCompressionLevel;
+ }
+
+ LoopResources getLoopResources() {
+ return loopResources;
+ }
+
Extensions getExtensions() {
return extensions;
}
@@ -229,6 +315,19 @@ Publisher getPasswordPublisher() {
return passwordPublisher;
}
+ @Nullable
+ AddressResolverGroup> getResolver() {
+ return resolver;
+ }
+
+ boolean isMetrics() {
+ return metrics;
+ }
+
+ boolean isTinyInt1isBit() {
+ return tinyInt1isBit;
+ }
+
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -245,54 +344,81 @@ public boolean equals(Object o) {
tcpKeepAlive == that.tcpKeepAlive &&
tcpNoDelay == that.tcpNoDelay &&
Objects.equals(connectTimeout, that.connectTimeout) &&
- Objects.equals(serverZoneId, that.serverZoneId) &&
+ preserveInstants == that.preserveInstants &&
+ Objects.equals(connectionTimeZone, that.connectionTimeZone) &&
+ forceConnectionTimeZoneToSession == that.forceConnectionTimeZoneToSession &&
zeroDateOption == that.zeroDateOption &&
user.equals(that.user) &&
Objects.equals(password, that.password) &&
database.equals(that.database) &&
createDatabaseIfNotExist == that.createDatabaseIfNotExist &&
Objects.equals(preferPrepareStatement, that.preferPrepareStatement) &&
+ sessionVariables.equals(that.sessionVariables) &&
+ Objects.equals(lockWaitTimeout, that.lockWaitTimeout) &&
+ Objects.equals(statementTimeout, that.statementTimeout) &&
Objects.equals(loadLocalInfilePath, that.loadLocalInfilePath) &&
localInfileBufferSize == that.localInfileBufferSize &&
queryCacheSize == that.queryCacheSize &&
prepareCacheSize == that.prepareCacheSize &&
+ compressionAlgorithms.equals(that.compressionAlgorithms) &&
+ zstdCompressionLevel == that.zstdCompressionLevel &&
+ Objects.equals(loopResources, that.loopResources) &&
extensions.equals(that.extensions) &&
- Objects.equals(passwordPublisher, that.passwordPublisher);
+ Objects.equals(passwordPublisher, that.passwordPublisher) &&
+ Objects.equals(resolver, that.resolver) &&
+ metrics == that.metrics &&
+ tinyInt1isBit == that.tinyInt1isBit;
}
@Override
public int hashCode() {
return Objects.hash(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, connectTimeout,
- serverZoneId, zeroDateOption, user, password, database, createDatabaseIfNotExist,
- preferPrepareStatement, loadLocalInfilePath, localInfileBufferSize, queryCacheSize,
- prepareCacheSize, extensions, passwordPublisher);
+ preserveInstants, connectionTimeZone, forceConnectionTimeZoneToSession,
+ zeroDateOption, user, password, database, createDatabaseIfNotExist,
+ preferPrepareStatement,
+ sessionVariables,
+ lockWaitTimeout,
+ statementTimeout,
+ loadLocalInfilePath, localInfileBufferSize,
+ queryCacheSize, prepareCacheSize,
+ compressionAlgorithms, zstdCompressionLevel,
+ loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit);
}
@Override
public String toString() {
- if (isHost) {
- return "MySqlConnectionConfiguration{host='" + domain + "', port=" + port + ", ssl=" + ssl +
- ", tcpNoDelay=" + tcpNoDelay + ", tcpKeepAlive=" + tcpKeepAlive +
- ", connectTimeout=" + connectTimeout + ", serverZoneId=" + serverZoneId +
- ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password +
+ return "MySqlConnectionConfiguration{" +
+ (isHost ? "host='" + domain + "', port=" + port + ", ssl=" + ssl +
+ ", tcpNoDelay=" + tcpNoDelay + ", tcpKeepAlive=" + tcpKeepAlive :
+ "unixSocket='" + domain + "'") +
+ buildCommonToStringPart() +
+ '}';
+ }
+
+ private String buildCommonToStringPart() {
+ return ", connectTimeout=" + connectTimeout +
+ ", preserveInstants=" + preserveInstants +
+ ", connectionTimeZone=" + connectionTimeZone +
+ ", forceConnectionTimeZoneToSession=" + forceConnectionTimeZoneToSession +
+ ", zeroDateOption=" + zeroDateOption +
+ ", user='" + user + "', password=" + password +
", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist +
", preferPrepareStatement=" + preferPrepareStatement +
+ ", sessionVariables=" + sessionVariables +
+ ", lockWaitTimeout=" + lockWaitTimeout +
+ ", statementTimeout=" + statementTimeout +
", loadLocalInfilePath=" + loadLocalInfilePath +
", localInfileBufferSize=" + localInfileBufferSize +
- ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize +
- ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}';
- }
-
- return "MySqlConnectionConfiguration{unixSocket='" + domain +
- "', connectTimeout=" + connectTimeout + ", serverZoneId=" + serverZoneId +
- ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password +
- ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist +
- ", preferPrepareStatement=" + preferPrepareStatement +
- ", loadLocalInfilePath=" + loadLocalInfilePath +
- ", localInfileBufferSize=" + localInfileBufferSize +
- ", queryCacheSize=" + queryCacheSize +
- ", prepareCacheSize=" + prepareCacheSize + ", extensions=" + extensions +
- ", passwordPublisher=" + passwordPublisher + '}';
+ ", queryCacheSize=" + queryCacheSize +
+ ", prepareCacheSize=" + prepareCacheSize +
+ ", compressionAlgorithms=" + compressionAlgorithms +
+ ", zstdCompressionLevel=" + zstdCompressionLevel +
+ ", loopResources=" + loopResources +
+ ", extensions=" + extensions +
+ ", passwordPublisher=" + passwordPublisher +
+ ", resolver=" + resolver +
+ ", metrics=" + metrics +
+ ", tinyInt1isBit=" + tinyInt1isBit;
}
/**
@@ -321,8 +447,11 @@ public static final class Builder {
private ZeroDateOption zeroDateOption = ZeroDateOption.USE_NULL;
- @Nullable
- private ZoneId serverZoneId;
+ private boolean preserveInstants = true;
+
+ private String connectionTimeZone = "LOCAL";
+
+ private boolean forceConnectionTimeZoneToSession;
@Nullable
private SslMode sslMode;
@@ -354,6 +483,14 @@ public static final class Builder {
@Nullable
private Predicate preferPrepareStatement;
+ @Nullable
+ private Duration lockWaitTimeout;
+
+ @Nullable
+ private Duration statementTimeout;
+
+ private List sessionVariables = Collections.emptyList();
+
@Nullable
private Path loadLocalInfilePath;
@@ -363,6 +500,14 @@ public static final class Builder {
private int prepareCacheSize = 256;
+ private Set compressionAlgorithms =
+ Collections.singleton(CompressionAlgorithm.UNCOMPRESSED);
+
+ private int zstdCompressionLevel = 3;
+
+ @Nullable
+ private LoopResources loopResources;
+
private boolean autodetectExtensions = true;
private final List extensions = new ArrayList<>();
@@ -370,6 +515,13 @@ public static final class Builder {
@Nullable
private Publisher passwordPublisher;
+ @Nullable
+ private AddressResolverGroup> resolver;
+
+ private boolean metrics;
+
+ private boolean tinyInt1isBit = true;
+
/**
* Builds an immutable {@link MySqlConnectionConfiguration} with current options.
*
@@ -392,14 +544,23 @@ public MySqlConnectionConfiguration build() {
MySqlSslConfiguration ssl = MySqlSslConfiguration.create(sslMode, tlsVersion, sslHostnameVerifier,
sslCa, sslKey, sslKeyPassword, sslCert, sslContextBuilderCustomizer);
return new MySqlConnectionConfiguration(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay,
- connectTimeout, zeroDateOption, serverZoneId, user, password, database,
- createDatabaseIfNotExist, preferPrepareStatement, loadLocalInfilePath,
+ connectTimeout, zeroDateOption,
+ preserveInstants,
+ connectionTimeZone,
+ forceConnectionTimeZoneToSession,
+ user, password, database,
+ createDatabaseIfNotExist, preferPrepareStatement,
+ sessionVariables,
+ lockWaitTimeout,
+ statementTimeout,
+ loadLocalInfilePath,
localInfileBufferSize, queryCacheSize, prepareCacheSize,
- Extensions.from(extensions, autodetectExtensions), passwordPublisher);
+ compressionAlgorithms, zstdCompressionLevel, loopResources,
+ Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit);
}
/**
- * Configure the database. Default no database.
+ * Configures the database. Default no database.
*
* @param database the database, or {@code null} if no database want to be login.
* @return this {@link Builder}.
@@ -411,7 +572,7 @@ public Builder database(@Nullable String database) {
}
/**
- * Configure to create the database given in the configuration if it does not yet exist. Default to
+ * Configures to create the database given in the configuration if it does not yet exist. Default to
* {@code false}.
*
* @param enabled to discover and register extensions.
@@ -424,7 +585,7 @@ public Builder createDatabaseIfNotExist(boolean enabled) {
}
/**
- * Configure the Unix Domain Socket to connect to.
+ * Configures the Unix Domain Socket to connect to.
*
* @param unixSocket the socket file path.
* @return this {@link Builder}.
@@ -438,7 +599,7 @@ public Builder unixSocket(String unixSocket) {
}
/**
- * Configure the host.
+ * Configures the host.
*
* @param host the host.
* @return this {@link Builder}.
@@ -452,7 +613,7 @@ public Builder host(String host) {
}
/**
- * Configure the password, MySQL allows to login without password.
+ * Configures the password. Default login without password.
*
* Note: for memory security, should not use intern {@link String} for password.
*
@@ -466,7 +627,7 @@ public Builder password(@Nullable CharSequence password) {
}
/**
- * Configure the port. Defaults to {@code 3306}.
+ * Configures the port. Defaults to {@code 3306}.
*
* @param port the port.
* @return this {@link Builder}.
@@ -481,9 +642,9 @@ public Builder port(int port) {
}
/**
- * Configure the connection timeout. Default no timeout.
+ * Configures the connection timeout. Default no timeout.
*
- * @param connectTimeout the connection timeout, or {@code null} if has no timeout.
+ * @param connectTimeout the connection timeout, or {@code null} if no timeout.
* @return this {@link Builder}.
* @since 0.8.1
*/
@@ -493,7 +654,7 @@ public Builder connectTimeout(@Nullable Duration connectTimeout) {
}
/**
- * Set the user for login the database.
+ * Configures the user for login the database.
*
* @param user the user.
* @return this {@link Builder}.
@@ -518,21 +679,68 @@ public Builder username(String user) {
}
/**
- * Enforce the time zone of server. Default to query server time zone in initialization (no
- * enforce).
+ * Configures the time zone conversion. Default to {@code true} means enable conversion between JVM
+ * and {@link #connectionTimeZone(String)}.
+ *
+ * Note: disable it will ignore the time zone of connection, and use the JVM local time zone.
*
- * @param serverZoneId the {@link ZoneId}, or {@code null} if query in initialization.
- * @return this {@link Builder}.
+ * @param enabled {@code true} to preserve instants, or {@code false} to disable conversion.
+ * @return {@link Builder this}
+ * @since 1.1.2
+ */
+ public Builder preserveInstants(boolean enabled) {
+ this.preserveInstants = enabled;
+ return this;
+ }
+
+ /**
+ * Configures the time zone of connection. Default to {@code LOCAL} means use JVM local time zone.
+ * {@code "SERVER"} means querying the server-side timezone during initialization.
+ *
+ * @param connectionTimeZone {@code "LOCAL"}, {@code "SERVER"}, or a valid ID of {@code ZoneId}.
+ * @return {@link Builder this}
+ * @throws IllegalArgumentException if {@code connectionTimeZone} is {@code null} or empty.
+ * @since 1.1.2
+ */
+ public Builder connectionTimeZone(String connectionTimeZone) {
+ requireNonEmpty(connectionTimeZone, "connectionTimeZone must not be empty");
+
+ this.connectionTimeZone = connectionTimeZone;
+ return this;
+ }
+
+ /**
+ * Configures to force the connection time zone to session time zone. Default to {@code false}. Used
+ * only if the {@link #connectionTimeZone(String)} is not {@code "SERVER"}.
+ *
+ * Note: alter the time zone of session will affect the results of MySQL date/time functions, e.g.
+ * {@code NOW([n])}, {@code CURRENT_TIME([n])}, {@code CURRENT_DATE()}, etc. Please use with caution.
+ *
+ * @param enabled {@code true} to force the connection time zone to session time zone.
+ * @return {@link Builder this}
+ * @since 1.1.2
+ */
+ public Builder forceConnectionTimeZoneToSession(boolean enabled) {
+ this.forceConnectionTimeZoneToSession = enabled;
+ return this;
+ }
+
+ /**
+ * Configures the time zone of server. Since 1.1.2, default to use JVM local time zone.
+ *
+ * @param serverZoneId the {@link ZoneId}, or {@code null} if query server during initialization.
+ * @return {@link Builder this}
* @since 0.8.2
+ * @deprecated since 1.1.2, use {@link #connectionTimeZone(String)} instead.
*/
+ @Deprecated
public Builder serverZoneId(@Nullable ZoneId serverZoneId) {
- this.serverZoneId = serverZoneId;
- return this;
+ return connectionTimeZone(serverZoneId == null ? "SERVER" : serverZoneId.getId());
}
/**
- * Configure the {@link ZeroDateOption}. It is a behavior option when this driver receives a value of
- * zero-date.
+ * Configures the {@link ZeroDateOption}. Default to {@link ZeroDateOption#USE_NULL}. It is a
+ * behavior option when this driver receives a value of zero-date.
*
* @param zeroDate the {@link ZeroDateOption}.
* @return this {@link Builder}.
@@ -545,7 +753,7 @@ public Builder zeroDateOption(ZeroDateOption zeroDate) {
}
/**
- * Configure ssl mode. See also {@link SslMode}.
+ * Configures ssl mode. See also {@link SslMode}.
*
* @param sslMode the SSL mode to use.
* @return this {@link Builder}.
@@ -558,7 +766,7 @@ public Builder sslMode(SslMode sslMode) {
}
/**
- * Configure TLS versions, see {@link io.asyncer.r2dbc.mysql.constant.TlsVersions}.
+ * Configures TLS versions, see {@link io.asyncer.r2dbc.mysql.constant.TlsVersions TlsVersions}.
*
* @param tlsVersion TLS versions.
* @return this {@link Builder}.
@@ -581,7 +789,7 @@ public Builder tlsVersion(String... tlsVersion) {
}
/**
- * Configure SSL {@link HostnameVerifier}, it is available only set {@link #sslMode(SslMode)} as
+ * Configures SSL {@link HostnameVerifier}, it is available only set {@link #sslMode(SslMode)} as
* {@link SslMode#VERIFY_IDENTITY}. It is useful when server was using special Certificates or need
* special verification.
*
@@ -599,7 +807,7 @@ public Builder sslHostnameVerifier(HostnameVerifier sslHostnameVerifier) {
}
/**
- * Configure SSL root certification for server certificate validation. It is only available if the
+ * Configures SSL root certification for server certificate validation. It is only available if the
* {@link #sslMode(SslMode)} is configured for verify server certification.
*
* Default is {@code null}, which means that the default algorithm is used for the trust manager.
@@ -614,7 +822,7 @@ public Builder sslCa(@Nullable String sslCa) {
}
/**
- * Configure client SSL certificate for client authentication.
+ * Configures client SSL certificate for client authentication.
*
* The {@link #sslCert} and {@link #sslKey} must be both non-{@code null} or both {@code null}.
*
@@ -628,7 +836,7 @@ public Builder sslCert(@Nullable String sslCert) {
}
/**
- * Configure client SSL key for client authentication.
+ * Configures client SSL key for client authentication.
*
* The {@link #sslCert} and {@link #sslKey} must be both non-{@code null} or both {@code null}.
*
@@ -642,7 +850,7 @@ public Builder sslKey(@Nullable String sslKey) {
}
/**
- * Configure the password of SSL key file for client certificate authentication.
+ * Configures the password of SSL key file for client certificate authentication.
*
* It will be used only if {@link #sslKey} and {@link #sslCert} non-null.
*
@@ -657,7 +865,7 @@ public Builder sslKeyPassword(@Nullable CharSequence sslKeyPassword) {
}
/**
- * Configure a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL
+ * Configures a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL
* connection attempt to allow for just-in-time configuration updates. The {@link Function} gets
* called with the prepared {@link SslContextBuilder} that has all configuration options applied. The
* customizer may return the same builder or return a new builder instance to be used to build the SSL
@@ -677,7 +885,7 @@ public Builder sslContextBuilderCustomizer(
}
/**
- * Configure TCP KeepAlive.
+ * Configures TCP KeepAlive.
*
* @param enabled whether to enable TCP KeepAlive
* @return this {@link Builder}
@@ -690,7 +898,7 @@ public Builder tcpKeepAlive(boolean enabled) {
}
/**
- * Configure TCP NoDelay.
+ * Configures TCP NoDelay.
*
* @param enabled whether to enable TCP NoDelay
* @return this {@link Builder}
@@ -703,7 +911,7 @@ public Builder tcpNoDelay(boolean enabled) {
}
/**
- * Configure the protocol of parametrized statements to the text protocol.
+ * Configures the protocol of parameterized statements to the text protocol.
*
* The text protocol is default protocol that's using client-preparing. See also MySQL
* documentations.
@@ -717,7 +925,7 @@ public Builder useClientPrepareStatement() {
}
/**
- * Configure the protocol of parametrized statements to the binary protocol.
+ * Configures the protocol of parameterized statements to the binary protocol.
*
* The binary protocol is compact protocol that's using server-preparing. See also MySQL
* documentations.
@@ -730,7 +938,7 @@ public Builder useServerPrepareStatement() {
}
/**
- * Configure the protocol of parametrized statements and prepare-preferred simple statements to the
+ * Configures the protocol of parameterized statements and prepare-preferred simple statements to the
* binary protocol.
*
* The {@code preferPrepareStatement} configures whether to prefer prepare execution on a
@@ -753,6 +961,47 @@ public Builder useServerPrepareStatement(Predicate preferPrepareStatemen
return this;
}
+ /**
+ * Configures the session variables, used to set session variables immediately after login. Default no
+ * session variables to set. It should be a list of key-value pairs. e.g.
+ * {@code ["sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'", "time_zone=00:00"]}.
+ *
+ * @param sessionVariables the session variables to set.
+ * @return {@link Builder this}
+ * @throws IllegalArgumentException if {@code sessionVariables} is {@code null}.
+ * @since 1.1.2
+ */
+ public Builder sessionVariables(String... sessionVariables) {
+ requireNonNull(sessionVariables, "sessionVariables must not be null");
+
+ this.sessionVariables = InternalArrays.toImmutableList(sessionVariables);
+ return this;
+ }
+
+ /**
+ * Configures the lock wait timeout. Default to use the server-side default value.
+ *
+ * @param lockWaitTimeout the lock wait timeout, or {@code null} to use the server-side default value.
+ * @return {@link Builder this}
+ * @since 1.1.3
+ */
+ public Builder lockWaitTimeout(@Nullable Duration lockWaitTimeout) {
+ this.lockWaitTimeout = lockWaitTimeout;
+ return this;
+ }
+
+ /**
+ * Configures the statement timeout. Default to use the server-side default value.
+ *
+ * @param statementTimeout the statement timeout, or {@code null} to use the server-side default value.
+ * @return {@link Builder this}
+ * @since 1.1.3
+ */
+ public Builder statementTimeout(@Nullable Duration statementTimeout) {
+ this.statementTimeout = statementTimeout;
+ return this;
+ }
+
/**
* Configures to allow the {@code LOAD DATA LOCAL INFILE} statement in the given {@code path} or
* disallow the statement. Default to {@code null} which means not allow the statement.
@@ -804,7 +1053,7 @@ public Builder queryCacheSize(int queryCacheSize) {
/**
* Configures the maximum size of the server-preparing cache. Usually it should be power of two.
* Default to {@code 256}. Driver will use unbounded cache if size is less than {@code 0}. It is used
- * only if using server-preparing parametrized statements, i.e. the {@link #useServerPrepareStatement}
+ * only if using server-preparing parameterized statements, i.e. the {@link #useServerPrepareStatement}
* is set.
*
* Notice: the cache is using EC model (the PACELC theorem) for ensure consistency. Consistency is
@@ -822,6 +1071,78 @@ public Builder prepareCacheSize(int prepareCacheSize) {
return this;
}
+ /**
+ * Configures the compression algorithms. Default to [{@link CompressionAlgorithm#UNCOMPRESSED}].
+ *
+ * It will auto choose an algorithm that's contained in the list and supported by the server,
+ * preferring zstd, then zlib. If the list does not contain {@link CompressionAlgorithm#UNCOMPRESSED}
+ * and the server does not support any algorithm in the list, an exception will be thrown when
+ * connecting.
+ *
+ * Note: zstd requires a dependency {@code com.github.luben:zstd-jni}.
+ *
+ * @param compressionAlgorithms the list of compression algorithms.
+ * @return {@link Builder this}.
+ * @throws IllegalArgumentException if {@code compressionAlgorithms} is {@code null} or empty.
+ * @since 1.1.2
+ */
+ public Builder compressionAlgorithms(CompressionAlgorithm... compressionAlgorithms) {
+ requireNonNull(compressionAlgorithms, "compressionAlgorithms must not be null");
+ require(compressionAlgorithms.length != 0, "compressionAlgorithms must not be empty");
+
+ if (compressionAlgorithms.length == 1) {
+ requireNonNull(compressionAlgorithms[0], "compressionAlgorithms must not contain null");
+ this.compressionAlgorithms = Collections.singleton(compressionAlgorithms[0]);
+ } else {
+ Set algorithms = EnumSet.noneOf(CompressionAlgorithm.class);
+
+ for (CompressionAlgorithm algorithm : compressionAlgorithms) {
+ requireNonNull(algorithm, "compressionAlgorithms must not contain null");
+ algorithms.add(algorithm);
+ }
+
+ this.compressionAlgorithms = algorithms;
+ }
+
+ return this;
+ }
+
+ /**
+ * Configures the zstd compression level. Default to {@code 3}.
+ *
+ * It is only used if zstd is chosen for the connection.
+ *
+ * Note: MySQL protocol does not allow to set the zlib compression level of the server, only zstd is
+ * configurable.
+ *
+ * @param level the compression level.
+ * @return {@link Builder this}.
+ * @throws IllegalArgumentException if {@code level} is not between 1 and 22.
+ * @see
+ * MySQL Connection Options --zstd-compression-level
+ * @since 1.1.2
+ */
+ public Builder zstdCompressionLevel(int level) {
+ require(level >= 1 && level <= 22, "level must be between 1 and 22");
+
+ this.zstdCompressionLevel = level;
+ return this;
+ }
+
+ /**
+ * Configures the {@link LoopResources} for the driver. Default to
+ * {@link TcpResources#get() global tcp resources}.
+ *
+ * @param loopResources the {@link LoopResources}.
+ * @return this {@link Builder}.
+ * @throws IllegalArgumentException if {@code loopResources} is {@code null}.
+ * @since 1.1.2
+ */
+ public Builder loopResources(LoopResources loopResources) {
+ this.loopResources = requireNonNull(loopResources, "loopResources must not be null");
+ return this;
+ }
+
/**
* Configures whether to use {@link ServiceLoader} to discover and register extensions. Defaults to
* {@code true}.
@@ -863,6 +1184,56 @@ public Builder passwordPublisher(Publisher passwordPublisher) {
return this;
}
+ /**
+ * Sets the {@link AddressResolverGroup} for resolving host addresses.
+ *
+ * This can be used to customize the DNS resolution mechanism, which is particularly useful in environments
+ * with specific DNS configuration needs or where a custom DNS resolver is required.
+ *
+ * @param resolver the resolver group to use for host address resolution.
+ * @return this {@link Builder}.
+ * @since 1.2.0
+ */
+ public Builder resolver(AddressResolverGroup> resolver) {
+ this.resolver = resolver;
+ return this;
+ }
+
+ /**
+ * Option to enable metrics to be collected and registered in Micrometer's globalRegistry
+ * with {@link reactor.netty.tcp.TcpClient#metrics(boolean)}. Defaults to {@code false}.
+ *
+ * Note: It is required to add {@code io.micrometer.micrometer-core} dependency to classpath.
+ *
+ * @param enabled enable metrics for {@link reactor.netty.tcp.TcpClient}.
+ * @return this {@link Builder}
+ * @throws IllegalArgumentException if {@code io.micrometer:micrometer-core} is not on the classpath.
+ * @since 1.3.2
+ */
+ public Builder metrics(boolean enabled) {
+ require(!enabled || Metrics.isMicrometerAvailable(),
+ "dependency `io.micrometer:micrometer-core` must be added to classpath if metrics enabled"
+ );
+ this.metrics = enabled;
+ return this;
+ }
+
+ /**
+ * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type.
+ * When enabled, TINYINT(1) columns will be treated as BIT. Defaults to {@code true}.
+ *
+ * Note: Only signed TINYINT(1) columns can be treated as BIT or Boolean.
+ * Ref: https://bugs.mysql.com/bug.php?id=100309
+ *
+ * @param tinyInt1isBit {@code true} to treat TINYINT(1) as BIT
+ * @return this {@link Builder}
+ * @since 1.4.0
+ */
+ public Builder tinyInt1isBit(boolean tinyInt1isBit) {
+ this.tinyInt1isBit = tinyInt1isBit;
+ return this;
+ }
+
private SslMode requireSslMode() {
SslMode sslMode = this.sslMode;
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java
new file mode 100644
index 000000000..094674f2a
--- /dev/null
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2023 asyncer.io projects
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.asyncer.r2dbc.mysql;
+
+import io.asyncer.r2dbc.mysql.api.MySqlConnection;
+import io.asyncer.r2dbc.mysql.cache.Caches;
+import io.asyncer.r2dbc.mysql.cache.QueryCache;
+import io.asyncer.r2dbc.mysql.client.Client;
+import io.asyncer.r2dbc.mysql.internal.util.StringUtils;
+import io.netty.channel.unix.DomainSocketAddress;
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.ConnectionFactoryMetadata;
+import org.jetbrains.annotations.Nullable;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Mono;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.time.ZoneId;
+import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Supplier;
+
+import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
+
+/**
+ * An implementation of {@link ConnectionFactory} for creating connections to a MySQL database.
+ */
+public final class MySqlConnectionFactory implements ConnectionFactory {
+
+ private final MySqlConnectionConfiguration configuration;
+ private final LazyQueryCache queryCache;
+
+ private MySqlConnectionFactory(MySqlConnectionConfiguration configuration) {
+ this.configuration = configuration;
+ this.queryCache = new LazyQueryCache(configuration.getQueryCacheSize());
+ }
+
+ @Override
+ public Mono extends MySqlConnection> create() {
+ MySqlSslConfiguration ssl;
+ SocketAddress address;
+
+ if (configuration.isHost()) {
+ ssl = configuration.getSsl();
+ address = InetSocketAddress.createUnresolved(configuration.getDomain(),
+ configuration.getPort());
+ } else {
+ ssl = MySqlSslConfiguration.disabled();
+ address = new DomainSocketAddress(configuration.getDomain());
+ }
+
+ String user = configuration.getUser();
+ CharSequence password = configuration.getPassword();
+ Publisher passwordPublisher = configuration.getPasswordPublisher();
+
+ if (Objects.nonNull(passwordPublisher)) {
+ return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection(
+ configuration, ssl,
+ queryCache,
+ address,
+ user,
+ token
+ ));
+ }
+
+ return getMySqlConnection(
+ configuration, ssl,
+ queryCache,
+ address,
+ user,
+ password
+ );
+ }
+
+ @Override
+ public ConnectionFactoryMetadata getMetadata() {
+ return MySqlConnectionFactoryMetadata.INSTANCE;
+ }
+
+ /**
+ * Creates a {@link MySqlConnectionFactory} with a {@link MySqlConnectionConfiguration}.
+ *
+ * @param configuration the {@link MySqlConnectionConfiguration}.
+ * @return configured {@link MySqlConnectionFactory}.
+ */
+ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configuration) {
+ requireNonNull(configuration, "configuration must not be null");
+ return new MySqlConnectionFactory(configuration);
+ }
+
+ /**
+ * Gets an initialized {@link MySqlConnection} from authentication credential and configurations.
+ *
+ * It contains following steps:
+ *
Create connection context
+ * Connect to MySQL server with TCP or Unix Domain Socket
+ * Handshake/login and init handshake states
+ * Init session states
+ *
+ * @param configuration the connection configuration.
+ * @param ssl the SSL configuration.
+ * @param queryCache lazy-init query cache, it is shared among all connections from the same factory.
+ * @param address TCP or Unix Domain Socket address.
+ * @param user the user of the authentication.
+ * @param password the password of the authentication.
+ * @return a {@link MySqlConnection}.
+ */
+ private static Mono getMySqlConnection(
+ final MySqlConnectionConfiguration configuration,
+ final MySqlSslConfiguration ssl,
+ final LazyQueryCache queryCache,
+ final SocketAddress address,
+ final String user,
+ @Nullable final CharSequence password
+ ) {
+ return Mono.fromSupplier(() -> {
+ ZoneId connectionTimeZone = retrieveZoneId(configuration.getConnectionTimeZone());
+ return new ConnectionContext(
+ configuration.getZeroDateOption(),
+ configuration.getLoadLocalInfilePath(),
+ configuration.getLocalInfileBufferSize(),
+ configuration.isTinyInt1isBit(),
+ configuration.isPreserveInstants(),
+ connectionTimeZone
+ );
+ }).flatMap(context -> Client.connect(
+ ssl,
+ address,
+ configuration.isTcpKeepAlive(),
+ configuration.isTcpNoDelay(),
+ context,
+ configuration.getConnectTimeout(),
+ configuration.getLoopResources(),
+ configuration.getResolver(),
+ configuration.isMetrics()
+ )).flatMap(client -> {
+ // Lazy init database after handshake/login
+ boolean deferDatabase = configuration.isCreateDatabaseIfNotExist();
+ String database = configuration.getDatabase();
+ String loginDb = deferDatabase ? "" : database;
+ String sessionDb = deferDatabase ? database : "";
+
+ return InitFlow.initHandshake(
+ client,
+ ssl.getSslMode(),
+ loginDb,
+ user,
+ password,
+ configuration.getCompressionAlgorithms(),
+ configuration.getZstdCompressionLevel()
+ ).then(InitFlow.initSession(
+ client,
+ sessionDb,
+ configuration.getPrepareCacheSize(),
+ configuration.getSessionVariables(),
+ configuration.isForceConnectionTimeZoneToSession(),
+ configuration.getLockWaitTimeout(),
+ configuration.getStatementTimeout(),
+ configuration.getExtensions()
+ )).map(codecs -> new MySqlSimpleConnection(
+ client,
+ codecs,
+ queryCache.get(),
+ configuration.getPreferPrepareStatement()
+ )).onErrorResume(e -> client.forceClose().then(Mono.error(e)));
+ });
+ }
+
+ @Nullable
+ private static ZoneId retrieveZoneId(String timeZone) {
+ if ("LOCAL".equalsIgnoreCase(timeZone)) {
+ return ZoneId.systemDefault().normalized();
+ } else if ("SERVER".equalsIgnoreCase(timeZone)) {
+ return null;
+ }
+
+ return StringUtils.parseZoneId(timeZone);
+ }
+
+ private static final class LazyQueryCache implements Supplier {
+
+ private final int capacity;
+
+ private final ReentrantLock lock = new ReentrantLock();
+
+ @Nullable
+ private volatile QueryCache cache;
+
+ private LazyQueryCache(int capacity) {
+ this.capacity = capacity;
+ }
+
+ @Override
+ public QueryCache get() {
+ QueryCache cache = this.cache;
+ if (cache == null) {
+ lock.lock();
+ try {
+ if ((cache = this.cache) == null) {
+ this.cache = cache = Caches.createQueryCache(capacity);
+ }
+ return cache;
+ } finally {
+ lock.unlock();
+ }
+ }
+ return cache;
+ }
+ }
+}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java
similarity index 100%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java
new file mode 100644
index 000000000..5905c56ca
--- /dev/null
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java
@@ -0,0 +1,630 @@
+/*
+ * Copyright 2023 asyncer.io projects
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.asyncer.r2dbc.mysql;
+
+import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm;
+import io.asyncer.r2dbc.mysql.constant.SslMode;
+import io.asyncer.r2dbc.mysql.constant.ZeroDateOption;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.resolver.AddressResolverGroup;
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.ConnectionFactoryOptions;
+import io.r2dbc.spi.ConnectionFactoryProvider;
+import io.r2dbc.spi.Option;
+import org.reactivestreams.Publisher;
+import reactor.netty.resources.LoopResources;
+
+import javax.net.ssl.HostnameVerifier;
+import java.time.Duration;
+import java.time.ZoneId;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
+import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_STRINGS;
+import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT;
+import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE;
+import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER;
+import static io.r2dbc.spi.ConnectionFactoryOptions.HOST;
+import static io.r2dbc.spi.ConnectionFactoryOptions.LOCK_WAIT_TIMEOUT;
+import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD;
+import static io.r2dbc.spi.ConnectionFactoryOptions.PORT;
+import static io.r2dbc.spi.ConnectionFactoryOptions.SSL;
+import static io.r2dbc.spi.ConnectionFactoryOptions.STATEMENT_TIMEOUT;
+import static io.r2dbc.spi.ConnectionFactoryOptions.USER;
+
+/**
+ * An implementation of {@link ConnectionFactoryProvider} for creating {@link MySqlConnectionFactory}s.
+ */
+public final class MySqlConnectionFactoryProvider implements ConnectionFactoryProvider {
+
+ /**
+ * The name of the driver used for discovery, should not be changed.
+ */
+ public static final String MYSQL_DRIVER = "mysql";
+
+ /**
+ * Option to set the Unix Domain Socket.
+ *
+ * @since 0.8.1
+ */
+ public static final Option UNIX_SOCKET = Option.valueOf("unixSocket");
+
+ /**
+ * Option to set the time zone conversion. Default to {@code true} means enable conversion between JVM
+ * and {@link #CONNECTION_TIME_ZONE}.
+ *
+ * Note: disable it will ignore the time zone of connection, and use the JVM local time zone.
+ *
+ * @since 1.1.2
+ */
+ public static final Option PRESERVE_INSTANTS = Option.valueOf("preserveInstants");
+
+ /**
+ * Option to set the time zone of connection. Default to {@code LOCAL} means use JVM local time zone.
+ * It should be {@code "LOCAL"}, {@code "SERVER"}, or a valid ID of {@code ZoneId}. {@code "SERVER"} means
+ * querying the server-side timezone during initialization.
+ *
+ * @since 1.1.2
+ */
+ public static final Option CONNECTION_TIME_ZONE = Option.valueOf("connectionTimeZone");
+
+ /**
+ * Option to force the time zone of connection to session time zone. Default to {@code false}.
+ *
+ * Note: alter the time zone of session will affect the results of MySQL date/time functions, e.g.
+ * {@code NOW([n])}, {@code CURRENT_TIME([n])}, {@code CURRENT_DATE()}, etc. Please use with caution.
+ *
+ * @since 1.1.2
+ */
+ public static final Option FORCE_CONNECTION_TIME_ZONE_TO_SESSION =
+ Option.valueOf("forceConnectionTimeZoneToSession");
+
+ /**
+ * Option to set {@link ZoneId} of server. If it is set, driver will ignore the real time zone of
+ * server-side.
+ *
+ * @since 0.8.2
+ * @deprecated since 1.1.2, use {@link #CONNECTION_TIME_ZONE} instead.
+ */
+ @Deprecated
+ public static final Option SERVER_ZONE_ID = Option.valueOf("serverZoneId");
+
+ /**
+ * Option to configure handling when MySQL server returning "zero date" (aka. "0000-00-00 00:00:00")
+ *
+ * @since 0.8.1
+ */
+ public static final Option ZERO_DATE = Option.valueOf("zeroDate");
+
+ /**
+ * Option to {@link SslMode}.
+ *
+ * @since 0.8.1
+ */
+ public static final Option SSL_MODE = Option.valueOf("sslMode");
+
+ /**
+ * Option to configure {@link HostnameVerifier}. It is available only if the {@link #SSL_MODE} set to
+ * {@link SslMode#VERIFY_IDENTITY}. It can be an implementation class name of {@link HostnameVerifier}
+ * with a public no-args constructor.
+ *
+ * @since 0.8.2
+ */
+ public static final Option SSL_HOSTNAME_VERIFIER =
+ Option.valueOf("sslHostnameVerifier");
+
+ /**
+ * Option to TLS versions for SslContext protocols, see also {@code TlsVersions}. Usually sorted from
+ * higher to lower. It can be a {@code Collection}. It can be a {@link String}, protocols will be
+ * split by {@code ,}. e.g. "TLSv1.2,TLSv1.1,TLSv1".
+ *
+ * @since 0.8.1
+ */
+ public static final Option TLS_VERSION = Option.valueOf("tlsVersion");
+
+ /**
+ * Option to set a PEM file of server SSL CA. It will be used to verify server certificates. And it will
+ * be used only if {@link #SSL_MODE} set to {@link SslMode#VERIFY_CA} or higher level.
+ *
+ * @since 0.8.1
+ */
+ public static final Option SSL_CA = Option.valueOf("sslCa");
+
+ /**
+ * Option to set a PEM file of client SSL key.
+ *
+ * @since 0.8.1
+ */
+ public static final Option SSL_KEY = Option.valueOf("sslKey");
+
+ /**
+ * Option to set a PEM file password of client SSL key. It will be used only if {@link #SSL_KEY} and
+ * {@link #SSL_CERT} set.
+ *
+ * @since 0.8.1
+ */
+ public static final Option SSL_KEY_PASSWORD = Option.sensitiveValueOf("sslKeyPassword");
+
+ /**
+ * Option to set a PEM file of client SSL cert.
+ *
+ * @since 0.8.1
+ */
+ public static final Option SSL_CERT = Option.valueOf("sslCert");
+
+ /**
+ * Option to custom {@link SslContextBuilder}. It can be an implementation class name of {@link Function}
+ * with a public no-args constructor.
+ *
+ * @since 0.8.2
+ */
+ public static final Option>
+ SSL_CONTEXT_BUILDER_CUSTOMIZER = Option.valueOf("sslContextBuilderCustomizer");
+
+ /**
+ * Enable/Disable TCP KeepAlive.
+ *
+ * @since 0.8.2
+ */
+ public static final Option TCP_KEEP_ALIVE = Option.valueOf("tcpKeepAlive");
+
+ /**
+ * Enable/Disable TCP NoDelay.
+ *
+ * @since 0.8.2
+ */
+ public static final Option TCP_NO_DELAY = Option.valueOf("tcpNoDelay");
+
+ /**
+ * Enable/Disable database creation if not exist.
+ *
+ * @since 1.0.6
+ */
+ public static final Option CREATE_DATABASE_IF_NOT_EXIST =
+ Option.valueOf("createDatabaseIfNotExist");
+
+ /**
+ * Enable server preparing for parameterized statements and prefer server preparing simple statements.
+ *
+ * The value can be a {@link Boolean}. If it is {@code true}, driver will use server preparing for
+ * parameterized statements and text query for simple statements. If it is {@code false}, driver will use
+ * client preparing for parameterized statements and text query for simple statements.
+ *
+ * The value can be a {@link Predicate}{@code <}{@link String}{@code >}. If it is set, driver will server
+ * preparing for parameterized statements, it configures whether to prefer prepare execution on a
+ * statement-by-statement basis (simple statements). The {@link Predicate}{@code <}{@link String}{@code >}
+ * accepts the simple SQL query string and returns a {@code boolean} flag indicating preference.
+ *
+ * The value can be a {@link String}. If it is set, driver will try to convert it to {@link Boolean} or an
+ * instance of {@link Predicate}{@code <}{@link String}{@code >} which use reflection with a public
+ * no-args constructor.
+ *
+ * @since 0.8.1
+ */
+ public static final Option USE_SERVER_PREPARE_STATEMENT =
+ Option.valueOf("useServerPrepareStatement");
+
+ /**
+ * Option to set session variables. It should be a list of key-value pairs. e.g.
+ * {@code ["sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'", "time_zone=00:00"]}.
+ *
+ * @since 1.1.2
+ */
+ public static final Option SESSION_VARIABLES = Option.valueOf("sessionVariables");
+
+ /**
+ * Option to set the allowed local infile path.
+ *
+ * @since 1.1.0
+ */
+ public static final Option ALLOW_LOAD_LOCAL_INFILE_IN_PATH =
+ Option.valueOf("allowLoadLocalInfileInPath");
+
+ /**
+ * Option to set the buffer size for local infile. Default to {@code 8192}.
+ *
+ * @since 1.1.2
+ */
+ public static final Option LOCAL_INFILE_BUFFER_SIZE =
+ Option.valueOf("localInfileBufferSize");
+
+ /**
+ * Option to set compression algorithms. Default to [{@link CompressionAlgorithm#UNCOMPRESSED}].
+ *
+ * It will auto choose an algorithm that's contained in the list and supported by the server, preferring
+ * zstd, then zlib. If the list does not contain {@link CompressionAlgorithm#UNCOMPRESSED} and the server
+ * does not support any algorithm in the list, an exception will be thrown when connecting.
+ *
+ * Note: zstd requires a dependency {@code com.github.luben:zstd-jni}.
+ *
+ * @since 1.1.2
+ */
+ public static final Option COMPRESSION_ALGORITHMS =
+ Option.valueOf("compressionAlgorithms");
+
+ /**
+ * Option to set the zstd compression level. Default to {@code 3}.
+ *
+ * It is only used if zstd is chosen for the connection.
+ *
+ * Note: MySQL protocol does not allow to set the zlib compression level of the server, only zstd is
+ * configurable.
+ *
+ * @since 1.1.2
+ */
+ public static final Option ZSTD_COMPRESSION_LEVEL =
+ Option.valueOf("zstdCompressionLevel");
+
+ /**
+ * Option to set the {@link LoopResources} for the connection. Default to
+ * {@link reactor.netty.tcp.TcpResources#get() global tcp Resources}
+ *
+ * @since 1.1.2
+ */
+ public static final Option LOOP_RESOURCES = Option.valueOf("loopResources");
+
+ /**
+ * Option to set the maximum size of the {@link Query} parsing cache. Default to {@code 256}.
+ *
+ * @since 0.8.3
+ */
+ public static final Option PREPARE_CACHE_SIZE = Option.valueOf("prepareCacheSize");
+
+ /**
+ * Option to set the maximum size of the server-preparing cache. Default to {@code 0}.
+ *
+ * @since 0.8.3
+ */
+ public static final Option QUERY_CACHE_SIZE = Option.valueOf("queryCacheSize");
+
+ /**
+ * Enable/Disable auto-detect driver extensions.
+ *
+ * @since 0.8.2
+ */
+ public static final Option AUTODETECT_EXTENSIONS = Option.valueOf("autodetectExtensions");
+
+ /**
+ * Password Publisher function can be used to retrieve password before creating a connection. This can be
+ * used with Amazon RDS Aurora IAM authentication, wherein it requires token to be generated. The token is
+ * valid for 15 minutes, and this token will be used as password.
+ */
+ public static final Option> PASSWORD_PUBLISHER = Option.valueOf("passwordPublisher");
+
+ /**
+ * Option to set the {@link AddressResolverGroup} for resolving host addresses.
+ *
+ * This can be used to customize the DNS resolution mechanism, which is particularly useful in environments
+ * with specific DNS configuration needs or where a custom DNS resolver is required.
+ *
+ *
+ * @since 1.2.0
+ */
+ public static final Option> RESOLVER = Option.valueOf("resolver");
+
+ /**
+ * Option to enable metrics to be collected and registered in Micrometer's globalRegistry
+ * with {@link reactor.netty.tcp.TcpClient#metrics(boolean)}. Defaults to {@code false}.
+ *
+ * Note: It is required to add {@code io.micrometer.micrometer-core} dependency to classpath.
+ *
+ * @since 1.3.2
+ */
+ public static final Option METRICS = Option.valueOf("metrics");
+
+ /**
+ * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type.
+ * When enabled, TINYINT(1) columns will be treated as BIT. Defaults to {@code true}.
+ *
+ * Note: Only signed TINYINT(1) columns can be treated as BIT or Boolean.
+ * Ref: https://bugs.mysql.com/bug.php?id=100309
+ *
+ * @since 1.4.0
+ */
+ public static final Option TINY_INT_1_IS_BIT = Option.valueOf("tinyInt1isBit");
+
+ @Override
+ public ConnectionFactory create(ConnectionFactoryOptions options) {
+ requireNonNull(options, "connectionFactoryOptions must not be null");
+
+ return MySqlConnectionFactory.from(setup(options));
+ }
+
+ @Override
+ public boolean supports(ConnectionFactoryOptions options) {
+ requireNonNull(options, "connectionFactoryOptions must not be null");
+ return MYSQL_DRIVER.equals(options.getValue(DRIVER));
+ }
+
+ @Override
+ public String getDriver() {
+ return MYSQL_DRIVER;
+ }
+
+ /**
+ * Visible for unit tests.
+ *
+ * @param options the {@link ConnectionFactoryOptions} for setup {@link MySqlConnectionConfiguration}.
+ * @return completed {@link MySqlConnectionConfiguration}.
+ */
+ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) {
+ OptionMapper mapper = new OptionMapper(options);
+ MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder();
+
+ mapper.requires(USER).asString()
+ .to(builder::user);
+ mapper.optional(PASSWORD).asPassword()
+ .to(builder::password);
+ mapper.optional(UNIX_SOCKET).asString()
+ .to(builder::unixSocket)
+ .otherwise(() -> setupHost(builder, mapper));
+ mapper.optional(PRESERVE_INSTANTS).asBoolean()
+ .to(builder::preserveInstants);
+ mapper.optional(CONNECTION_TIME_ZONE).asString()
+ .to(builder::connectionTimeZone)
+ .otherwise(() -> mapper.optional(SERVER_ZONE_ID)
+ .as(ZoneId.class, id -> ZoneId.of(id, ZoneId.SHORT_IDS))
+ .to(builder::serverZoneId));
+ mapper.optional(FORCE_CONNECTION_TIME_ZONE_TO_SESSION).asBoolean()
+ .to(builder::forceConnectionTimeZoneToSession);
+ mapper.optional(TCP_KEEP_ALIVE).asBoolean()
+ .to(builder::tcpKeepAlive);
+ mapper.optional(TCP_NO_DELAY).asBoolean()
+ .to(builder::tcpNoDelay);
+ mapper.optional(ZERO_DATE)
+ .as(ZeroDateOption.class, id -> ZeroDateOption.valueOf(id.toUpperCase()))
+ .to(builder::zeroDateOption);
+ mapper.optional(USE_SERVER_PREPARE_STATEMENT).prepare(builder::useClientPrepareStatement,
+ builder::useServerPrepareStatement, builder::useServerPrepareStatement);
+ mapper.optional(ALLOW_LOAD_LOCAL_INFILE_IN_PATH).asString()
+ .to(builder::allowLoadLocalInfileInPath);
+ mapper.optional(LOCAL_INFILE_BUFFER_SIZE).asInt()
+ .to(builder::localInfileBufferSize);
+ mapper.optional(QUERY_CACHE_SIZE).asInt()
+ .to(builder::queryCacheSize);
+ mapper.optional(PREPARE_CACHE_SIZE).asInt()
+ .to(builder::prepareCacheSize);
+ mapper.optional(AUTODETECT_EXTENSIONS).asBoolean()
+ .to(builder::autodetectExtensions);
+ mapper.optional(CONNECT_TIMEOUT).as(Duration.class, Duration::parse)
+ .to(builder::connectTimeout);
+ mapper.optional(DATABASE).asString()
+ .to(builder::database);
+ mapper.optional(CREATE_DATABASE_IF_NOT_EXIST).asBoolean()
+ .to(builder::createDatabaseIfNotExist);
+ mapper.optional(COMPRESSION_ALGORITHMS).asArray(
+ CompressionAlgorithm[].class,
+ it -> CompressionAlgorithm.valueOf(it.toUpperCase()),
+ it -> it.split(","),
+ CompressionAlgorithm[]::new
+ ).to(builder::compressionAlgorithms);
+ mapper.optional(ZSTD_COMPRESSION_LEVEL).asInt()
+ .to(builder::zstdCompressionLevel);
+ mapper.optional(LOOP_RESOURCES).as(LoopResources.class)
+ .to(builder::loopResources);
+ mapper.optional(PASSWORD_PUBLISHER).as(Publisher.class)
+ .to(builder::passwordPublisher);
+ mapper.optional(RESOLVER).as(AddressResolverGroup.class)
+ .to(builder::resolver);
+ mapper.optional(SESSION_VARIABLES).asArray(
+ String[].class,
+ Function.identity(),
+ MySqlConnectionFactoryProvider::splitVariables,
+ String[]::new
+ ).to(builder::sessionVariables);
+ mapper.optional(LOCK_WAIT_TIMEOUT).as(Duration.class, Duration::parse)
+ .to(builder::lockWaitTimeout);
+ mapper.optional(STATEMENT_TIMEOUT).as(Duration.class, Duration::parse)
+ .to(builder::statementTimeout);
+ mapper.optional(METRICS).asBoolean()
+ .to(builder::metrics);
+ mapper.optional(TINY_INT_1_IS_BIT).asBoolean()
+ .to(builder::tinyInt1isBit);
+
+ return builder.build();
+ }
+
+ /**
+ * Set builder of {@link MySqlConnectionConfiguration} for hostname-based address with SSL
+ * configurations.
+ *
+ * @param builder the builder of {@link MySqlConnectionConfiguration}.
+ * @param mapper the {@link OptionMapper} of {@code options}.
+ */
+ private static void setupHost(MySqlConnectionConfiguration.Builder builder, OptionMapper mapper) {
+ mapper.requires(HOST).asString()
+ .to(builder::host);
+ mapper.optional(PORT).asInt()
+ .to(builder::port);
+ mapper.optional(SSL).asBoolean()
+ .to(isSsl -> builder.sslMode(isSsl ? SslMode.REQUIRED : SslMode.DISABLED));
+ mapper.optional(SSL_MODE).as(SslMode.class, id -> SslMode.valueOf(id.toUpperCase()))
+ .to(builder::sslMode);
+ mapper.optional(TLS_VERSION)
+ .asArray(String[].class, Function.identity(), it -> it.split(","), String[]::new)
+ .to(builder::tlsVersion);
+ mapper.optional(SSL_HOSTNAME_VERIFIER).as(HostnameVerifier.class)
+ .to(builder::sslHostnameVerifier);
+ mapper.optional(SSL_CERT).asString()
+ .to(builder::sslCert);
+ mapper.optional(SSL_KEY).asString()
+ .to(builder::sslKey);
+ mapper.optional(SSL_KEY_PASSWORD).asPassword()
+ .to(builder::sslKeyPassword);
+ mapper.optional(SSL_CONTEXT_BUILDER_CUSTOMIZER).as(Function.class)
+ .to(builder::sslContextBuilderCustomizer);
+ mapper.optional(SSL_CA).asString()
+ .to(builder::sslCa);
+ }
+
+ /**
+ * Splits session variables from user input. e.g. {@code sql_mode='ANSI_QUOTE,STRICT',c=d;e=f} will be
+ * split into {@code ["sql_mode='ANSI_QUOTE,STRICT'", "c=d", "e=f"]}.
+ *
+ * It supports escaping characters with backslash, quoted values with single or double quotes, and nested
+ * brackets. Priorities are: backslash in quoted > single quote = double quote > bracket, backslash
+ * will not be a valid escape character if it is not in a quoted value.
+ *
+ * Note that it does not strictly check syntax validity, so it will not throw syntax exceptions.
+ *
+ * @param sessionVariables the session variables from user input.
+ * @return the split list
+ * @throws IllegalArgumentException if {@code sessionVariables} is {@code null}.
+ */
+ private static String[] splitVariables(String sessionVariables) {
+ requireNonNull(sessionVariables, "sessionVariables must not be null");
+
+ if (sessionVariables.isEmpty()) {
+ return EMPTY_STRINGS;
+ }
+
+ // 1: bracket, 2: single quote, 3: double quote, 4: backtick
+ ArrayDeque stack = new ArrayDeque<>();
+ int index = 0;
+ int len = sessionVariables.length();
+ List variables = new ArrayList<>();
+
+ for (int i = 0; i < len; ++i) {
+ switch (sessionVariables.charAt(i)) {
+ case '\\':
+ if (i + 1 < len) {
+ if (stack.isEmpty()) {
+ break;
+ }
+
+ switch (stack.peekLast()) {
+ case 2:
+ case 3:
+ // All valid escape characters
+ switch (sessionVariables.charAt(i + 1)) {
+ case '\'':
+ case '"':
+ case '\\':
+ case 'n':
+ case 'r':
+ case 't':
+ case 'b':
+ case 'f':
+ ++i;
+ break;
+ }
+ break;
+ default:
+ // Backtick does not support escape characters
+ break;
+ }
+ }
+ break;
+ case ';':
+ case ',':
+ if (stack.isEmpty()) {
+ variables.add(sessionVariables.substring(index, i).trim());
+ index = i + 1;
+ }
+ break;
+ case '(':
+ if (stack.isEmpty()) {
+ stack.addLast(1);
+ break;
+ }
+
+ switch (stack.peekLast()) {
+ case 2:
+ case 3:
+ case 4:
+ break;
+ default:
+ stack.addLast(1);
+ break;
+ }
+ break;
+ case ')':
+ if (stack.isEmpty()) {
+ // Invalid bracket, ignore
+ break;
+ }
+
+ if (stack.peekLast() == 1) {
+ stack.pollLast();
+ }
+ break;
+ case '\'':
+ if (stack.isEmpty()) {
+ stack.addLast(2);
+ break;
+ }
+
+ switch (stack.peekLast()) {
+ case 2:
+ stack.pollLast();
+ break;
+ case 3:
+ case 4:
+ break;
+ default:
+ stack.addLast(2);
+ break;
+ }
+ break;
+ case '"':
+ if (stack.isEmpty()) {
+ stack.addLast(3);
+ break;
+ }
+
+ switch (stack.peekLast()) {
+ case 3:
+ stack.pollLast();
+ break;
+ case 2:
+ case 4:
+ break;
+ default:
+ stack.addLast(3);
+ break;
+ }
+ break;
+ case '`':
+ if (stack.isEmpty()) {
+ stack.addLast(4);
+ break;
+ }
+
+ switch (stack.peekLast()) {
+ case 4:
+ stack.pollLast();
+ break;
+ case 2:
+ case 3:
+ break;
+ default:
+ stack.addLast(4);
+ break;
+ }
+ break;
+ }
+ }
+
+ variables.add(sessionVariables.substring(index).trim());
+
+ return variables.toArray(new String[0]);
+ }
+}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java
similarity index 67%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java
index 04cc12eff..05add4758 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java
@@ -16,11 +16,12 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlRow;
+import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata;
+import io.asyncer.r2dbc.mysql.codec.CodecContext;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import io.asyncer.r2dbc.mysql.message.FieldValue;
import io.r2dbc.spi.Row;
-import io.r2dbc.spi.RowMetadata;
-import org.jetbrains.annotations.Nullable;
import java.lang.reflect.ParameterizedType;
@@ -29,11 +30,11 @@
/**
* An implementation of {@link Row} for MySQL database.
*/
-public final class MySqlRow implements Row {
+final class MySqlDataRow implements MySqlRow {
private final FieldValue[] fields;
- private final MySqlRowMetadata rowMetadata;
+ private final MySqlRowDescriptor rowMetadata;
private final Codecs codecs;
@@ -42,10 +43,13 @@ public final class MySqlRow implements Row {
*/
private final boolean binary;
- private final ConnectionContext context;
+ /**
+ * It can be retained because it is provided by the executed connection instead of the current connection.
+ */
+ private final CodecContext context;
- MySqlRow(FieldValue[] fields, MySqlRowMetadata rowMetadata, Codecs codecs, boolean binary,
- ConnectionContext context) {
+ MySqlDataRow(FieldValue[] fields, MySqlRowDescriptor rowMetadata, Codecs codecs, boolean binary,
+ CodecContext context) {
this.fields = requireNonNull(fields, "fields must not be null");
this.rowMetadata = requireNonNull(rowMetadata, "rowMetadata must not be null");
this.codecs = requireNonNull(codecs, "codecs must not be null");
@@ -69,16 +73,7 @@ public T get(String name, Class type) {
return codecs.decode(fields[info.getIndex()], info, type, binary, context);
}
- /**
- * Returns the value for a column in this row. The value can be a parameterized type.
- *
- * @param index the index of the column starting at {@code 0}.
- * @param type the parameterized type of item to return.
- * @param the type of the item being returned.
- * @return the value for a column in this row. Value can be {@code null}.
- * @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}.
- */
- @Nullable
+ @Override
public T get(int index, ParameterizedType type) {
requireNonNull(type, "type must not be null");
@@ -86,16 +81,7 @@ public T get(int index, ParameterizedType type) {
return codecs.decode(fields[index], info, type, binary, context);
}
- /**
- * Returns the value for a column in this row. The value can be a parameterized type.
- *
- * @param name the name of the column.
- * @param type the parameterized type of item to return.
- * @param the type of the item being returned.
- * @return the value for a column in this row. Value can be {@code null}.
- * @throws IllegalArgumentException if {@code name} or {@code type} is {@code null}.
- */
- @Nullable
+ @Override
public T get(String name, ParameterizedType type) {
requireNonNull(type, "type must not be null");
@@ -107,7 +93,7 @@ public T get(String name, ParameterizedType type) {
* {@inheritDoc}
*/
@Override
- public RowMetadata getMetadata() {
+ public MySqlRowMetadata getMetadata() {
return rowMetadata;
}
}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java
similarity index 93%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java
index 08df22a4c..25088dff4 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java
@@ -43,7 +43,7 @@ default boolean isNull() {
* Binary protocol encoding. See MySQL protocol documentations, if don't want to support the binary
* protocol, please receive an exception.
*
- * Note: not like the text protocol, it make a sense for state-less.
+ * Note: not like the text protocol, it makes a sense for state-less.
*
* Binary protocol maybe need to add a var-integer length before encoded content. So if makes it like
* {@code Mono publishBinary (Xxx binaryWriter)}, and if supports multiple times writing like a
@@ -75,9 +75,9 @@ default boolean isNull() {
Mono publishText(ParameterWriter writer);
/**
- * Get the {@link MySqlType} of this parameter data.
+ * Gets the {@link MySqlType} of this parameter data.
*
- * If don't want to support the binary protocol, just throw an exception please.
+ * If it does not want to support the binary protocol, just throw an exception please.
*
* @return the MySQL type.
*/
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java
similarity index 53%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java
index 37f26ac7b..a86d6a9ab 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java
@@ -16,57 +16,37 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata;
import io.asyncer.r2dbc.mysql.internal.util.InternalArrays;
import io.asyncer.r2dbc.mysql.message.server.DefinitionMetadataMessage;
-import io.r2dbc.spi.RowMetadata;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.VisibleForTesting;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
import java.util.NoSuchElementException;
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
/**
- * An implementation of {@link RowMetadata} for MySQL database text/binary results.
- *
- * @see MySqlNames column name searching rules.
+ * An implementation of {@link MySqlRowMetadata} for MySQL database text/binary results.
*/
-final class MySqlRowMetadata implements RowMetadata {
+final class MySqlRowDescriptor implements MySqlRowMetadata {
private final MySqlColumnDescriptor[] originMetadata;
- private final MySqlColumnDescriptor[] sortedMetadata;
-
- private final ColumnNameSet nameSet;
-
- private MySqlRowMetadata(MySqlColumnDescriptor[] metadata) {
- int size = metadata.length;
-
- switch (size) {
- case 0:
- throw new IllegalArgumentException("Least 1 column metadata");
- case 1:
- String name = metadata[0].getName();
-
- this.originMetadata = metadata;
- this.sortedMetadata = metadata;
- this.nameSet = ColumnNameSet.of(name);
-
- break;
- default:
- MySqlColumnDescriptor[] sortedMetadata = new MySqlColumnDescriptor[size];
- System.arraycopy(metadata, 0, sortedMetadata, 0, size);
- Arrays.sort(sortedMetadata, ColumnNameSet.NAME_COMPARATOR);
-
- String[] originNames = getNames(metadata);
- String[] sortedNames = getNames(sortedMetadata);
+ @Nullable
+ private Map indexMap;
- this.originMetadata = metadata;
- this.sortedMetadata = sortedMetadata;
- this.nameSet = ColumnNameSet.of(originNames, sortedNames);
-
- break;
- }
+ /**
+ * Visible for testing
+ */
+ @VisibleForTesting
+ MySqlRowDescriptor(MySqlColumnDescriptor[] metadata) {
+ originMetadata = metadata;
}
@Override
@@ -78,24 +58,43 @@ public MySqlColumnDescriptor getColumnMetadata(int index) {
return originMetadata[index];
}
+ private static Map createIndexMap(MySqlColumnDescriptor[] metadata) {
+ final int size = metadata.length;
+ final Map map = new HashMap<>(size);
+
+ for (int i = 0; i < size; ++i) {
+ map.putIfAbsent(metadata[i].getName().toLowerCase(Locale.ROOT), i);
+ }
+
+ return map;
+ }
+
+ private int find(final String name) {
+ Map indexMap = this.indexMap;
+ if (null == indexMap) {
+ indexMap = this.indexMap = createIndexMap(originMetadata);
+ }
+ return indexMap.getOrDefault(name.toLowerCase(Locale.ROOT), -1);
+ }
+
@Override
public MySqlColumnDescriptor getColumnMetadata(String name) {
requireNonNull(name, "name must not be null");
- int index = nameSet.findIndex(name);
+ final int index = find(name);
if (index < 0) {
throw new NoSuchElementException("Column name '" + name + "' does not exist");
}
- return sortedMetadata[index];
+ return originMetadata[index];
}
@Override
public boolean contains(String name) {
requireNonNull(name, "name must not be null");
- return nameSet.contains(name);
+ return find(name) >= 0;
}
@Override
@@ -105,15 +104,14 @@ public List getColumnMetadatas() {
@Override
public String toString() {
- return "MySqlRowMetadata{metadata=" + Arrays.toString(originMetadata) + ", sortedNames=" +
- Arrays.toString(nameSet.getSortedNames()) + '}';
+ return "MySqlRowDescriptor{metadata=" + Arrays.toString(originMetadata) + '}';
}
MySqlColumnDescriptor[] unwrap() {
return originMetadata;
}
- static MySqlRowMetadata create(DefinitionMetadataMessage[] columns) {
+ static MySqlRowDescriptor create(DefinitionMetadataMessage[] columns) {
int size = columns.length;
MySqlColumnDescriptor[] metadata = new MySqlColumnDescriptor[size];
@@ -121,17 +119,6 @@ static MySqlRowMetadata create(DefinitionMetadataMessage[] columns) {
metadata[i] = MySqlColumnDescriptor.create(i, columns[i]);
}
- return new MySqlRowMetadata(metadata);
- }
-
- private static String[] getNames(MySqlColumnDescriptor[] metadata) {
- int size = metadata.length;
- String[] names = new String[size];
-
- for (int i = 0; i < size; ++i) {
- names[i] = metadata[i].getName();
- }
-
- return names;
+ return new MySqlRowDescriptor(metadata);
}
}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java
similarity index 81%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java
index 749086572..3aafb1a3e 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java
@@ -16,6 +16,9 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
+import io.asyncer.r2dbc.mysql.api.MySqlRow;
+import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils;
import io.asyncer.r2dbc.mysql.internal.util.OperatorUtils;
@@ -49,16 +52,16 @@
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
/**
- * An implementation of {@link Result} representing the results of a query against the MySQL database.
+ * An implementation of {@link MySqlResult} representing the results of a query against the MySQL database.
*
- * A {@link Segment} provided by this implementation may be both {@link UpdateCount} and {@link RowSegment},
- * see also {@link MySqlOkSegment}.
+ * A {@link Segment} provided by this implementation may be both {@link UpdateCount} and {@link RowSegment}, see also
+ * {@link MySqlOkSegment}.
*/
-public final class MySqlResult implements Result {
+final class MySqlSegmentResult implements MySqlResult {
private final Flux segments;
- private MySqlResult(Flux segments) {
+ private MySqlSegmentResult(Flux segments) {
this.segments = segments;
}
@@ -81,7 +84,7 @@ public Flux map(BiFunction f) {
return segments.handle((segment, sink) -> {
if (segment instanceof RowSegment) {
- Row row = ((RowSegment) segment).row();
+ MySqlRow row = ((RowSegment) segment).row();
try {
sink.next(f.apply(row, row.getMetadata()));
@@ -116,10 +119,10 @@ public Flux map(Function super Readable, ? extends T> f) {
}
@Override
- public MySqlResult filter(Predicate filter) {
+ public MySqlResult filter(Predicate filter) {
requireNonNull(filter, "filter must not be null");
- return new MySqlResult(segments.filter(segment -> {
+ return new MySqlSegmentResult(segments.filter(segment -> {
if (filter.test(segment)) {
return true;
}
@@ -133,7 +136,7 @@ public MySqlResult filter(Predicate filter) {
}
@Override
- public Flux flatMap(Function> f) {
+ public Flux flatMap(Function> f) {
requireNonNull(f, "mapping function must not be null");
return segments.flatMap(segment -> {
@@ -154,15 +157,15 @@ public Flux flatMap(Function> f
});
}
- static MySqlResult toResult(boolean binary, Codecs codecs, ConnectionContext context,
- @Nullable String syntheticKeyName, Flux messages) {
+ static MySqlResult toResult(boolean binary, Client client, Codecs codecs,
+ @Nullable String syntheticKeyName, Flux messages) {
+ requireNonNull(client, "client must not be null");
requireNonNull(codecs, "codecs must not be null");
- requireNonNull(context, "context must not be null");
requireNonNull(messages, "messages must not be null");
- return new MySqlResult(OperatorUtils.discardOnCancel(messages)
+ return new MySqlSegmentResult(OperatorUtils.discardOnCancel(messages)
.doOnDiscard(ReferenceCounted.class, ReferenceCounted::release)
- .handle(new MySqlSegments(binary, codecs, context, syntheticKeyName)));
+ .handle(new MySqlSegments(binary, client, codecs, syntheticKeyName)));
}
private static final class MySqlMessage implements Message {
@@ -200,14 +203,14 @@ private static final class MySqlRowSegment extends AbstractReferenceCounted impl
private final FieldValue[] fields;
- private MySqlRowSegment(FieldValue[] fields, MySqlRowMetadata metadata, Codecs codecs, boolean binary,
+ private MySqlRowSegment(FieldValue[] fields, MySqlRowDescriptor metadata, Codecs codecs, boolean binary,
ConnectionContext context) {
- this.row = new MySqlRow(fields, metadata, codecs, binary, context);
+ this.row = new MySqlDataRow(fields, metadata, codecs, binary, context);
this.fields = fields;
}
@Override
- public Row row() {
+ public MySqlRow row() {
return row;
}
@@ -226,11 +229,14 @@ protected void deallocate() {
}
}
+ @SuppressWarnings("checkstyle:FinalClass")
private static class MySqlUpdateCount implements UpdateCount {
- protected final long rows;
+ private final long rows;
- private MySqlUpdateCount(long rows) { this.rows = rows; }
+ private MySqlUpdateCount(long rows) {
+ this.rows = rows;
+ }
@Override
public long value() {
@@ -255,7 +261,7 @@ private MySqlOkSegment(long rows, long lastInsertId, Codecs codecs, String keyNa
}
@Override
- public Row row() {
+ public MySqlRow row() {
return new InsertSyntheticRow(codecs, keyName, lastInsertId);
}
}
@@ -264,22 +270,21 @@ private static final class MySqlSegments implements BiConsumer sink) {
// Updated rows can be identified either by OK or rows in case of RETURNING
rowCount.getAndIncrement();
- MySqlRowMetadata metadata = this.rowMetadata;
+ MySqlRowDescriptor metadata = this.rowMetadata;
if (metadata == null) {
ReferenceCountUtil.safeRelease(message);
- sink.error(new IllegalStateException("No MySqlRowMetadata available"));
+ sink.error(new IllegalStateException("No metadata available"));
return;
}
@@ -305,7 +310,7 @@ public void accept(ServerMessage message, SynchronousSink sink) {
ReferenceCountUtil.safeRelease(message);
}
- sink.next(new MySqlRowSegment(fields, metadata, codecs, binary, context));
+ sink.next(new MySqlRowSegment(fields, metadata, codecs, binary, client.getContext()));
} else if (message instanceof SyntheticMetadataMessage) {
DefinitionMetadataMessage[] metadataMessages = ((SyntheticMetadataMessage) message).unwrap();
@@ -313,11 +318,11 @@ public void accept(ServerMessage message, SynchronousSink sink) {
return;
}
- this.rowMetadata = MySqlRowMetadata.create(metadataMessages);
+ this.rowMetadata = MySqlRowDescriptor.create(metadataMessages);
} else if (message instanceof OkMessage) {
OkMessage msg = (OkMessage) message;
- if (MySqlStatementSupport.supportReturning(context) && msg.isEndOfRows()) {
+ if (MySqlStatementSupport.supportReturning(client.getContext()) && msg.isEndOfRows()) {
sink.next(new MySqlUpdateCount(rowCount.getAndSet(0)));
} else {
long rows = msg.getAffectedRows();
diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java
new file mode 100644
index 000000000..8f8665a60
--- /dev/null
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2023 asyncer.io projects
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.asyncer.r2dbc.mysql;
+
+import io.asyncer.r2dbc.mysql.api.MySqlBatch;
+import io.asyncer.r2dbc.mysql.api.MySqlConnection;
+import io.asyncer.r2dbc.mysql.api.MySqlConnectionMetadata;
+import io.asyncer.r2dbc.mysql.api.MySqlStatement;
+import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition;
+import io.asyncer.r2dbc.mysql.cache.QueryCache;
+import io.asyncer.r2dbc.mysql.client.Client;
+import io.asyncer.r2dbc.mysql.codec.Codecs;
+import io.asyncer.r2dbc.mysql.internal.util.StringUtils;
+import io.asyncer.r2dbc.mysql.message.server.CompleteMessage;
+import io.asyncer.r2dbc.mysql.message.server.ErrorMessage;
+import io.asyncer.r2dbc.mysql.message.server.ServerMessage;
+import io.netty.util.ReferenceCountUtil;
+import io.netty.util.internal.logging.InternalLogger;
+import io.netty.util.internal.logging.InternalLoggerFactory;
+import io.r2dbc.spi.IsolationLevel;
+import io.r2dbc.spi.R2dbcNonTransientResourceException;
+import io.r2dbc.spi.TransactionDefinition;
+import io.r2dbc.spi.ValidationDepth;
+import org.jetbrains.annotations.Nullable;
+import org.jetbrains.annotations.TestOnly;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty;
+import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
+
+/**
+ * An implementation of {@link MySqlConnection} for connecting to the MySQL database.
+ */
+final class MySqlSimpleConnection implements MySqlConnection {
+
+ private static final InternalLogger logger = InternalLoggerFactory.getInstance(MySqlSimpleConnection.class);
+
+ private static final String PING_MARKER = "/* ping */";
+
+ private static final Function VALIDATE = message -> {
+ if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) {
+ return true;
+ }
+
+ if (message instanceof ErrorMessage) {
+ ErrorMessage msg = (ErrorMessage) message;
+ logger.debug("Remote validate failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), msg.getMessage());
+ } else {
+ ReferenceCountUtil.safeRelease(message);
+ }
+
+ return false;
+ };
+
+ private final Client client;
+
+ private final Codecs codecs;
+
+ private final MySqlConnectionMetadata metadata;
+
+ private final QueryCache queryCache;
+
+ @Nullable
+ private final Predicate prepare;
+
+ // TODO: Check it when executing
+ private final boolean batchSupported;
+
+ MySqlSimpleConnection(Client client, Codecs codecs, QueryCache queryCache, @Nullable Predicate prepare) {
+ ConnectionContext context = client.getContext();
+
+ this.client = client;
+ this.codecs = codecs;
+ this.metadata = new MySqlClientConnectionMetadata(client);
+ this.queryCache = queryCache;
+ this.prepare = prepare;
+ this.batchSupported = context.getCapability().isMultiStatementsAllowed();
+
+ if (this.batchSupported) {
+ logger.debug("Batch is supported by server");
+ } else {
+ logger.warn("The MySQL server does not support batch, fallback to executing one-by-one");
+ }
+ }
+
+ @Override
+ public Mono beginTransaction() {
+ return beginTransaction(MySqlTransactionDefinition.empty());
+ }
+
+ @Override
+ public Mono beginTransaction(TransactionDefinition definition) {
+ return Mono.defer(() -> QueryFlow.beginTransaction(client, batchSupported, definition));
+ }
+
+ @Override
+ public Mono close() {
+ Mono closer = client.close();
+
+ if (logger.isDebugEnabled()) {
+ return closer.doOnSubscribe(s -> logger.debug("Connection closing"))
+ .doOnSuccess(ignored -> logger.debug("Connection close succeed"));
+ }
+
+ return closer;
+ }
+
+ @Override
+ public Mono commitTransaction() {
+ return Mono.defer(() -> QueryFlow.doneTransaction(client, true, batchSupported));
+ }
+
+ @Override
+ public MySqlBatch createBatch() {
+ return batchSupported ? new MySqlBatchingBatch(client, codecs) : new MySqlSyntheticBatch(client, codecs);
+ }
+
+ @Override
+ public Mono createSavepoint(String name) {
+ requireNonEmpty(name, "Savepoint name must not be empty");
+
+ return QueryFlow.createSavepoint(client, name, batchSupported);
+ }
+
+ @Override
+ public MySqlStatement createStatement(String sql) {
+ requireNonNull(sql, "sql must not be null");
+
+ if (sql.startsWith(PING_MARKER)) {
+ return new PingStatement(client, codecs);
+ }
+
+ Query query = queryCache.get(sql);
+
+ if (query.isSimple()) {
+ if (prepare != null && prepare.test(sql)) {
+ logger.debug("Create a simple statement provided by prepare query");
+ return new PrepareSimpleStatement(client, codecs, sql);
+ }
+
+ logger.debug("Create a simple statement provided by text query");
+
+ return new TextSimpleStatement(client, codecs, sql);
+ }
+
+ if (prepare == null) {
+ logger.debug("Create a parameterized statement provided by text query");
+ return new TextParameterizedStatement(client, codecs, query);
+ }
+
+ logger.debug("Create a parameterized statement provided by prepare query");
+
+ return new PrepareParameterizedStatement(client, codecs, query);
+ }
+
+ @Override
+ public Mono postAllocate() {
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono preRelease() {
+ // Rollback if the connection is in transaction.
+ return rollbackTransaction();
+ }
+
+ @Override
+ public Mono releaseSavepoint(String name) {
+ requireNonEmpty(name, "Savepoint name must not be empty");
+
+ return QueryFlow.executeVoid(client, "RELEASE SAVEPOINT " + StringUtils.quoteIdentifier(name));
+ }
+
+ @Override
+ public Mono rollbackTransaction() {
+ return Mono.defer(() -> QueryFlow.doneTransaction(client, false, batchSupported));
+ }
+
+ @Override
+ public Mono rollbackTransactionToSavepoint(String name) {
+ requireNonEmpty(name, "Savepoint name must not be empty");
+
+ return QueryFlow.executeVoid(client, "ROLLBACK TO SAVEPOINT " + StringUtils.quoteIdentifier(name));
+ }
+
+ @Override
+ public MySqlConnectionMetadata getMetadata() {
+ return metadata;
+ }
+
+ /**
+ * MySQL does not have a way to query the isolation level of the current transaction, only inferred from past
+ * statements, so driver can not make sure the result is right.
+ *
+ * See MySQL Bug 53341
+ *
+ * {@inheritDoc}
+ */
+ @Override
+ public IsolationLevel getTransactionIsolationLevel() {
+ return client.getContext().getCurrentIsolationLevel();
+ }
+
+ @Override
+ public Mono setTransactionIsolationLevel(IsolationLevel isolationLevel) {
+ requireNonNull(isolationLevel, "isolationLevel must not be null");
+
+ // Set subsequent transaction isolation level.
+ return QueryFlow.executeVoid(client,
+ "SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql())
+ .doOnSuccess(ignored -> {
+ ConnectionContext context = client.getContext();
+
+ context.setSessionIsolationLevel(isolationLevel);
+ if (!context.isInTransaction()) {
+ context.setCurrentIsolationLevel(isolationLevel);
+ }
+ });
+ }
+
+ @Override
+ public Mono validate(ValidationDepth depth) {
+ requireNonNull(depth, "depth must not be null");
+
+ if (depth == ValidationDepth.LOCAL) {
+ return Mono.fromSupplier(client::isConnected);
+ }
+
+ return Mono.defer(() -> {
+ if (!client.isConnected()) {
+ return Mono.just(false);
+ }
+
+ return QueryFlow.ping(client)
+ .map(VALIDATE)
+ .last()
+ .onErrorResume(e -> {
+ // `last` maybe emit a NoSuchElementException, exchange maybe emit exception by Netty.
+ // But should NEVER emit any exception, so logging exception and emit false.
+ logger.debug("Remote validate failed", e);
+ return Mono.just(false);
+ });
+ });
+ }
+
+ @Override
+ public boolean isAutoCommit() {
+ return client.getContext().isAutoCommit();
+ }
+
+ @Override
+ public Mono setAutoCommit(boolean autoCommit) {
+ return Mono.defer(() -> QueryFlow.executeVoid(client, "SET autocommit=" + (autoCommit ? 1 : 0)));
+ }
+
+ @Override
+ public Mono setLockWaitTimeout(Duration timeout) {
+ requireNonNull(timeout, "timeout must not be null");
+
+ if (client.getContext().isLockWaitTimeoutSupported()) {
+ return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(timeout))
+ .doOnSuccess(ignored -> client.getContext().setAllLockWaitTimeout(timeout));
+ }
+
+ logger.warn("Lock wait timeout is not supported by server, setLockWaitTimeout operation is ignored");
+ return Mono.empty();
+
+ }
+
+ @Override
+ public Mono setStatementTimeout(Duration timeout) {
+ requireNonNull(timeout, "timeout must not be null");
+
+ ConnectionContext context = client.getContext();
+
+ // mariadb: https://mariadb.com/kb/en/aborting-statements/
+ // mysql: https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/
+ // ref: https://github.com/mariadb-corporation/mariadb-connector-r2dbc
+ if (context.isStatementTimeoutSupported()) {
+ String variable = StringUtils.statementTimeoutVariable(timeout, context.isMariaDb());
+ return QueryFlow.setSessionVariable(client, variable);
+ }
+
+ return Mono.error(
+ new R2dbcNonTransientResourceException(
+ "Statement timeout is not supported by server version " + context.getServerVersion(),
+ "HY000",
+ -1
+ )
+ );
+ }
+
+ /**
+ * Visible only for testing.
+ *
+ * @return current connection context
+ */
+ @TestOnly
+ ConnectionContext context() {
+ return client.getContext();
+ }
+}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java
similarity index 99%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java
index 2a4b1c0fa..d76662f40 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java
@@ -29,7 +29,7 @@
import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_STRINGS;
/**
- * MySQL configuration of SSL.
+ * A configuration of MySQL SSL connection.
*/
public final class MySqlSslConfiguration {
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java
similarity index 87%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java
index 696626ba0..5b40500ee 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java
@@ -16,6 +16,8 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlStatement;
+import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.internal.util.InternalArrays;
import org.jetbrains.annotations.Nullable;
@@ -31,19 +33,20 @@ abstract class MySqlStatementSupport implements MySqlStatement {
private static final String LAST_INSERT_ID = "LAST_INSERT_ID";
- protected final ConnectionContext context;
+ protected final Client client;
@Nullable
private String[] generatedColumns = null;
- MySqlStatementSupport(ConnectionContext context) {
- this.context = requireNonNull(context, "context must not be null");
+ MySqlStatementSupport(Client client) {
+ this.client = requireNonNull(client, "client must not be null");
}
@Override
public final MySqlStatement returnGeneratedValues(String... columns) {
requireNonNull(columns, "columns must not be null");
+ ConnectionContext context = client.getContext();
int len = columns.length;
if (len == 0) {
@@ -70,7 +73,7 @@ final String syntheticKeyName() {
String[] columns = this.generatedColumns;
// MariaDB should use `RETURNING` clause instead.
- if (columns == null || supportReturning(this.context)) {
+ if (columns == null || supportReturning(this.client.getContext())) {
return null;
}
@@ -84,7 +87,7 @@ final String syntheticKeyName() {
final String returningIdentifiers() {
String[] columns = this.generatedColumns;
- if (columns == null || !supportReturning(context)) {
+ if (columns == null || !supportReturning(this.client.getContext())) {
return "";
}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java
similarity index 82%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java
index 87325591e..73640a82e 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java
@@ -16,6 +16,8 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlBatch;
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import reactor.core.publisher.Flux;
@@ -29,20 +31,17 @@
* An implementation of {@link MySqlBatch} for executing a collection of statements in one-by-one against the
* MySQL database.
*/
-final class MySqlSyntheticBatch extends MySqlBatch {
+final class MySqlSyntheticBatch implements MySqlBatch {
private final Client client;
private final Codecs codecs;
- private final ConnectionContext context;
-
private final List statements = new ArrayList<>();
- MySqlSyntheticBatch(Client client, Codecs codecs, ConnectionContext context) {
+ MySqlSyntheticBatch(Client client, Codecs codecs) {
this.client = requireNonNull(client, "client must not be null");
this.codecs = requireNonNull(codecs, "codecs must not be null");
- this.context = requireNonNull(context, "context must not be null");
}
@Override
@@ -54,7 +53,7 @@ public MySqlBatch add(String sql) {
@Override
public Flux execute() {
return QueryFlow.execute(client, statements)
- .map(messages -> MySqlResult.toResult(false, codecs, context, null, messages));
+ .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, null, messages));
}
@Override
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java
similarity index 81%
rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java
index fab2bcc15..f5c9af4ed 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java
@@ -33,42 +33,44 @@
* and 1073741824.
*
* @since 0.9.0
+ * @deprecated since 1.1.3, use {@link io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition} instead.
*/
+@Deprecated
public final class MySqlTransactionDefinition implements TransactionDefinition {
/**
- * Use {@code WITH CONSISTENT SNAPSHOT} syntax, all MySQL-compatible servers should support this syntax.
+ * Use {@code WITH CONSISTENT SNAPSHOT} property.
+ *
* The option starts a consistent read for storage engines such as InnoDB and XtraDB that can do so, the
* same as if a {@code START TRANSACTION} followed by a {@code SELECT ...} from any InnoDB table was
* issued.
- *
- * NOTICE: This option and {@link #READ_ONLY} cannot be enabled at the same definition.
*/
- public static final Option WITH_CONSISTENT_SNAPSHOT = Option.valueOf("withConsistentSnapshot");
+ public static final Option WITH_CONSISTENT_SNAPSHOT =
+ io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT;
/**
- * Use {@code START TRANSACTION WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar syntax.
- * Only available when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}.
+ * Use {@code WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar property. Only available
+ * when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}.
*
- * NOTICE: This is an extended syntax for special servers. Before using it, check whether the server
- * supports the syntax.
+ * Note: This is an extended syntax based on specific distributions. Please check whether the server
+ * supports this property before using it.
*/
- public static final Option CONSISTENT_SNAPSHOT_ENGINE =
- Option.valueOf("consistentSnapshotEngine");
+ public static final Option> CONSISTENT_SNAPSHOT_ENGINE =
+ io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE;
/**
- * Use {@code START TRANSACTION WITH CONSISTENT SNAPSHOT FROM SESSION [session_id]} for Percona/MySQL or
- * similar syntax. Only available when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}.
+ * Use {@code WITH CONSISTENT SNAPSHOT FROM SESSION [session_id]} for Percona/MySQL or similar property.
+ * Only available when {@link #WITH_CONSISTENT_SNAPSHOT} is set to {@code true}.
*
- * The {@code session_id} is the session identifier reported in the {@code Id} column of the process list.
- * Reported by {@code SHOW COLUMNS FROM performance_schema.processlist}, it should be an unsigned 64-bit
- * integer. Use {@code SHOW PROCESSLIST} to find session identifier of the process list.
+ * The {@code session_id} is received by {@code SHOW COLUMNS FROM performance_schema.processlist}, it
+ * should be an unsigned 64-bit integer. Use {@code SHOW PROCESSLIST} to find session identifier of the
+ * process list.
*
- * NOTICE: This is an extended syntax for special servers. Before using it, check whether the server
- * supports the syntax.
+ * Note: This is an extended syntax based on specific distributions. Please check whether the server
+ * supports this property before using it.
*/
public static final Option CONSISTENT_SNAPSHOT_FROM_SESSION =
- Option.valueOf("consistentSnapshotFromSession");
+ io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_FROM_SESSION;
private static final MySqlTransactionDefinition EMPTY =
new MySqlTransactionDefinition(Collections.emptyMap());
@@ -187,7 +189,7 @@ public Builder withConsistentSnapshot(@Nullable Boolean withConsistentSnapshot)
* @return this builder.
*/
public Builder consistentSnapshotEngine(@Nullable ConsistentSnapshotEngine snapshotEngine) {
- return option(CONSISTENT_SNAPSHOT_ENGINE, snapshotEngine);
+ return option(CONSISTENT_SNAPSHOT_ENGINE, snapshotEngine == null ? null : snapshotEngine.asSql());
}
/**
@@ -200,7 +202,7 @@ public Builder consistentSnapshotFromSession(@Nullable Long sessionId) {
return option(CONSISTENT_SNAPSHOT_FROM_SESSION, sessionId);
}
- private Builder option(Option key, @Nullable T value) {
+ private Builder option(Option> key, @Nullable Object value) {
if (value == null) {
this.options.remove(key);
} else {
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java
similarity index 52%
rename from src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java
index 2ab077c0c..9217367fa 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java
@@ -16,12 +16,13 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlNativeTypeMetadata;
import io.asyncer.r2dbc.mysql.collation.CharCollation;
/**
- * A flag bitmap considers column definitions.
+ * An implementation of {@link MySqlNativeTypeMetadata}.
*/
-public final class ColumnDefinition {
+final class MySqlTypeMetadata implements MySqlNativeTypeMetadata {
private static final short NOT_NULL = 1;
@@ -48,75 +49,61 @@ public final class ColumnDefinition {
private static final short ALL_USED = NOT_NULL | UNSIGNED | BINARY | ENUM | SET;
+ private final int typeId;
+
/**
- * The original bitmap of {@link ColumnDefinition this}.
+ * The original bitmap of definitions.
*
* MySQL uses 32-bits definition flags, but only returns the lower 16-bits.
*/
- private final short bitmap;
+ private final short definitions;
/**
- * collation id(or charset number)
+ * The character collation id of the column.
*
* collationId > 0 when protocol version == 4.1, 0 otherwise.
*/
private final int collationId;
- private ColumnDefinition(short bitmap, int collationId) {
- this.bitmap = bitmap;
+ MySqlTypeMetadata(int typeId, int definitions, int collationId) {
+ this.typeId = typeId;
+ this.definitions = (short) (definitions & ALL_USED);
this.collationId = collationId;
}
- /**
- * Checks if value is not null.
- *
- * @return if value is not null.
- */
+ @Override
+ public int getTypeId() {
+ return typeId;
+ }
+
+ @Override
public boolean isNotNull() {
- return (bitmap & NOT_NULL) != 0;
+ return (definitions & NOT_NULL) != 0;
}
- /**
- * Checks if value is an unsigned number. e.g. INT UNSIGNED, BIGINT UNSIGNED.
- *
- * Note: IEEE-754 floating types (e.g. DOUBLE/FLOAT) do not supports it in MySQL 8.0+. When creating a
- * column as an unsigned floating type, the server may report a warning.
- *
- * @return if value is an unsigned number.
- */
+ @Override
public boolean isUnsigned() {
- return (bitmap & UNSIGNED) != 0;
+ return (definitions & UNSIGNED) != 0;
}
- /**
- * Checks if value is binary data.
- *
- * @return if value is binary data.
- */
+ @Override
public boolean isBinary() {
// Utilize collationId to ascertain whether it is binary or not.
// This is necessary since the union of JSON columns, varchar binary, and char binary
// results in a bitmap with the BINARY flag set.
// see: https://github.com/asyncer-io/r2dbc-mysql/issues/91
- return collationId == 0 & (bitmap & BINARY) != 0 | collationId == CharCollation.BINARY_ID;
+ // FIXME: use collationId to check, definitions is not reliable even in protocol version < 4.1
+ return (collationId == 0 && (definitions & BINARY) != 0) || collationId == CharCollation.BINARY_ID;
}
- /**
- * Checks if value type is enum.
- *
- * @return if value is an enum.
- */
+ @Override
public boolean isEnum() {
- return (bitmap & ENUM) != 0;
+ return (definitions & ENUM) != 0;
}
- /**
- * Checks if value type is set.
- *
- * @return if value is a set.
- */
+ @Override
public boolean isSet() {
- return (bitmap & SET) != 0;
+ return (definitions & SET) != 0;
}
@Override
@@ -124,45 +111,26 @@ public boolean equals(Object o) {
if (this == o) {
return true;
}
- if (!(o instanceof ColumnDefinition)) {
+ if (!(o instanceof MySqlTypeMetadata)) {
return false;
}
- ColumnDefinition that = (ColumnDefinition) o;
+ MySqlTypeMetadata that = (MySqlTypeMetadata) o;
- return bitmap == that.bitmap & collationId == that.collationId;
+ return typeId == that.typeId && definitions == that.definitions && collationId == that.collationId;
}
@Override
public int hashCode() {
- return bitmap;
+ int result = 31 * typeId + (int) definitions;
+ return 31 * result + collationId;
}
@Override
public String toString() {
- return "ColumnDefinition<0x" + Integer.toHexString(bitmap) + ", 0x" + Integer.toHexString(collationId)+ '>';
- }
-
- /**
- * Creates a {@link ColumnDefinition} with column definitions bitmap. It will unset all unknown or useless
- * flags.
- *
- * @param definitions the column definitions bitmap.
- * @return the {@link ColumnDefinition} without unknown or useless flags.
- */
- public static ColumnDefinition of(int definitions) {
- return new ColumnDefinition((short) (definitions & ALL_USED), 0);
- }
-
- /**
- * Creates a {@link ColumnDefinition} with column definitions bitmap. It will unset all unknown or useless
- * flags.
- *
- * @param definitions the column definitions bitmap.
- * @param collationId the collation id.
- * @return the {@link ColumnDefinition} without unknown or useless flags.
- */
- public static ColumnDefinition of(int definitions, int collationId) {
- return new ColumnDefinition((short) (definitions & ALL_USED), collationId);
+ return "MySqlTypeMetadata{typeId=" + typeId +
+ ", definitions=0x" + Integer.toHexString(definitions) +
+ ", collationId=" + collationId +
+ '}';
}
}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java
similarity index 81%
rename from src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java
index 218cc11df..f75a913f1 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java
@@ -23,6 +23,7 @@
import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.IntFunction;
import java.util.function.Predicate;
/**
@@ -55,7 +56,9 @@ final class Source {
@Nullable
private final T value;
- private Source(@Nullable T value) { this.value = value; }
+ private Source(@Nullable T value) {
+ this.value = value;
+ }
Otherwise to(Consumer super T> consumer) {
if (value == null) {
@@ -105,21 +108,39 @@ Source as(Class type, Function mapping) {
throw new IllegalArgumentException(toMessage(value, type.getTypeName()));
}
- Source asStrings() {
+ Source asArray(Class arrayType, Function mapper,
+ Function splitter, IntFunction generator) {
if (value == null) {
return nilSource();
}
- if (value instanceof String[]) {
- return new Source<>((String[]) value);
+ if (arrayType.isInstance(value)) {
+ return new Source<>(arrayType.cast(value));
+ } else if (value instanceof String[]) {
+ return new Source<>(mapArray((String[]) value, mapper, generator));
} else if (value instanceof String) {
- return new Source<>(((String) value).split(","));
+ String[] strings = splitter.apply((String) value);
+
+ if (arrayType.isInstance(strings)) {
+ return new Source<>(arrayType.cast(strings));
+ }
+
+ return new Source<>(mapArray(strings, mapper, generator));
} else if (value instanceof Collection>) {
- return new Source<>(((Collection>) value).stream()
- .map(String.class::cast).toArray(String[]::new));
+ @SuppressWarnings("unchecked")
+ Class type = (Class) arrayType.getComponentType();
+ R[] array = ((Collection>) value).stream().map(e -> {
+ if (type.isInstance(e)) {
+ return type.cast(e);
+ } else {
+ return mapper.apply(e.toString());
+ }
+ }).toArray(generator);
+
+ return new Source<>(array);
}
- throw new IllegalArgumentException(toMessage(value, "String[]"));
+ throw new IllegalArgumentException(toMessage(value, arrayType.getTypeName()));
}
Source asBoolean() {
@@ -236,6 +257,16 @@ private static Source nilSource() {
private static String toMessage(Object value, String type) {
return "Cannot convert value " + value + " to " + type;
}
+
+ private static O[] mapArray(String[] input, Function mapper, IntFunction generator) {
+ O[] output = generator.apply(input.length);
+
+ for (int i = 0; i < input.length; i++) {
+ output[i] = mapper.apply(input[i]);
+ }
+
+ return output;
+ }
}
enum Otherwise {
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java
similarity index 100%
rename from src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java
similarity index 95%
rename from src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java
index 62ac43e29..590762bc4 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java
@@ -25,7 +25,7 @@
import java.nio.ByteBuffer;
/**
- * A writer for {@link MySqlParameter}s of parametrized statements with text-based protocol.
+ * A writer for {@link MySqlParameter}s of parameterized statements with text-based protocol.
*/
public abstract class ParameterWriter extends Writer {
@@ -38,7 +38,7 @@ public abstract class ParameterWriter extends Writer {
/**
* Writes a value of {@code int} to current parameter. If current mode is string mode, it will write as a
- * string like {@code write(String.valueOf(value))}. If it write as a numeric, nothing else can be written
+ * string like {@code write(String.valueOf(value))}. If write as a numeric, nothing else can be written
* before or after this.
*
* @param value the value of {@code int}.
@@ -68,7 +68,7 @@ public abstract class ParameterWriter extends Writer {
/**
* Writes a value of {@link BigInteger} to current parameter. If current mode is string mode, it will
- * write as a string like {@code write(value.toString())}. If it write as a numeric, nothing else can be
+ * write as a string like {@code write(value.toString())}. If write as a numeric, nothing else can be
* written before or after this.
*
* @param value the value of {@link BigInteger}.
@@ -79,7 +79,7 @@ public abstract class ParameterWriter extends Writer {
/**
* Writes a value of {@code float} to current parameter. If current mode is string mode, it will write as
- * a string like {@code write(String.valueOf(value))}. If it write as a numeric, nothing else can be
+ * a string like {@code write(String.valueOf(value))}. If write as a numeric, nothing else can be
* written before or after this.
*
* @param value the value of {@code float}.
@@ -89,7 +89,7 @@ public abstract class ParameterWriter extends Writer {
/**
* Writes a value of {@code double} to current parameter. If current mode is string mode, it will write as
- * a string like {@code write(String.valueOf(value))}. If it write as a numeric, nothing else can be
+ * a string like {@code write(String.valueOf(value))}. If write as a numeric, nothing else can be
* written before or after this.
*
* @param value the value of {@code double}.
@@ -99,7 +99,7 @@ public abstract class ParameterWriter extends Writer {
/**
* Writes a value of {@link BigDecimal} to current parameter. If current mode is string mode, it will
- * write as a string like {@code write(value.toString())}. If it write as a numeric, nothing else can be
+ * write as a string like {@code write(value.toString())}. If write as a numeric, nothing else can be
* written before or after this.
*
* @param value the value of {@link BigDecimal}.
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java
similarity index 88%
rename from src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java
index ffe203077..2b1d64ed5 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java
@@ -16,6 +16,8 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
+import io.asyncer.r2dbc.mysql.api.MySqlStatement;
import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import reactor.core.publisher.Flux;
@@ -32,14 +34,12 @@
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull;
/**
- * Base class considers parametrized {@link MySqlStatement} with parameter markers.
+ * Base class considers parameterized {@link MySqlStatement} with parameter markers.
*
- * MySQL uses indexed parameters which are marked by {@literal ?} without naming. Implementations should uses
+ * MySQL uses indexed parameters which are marked by {@literal ?} without naming. Implementations should use
* {@link Query} to supports named parameters.
*/
-abstract class ParametrizedStatementSupport extends MySqlStatementSupport {
-
- protected final Client client;
+abstract class ParameterizedStatementSupport extends MySqlStatementSupport {
protected final Codecs codecs;
@@ -49,13 +49,12 @@ abstract class ParametrizedStatementSupport extends MySqlStatementSupport {
private final AtomicBoolean executed = new AtomicBoolean();
- ParametrizedStatementSupport(Client client, Codecs codecs, Query query, ConnectionContext context) {
- super(context);
+ ParameterizedStatementSupport(Client client, Codecs codecs, Query query) {
+ super(client);
requireNonNull(query, "query must not be null");
require(query.getParameters() > 0, "parameters must be a positive integer");
- this.client = requireNonNull(client, "client must not be null");
this.codecs = requireNonNull(codecs, "codecs must not be null");
this.query = query;
this.bindings = new Bindings(query.getParameters());
@@ -73,7 +72,7 @@ public final MySqlStatement add() {
public final MySqlStatement bind(int index, Object value) {
requireNonNull(value, "value must not be null");
- addBinding(index, codecs.encode(value, context));
+ addBinding(index, codecs.encode(value, client.getContext()));
return this;
}
@@ -82,7 +81,7 @@ public final MySqlStatement bind(String name, Object value) {
requireNonNull(name, "name must not be null");
requireNonNull(value, "value must not be null");
- addBinding(getIndexes(name), codecs.encode(value, context));
+ addBinding(getIndexes(name), codecs.encode(value, client.getContext()));
return this;
}
@@ -106,7 +105,7 @@ public final MySqlStatement bindNull(String name, Class> type) {
}
@Override
- public final Flux execute() {
+ public final Flux extends MySqlResult> execute() {
if (bindings.bindings.isEmpty()) {
throw new IllegalStateException("No parameters bound for current statement");
}
@@ -114,14 +113,14 @@ public final Flux execute() {
return Flux.defer(() -> {
if (!executed.compareAndSet(false, true)) {
- return Flux.error(new IllegalStateException("Parametrized statement was already executed"));
+ return Flux.error(new IllegalStateException("Parameterized statement was already executed"));
}
return execute(bindings.bindings);
});
}
- abstract protected Flux execute(List bindings);
+ protected abstract Flux extends MySqlResult> execute(List bindings);
/**
* Get parameter index(es) by parameter name.
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java
similarity index 78%
rename from src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java
index d11717a34..9cdc43c95 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java
@@ -16,24 +16,25 @@
package io.asyncer.r2dbc.mysql;
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
+import io.asyncer.r2dbc.mysql.api.MySqlStatement;
+import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
/**
* An implementation of {@link MySqlStatement} considers the lightweight ping syntax.
*/
final class PingStatement implements MySqlStatement {
- private final MySqlConnection connection;
+ private final Client client;
private final Codecs codecs;
- private final ConnectionContext context;
-
- PingStatement(MySqlConnection connection, Codecs codecs, ConnectionContext context) {
- this.connection = connection;
+ PingStatement(Client client, Codecs codecs) {
+ this.client = client;
this.codecs = codecs;
- this.context = context;
}
@Override
@@ -63,7 +64,12 @@ public MySqlStatement bindNull(String name, Class> type) {
@Override
public Flux execute() {
- return Flux.just(MySqlResult.toResult(false, codecs, context, null,
- connection.doPingInternal()));
+ return Flux.from(Mono.fromSupplier(() -> MySqlSegmentResult.toResult(
+ false,
+ client,
+ codecs,
+ null,
+ QueryFlow.ping(client)
+ )));
}
}
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java
similarity index 68%
rename from src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java
index 3a946f3ea..44edd9509 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java
@@ -16,7 +16,8 @@
package io.asyncer.r2dbc.mysql;
-import io.asyncer.r2dbc.mysql.cache.PrepareCache;
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
+import io.asyncer.r2dbc.mysql.api.MySqlStatement;
import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import io.asyncer.r2dbc.mysql.internal.util.StringUtils;
@@ -27,27 +28,23 @@
import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require;
/**
- * An implementation of {@link ParametrizedStatementSupport} based on MySQL prepare query.
+ * An implementation of {@link ParameterizedStatementSupport} based on MySQL prepare query.
*/
-final class PrepareParametrizedStatement extends ParametrizedStatementSupport {
-
- private final PrepareCache prepareCache;
+final class PrepareParameterizedStatement extends ParameterizedStatementSupport {
private int fetchSize = 0;
- PrepareParametrizedStatement(Client client, Codecs codecs, Query query, ConnectionContext context,
- PrepareCache prepareCache) {
- super(client, codecs, query, context);
- this.prepareCache = prepareCache;
+ PrepareParameterizedStatement(Client client, Codecs codecs, Query query) {
+ super(client, codecs, query);
}
@Override
public Flux execute(List bindings) {
return Flux.defer(() -> QueryFlow.execute(client,
StringUtils.extendReturning(query.getFormattedSql(), returningIdentifiers()),
- bindings, fetchSize, prepareCache
+ bindings, fetchSize
))
- .map(messages -> MySqlResult.toResult(true, codecs, context, syntheticKeyName(), messages));
+ .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages));
}
@Override
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java
similarity index 77%
rename from src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java
index 2284b991e..29a2b8232 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java
@@ -16,7 +16,8 @@
package io.asyncer.r2dbc.mysql;
-import io.asyncer.r2dbc.mysql.cache.PrepareCache;
+import io.asyncer.r2dbc.mysql.api.MySqlResult;
+import io.asyncer.r2dbc.mysql.api.MySqlStatement;
import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.codec.Codecs;
import io.asyncer.r2dbc.mysql.internal.util.StringUtils;
@@ -34,21 +35,17 @@ final class PrepareSimpleStatement extends SimpleStatementSupport {
private static final List BINDINGS = Collections.singletonList(new Binding(0));
- private final PrepareCache prepareCache;
-
private int fetchSize = 0;
- PrepareSimpleStatement(Client client, Codecs codecs, ConnectionContext context, String sql,
- PrepareCache prepareCache) {
- super(client, codecs, context, sql);
- this.prepareCache = prepareCache;
+ PrepareSimpleStatement(Client client, Codecs codecs, String sql) {
+ super(client, codecs, sql);
}
@Override
public Flux execute() {
return Flux.defer(() -> QueryFlow.execute(client,
- StringUtils.extendReturning(sql, returningIdentifiers()), BINDINGS, fetchSize, prepareCache))
- .map(messages -> MySqlResult.toResult(true, codecs, context, syntheticKeyName(), messages));
+ StringUtils.extendReturning(sql, returningIdentifiers()), BINDINGS, fetchSize))
+ .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages));
}
@Override
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Query.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Query.java
similarity index 99%
rename from src/main/java/io/asyncer/r2dbc/mysql/Query.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Query.java
index 1fe94ee54..49c063e61 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/Query.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Query.java
@@ -167,7 +167,7 @@ public static Query parse(String sql) {
}
Map nameKeyedParams = new HashMap<>();
- // Used by singleton map, if SQL does not contains named-parameter, it will always be empty.
+ // Used by singleton map, if SQL does not contain named-parameter, it will always be empty.
String anyName = "";
// The last parameter end index (whatever named or not) of sql.
int lastParamEnd = 0;
diff --git a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java
similarity index 69%
rename from src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java
rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java
index 26721b911..3bae1dcc3 100644
--- a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java
+++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java
@@ -16,18 +16,14 @@
package io.asyncer.r2dbc.mysql;
-import io.asyncer.r2dbc.mysql.authentication.MySqlAuthProvider;
-import io.asyncer.r2dbc.mysql.cache.PrepareCache;
+import io.asyncer.r2dbc.mysql.api.MySqlBatch;
+import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition;
import io.asyncer.r2dbc.mysql.client.Client;
import io.asyncer.r2dbc.mysql.client.FluxExchangeable;
import io.asyncer.r2dbc.mysql.constant.ServerStatuses;
-import io.asyncer.r2dbc.mysql.constant.SslMode;
import io.asyncer.r2dbc.mysql.internal.util.StringUtils;
-import io.asyncer.r2dbc.mysql.message.client.AuthResponse;
import io.asyncer.r2dbc.mysql.message.client.ClientMessage;
-import io.asyncer.r2dbc.mysql.message.client.HandshakeResponse;
import io.asyncer.r2dbc.mysql.message.client.LocalInfileResponse;
-import io.asyncer.r2dbc.mysql.message.client.SubsequenceClientMessage;
import io.asyncer.r2dbc.mysql.message.client.PingMessage;
import io.asyncer.r2dbc.mysql.message.client.PrepareQueryMessage;
import io.asyncer.r2dbc.mysql.message.client.PreparedCloseMessage;
@@ -35,28 +31,21 @@
import io.asyncer.r2dbc.mysql.message.client.PreparedFetchMessage;
import io.asyncer.r2dbc.mysql.message.client.PreparedResetMessage;
import io.asyncer.r2dbc.mysql.message.client.PreparedTextQueryMessage;
-import io.asyncer.r2dbc.mysql.message.client.SslRequest;
import io.asyncer.r2dbc.mysql.message.client.TextQueryMessage;
-import io.asyncer.r2dbc.mysql.message.server.AuthMoreDataMessage;
-import io.asyncer.r2dbc.mysql.message.server.ChangeAuthMessage;
import io.asyncer.r2dbc.mysql.message.server.CompleteMessage;
import io.asyncer.r2dbc.mysql.message.server.EofMessage;
import io.asyncer.r2dbc.mysql.message.server.ErrorMessage;
-import io.asyncer.r2dbc.mysql.message.server.HandshakeHeader;
-import io.asyncer.r2dbc.mysql.message.server.HandshakeRequest;
import io.asyncer.r2dbc.mysql.message.server.LocalInfileRequest;
import io.asyncer.r2dbc.mysql.message.server.OkMessage;
import io.asyncer.r2dbc.mysql.message.server.PreparedOkMessage;
import io.asyncer.r2dbc.mysql.message.server.ServerMessage;
import io.asyncer.r2dbc.mysql.message.server.ServerStatusMessage;
import io.asyncer.r2dbc.mysql.message.server.SyntheticMetadataMessage;
-import io.asyncer.r2dbc.mysql.message.server.SyntheticSslResponseMessage;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.ReferenceCounted;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import io.r2dbc.spi.IsolationLevel;
-import io.r2dbc.spi.R2dbcPermissionDeniedException;
import io.r2dbc.spi.TransactionDefinition;
import org.jetbrains.annotations.Nullable;
import reactor.core.CoreSubscriber;
@@ -70,18 +59,17 @@
import java.time.Duration;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.Iterator;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
/**
- * A message flow considers both of parametrized and text queries, such as {@link TextParametrizedStatement},
- * {@link PrepareParametrizedStatement}, {@link TextSimpleStatement}, {@link PrepareSimpleStatement} and
+ * A message flow considers both of parameterized and text queries, such as {@link TextParameterizedStatement},
+ * {@link PrepareParameterizedStatement}, {@link TextSimpleStatement}, {@link PrepareSimpleStatement} and
* {@link MySqlBatch}.
*/
final class QueryFlow {
@@ -99,37 +87,47 @@ final class QueryFlow {
}
};
+ private static final BiConsumer> PING = (message, sink) -> {
+ if (message instanceof ErrorMessage) {
+ sink.next(message);
+ sink.complete();
+ } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) {
+ sink.next(message);
+ sink.complete();
+ } else {
+ ReferenceCountUtil.safeRelease(message);
+ }
+ };
+
/**
- * Execute multiple bindings of a server-preparing statement with one-by-one binary execution. The
- * execution terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. If client
- * receives a {@link ErrorMessage} will cancel subsequent {@link Binding}s. The exchange will be completed
- * by {@link CompleteMessage} after receive the last result for the last binding.
+ * Execute multiple bindings of a server-preparing statement with one-by-one binary execution. The execution
+ * terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. If client receives a
+ * {@link ErrorMessage} will cancel subsequent {@link Binding}s. The exchange will be completed by
+ * {@link CompleteMessage} after receive the last result for the last binding.
*
* @param client the {@link Client} to exchange messages with.
* @param sql the statement for exception tracing.
* @param bindings the data of bindings.
* @param fetchSize the size of fetching, if it less than or equal to {@literal 0} means fetch all rows.
- * @param cache the cache of server-preparing result.
* @return the messages received in response to this exchange.
*/
- static Flux> execute(Client client, String sql, List bindings, int fetchSize,
- PrepareCache cache) {
+ static Flux> execute(Client client, String sql, List bindings, int fetchSize) {
return Flux.defer(() -> {
if (bindings.isEmpty()) {
return Flux.empty();
}
// Note: the prepared SQL may not be sent when the cache matches.
- return client.exchange(new PrepareExchangeable(cache, sql, bindings.iterator(), fetchSize))
+ return client.exchange(new PrepareExchangeable(client, sql, bindings.iterator(), fetchSize))
.windowUntil(RESULT_DONE);
});
}
/**
- * Execute multiple bindings of a client-preparing statement with one-by-one text query. The execution
- * terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage}
- * will emit an exception and cancel subsequent {@link Binding}s. This exchange will be completed by
- * {@link CompleteMessage} after receive the last result for the last binding.
+ * Execute multiple bindings of a client-preparing statement with one-by-one text query. The execution terminates
+ * with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception
+ * and cancel subsequent {@link Binding}s. This exchange will be completed by {@link CompleteMessage} after receive
+ * the last result for the last binding.
*
* @param client the {@link Client} to exchange messages with.
* @param query the {@link Query} for synthetic client-preparing statement.
@@ -152,8 +150,8 @@ static Flux> execute(
/**
* Execute a simple compound query. Query execution terminates with the last {@link CompleteMessage} or a
- * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed
- * by {@link CompleteMessage} after receive the last result for the last binding.
+ * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed by
+ * {@link CompleteMessage} after receive the last result for the last binding.
*
* @param client the {@link Client} to exchange messages with.
* @param sql the query to execute, can be contains multi-statements.
@@ -165,9 +163,9 @@ static Flux> execute(Client client, String sql) {
/**
* Execute multiple simple compound queries with one-by-one. Query execution terminates with the last
- * {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception and
- * cancel subsequent statements' execution. The exchange will be completed by {@link CompleteMessage}
- * after receive the last result for the last binding.
+ * {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception and cancel
+ * subsequent statements' execution. The exchange will be completed by {@link CompleteMessage} after receive the
+ * last result for the last binding.
*
* @param client the {@link Client} to exchange messages with.
* @param statements bundled sql for execute.
@@ -188,29 +186,9 @@ static Flux> execute(Client client, List statements)
}
/**
- * Login a {@link Client} and receive the {@code client} after logon. It will emit an exception when
- * client receives a {@link ErrorMessage}.
- *
- * @param client the {@link Client} to exchange messages with.
- * @param sslMode the {@link SslMode} defines SSL capability and behavior.
- * @param database the database that will be connected.
- * @param user the user that will be login.
- * @param password the password of the {@code user}.
- * @param context the {@link ConnectionContext} for initialization.
- * @return the messages received in response to the login exchange.
- */
- static Mono login(Client client, SslMode sslMode, String database, String user,
- @Nullable CharSequence password, ConnectionContext context) {
- return client.exchange(new LoginExchangeable(client, sslMode, database, user, password, context))
- .onErrorResume(e -> client.forceClose().then(Mono.error(e)))
- .then(Mono.just(client));
- }
-
- /**
- * Execute a simple query and return a {@link Mono} for the complete signal or error. Query execution
- * terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage}
- * will emit an exception. The exchange will be completed by {@link CompleteMessage} after receive the
- * last result for the last binding.
+ * Execute a simple query and return a {@link Mono} for the complete signal or error. Query execution terminates
+ * with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception.
+ * The exchange will be completed by {@link CompleteMessage} after receive the last result for the last binding.
*
* Note: this method does not support {@code LOCAL INFILE} due to it should be used for excepted queries.
*
@@ -234,18 +212,16 @@ static Mono executeVoid(Client client, String sql) {
}
/**
- * Begins a new transaction with a {@link TransactionDefinition}. It will change current transaction
- * statuses of the {@link ConnectionState}.
+ * Begins a new transaction with a {@link TransactionDefinition}. It will change current transaction statuses of
+ * the {@link ConnectionContext}.
*
* @param client the {@link Client} to exchange messages with.
- * @param state the connection state for checks and sets transaction statuses.
* @param batchSupported if connection supports batch query.
* @param definition the {@link TransactionDefinition}.
* @return receives complete signal.
*/
- static Mono beginTransaction(Client client, ConnectionState state, boolean batchSupported,
- TransactionDefinition definition) {
- final StartTransactionState startState = new StartTransactionState(state, definition);
+ static Mono beginTransaction(Client client, boolean batchSupported, TransactionDefinition definition) {
+ final StartTransactionState startState = new StartTransactionState(client, definition);
if (batchSupported) {
return client.exchange(new TransactionBatchExchangeable(startState)).then();
@@ -255,18 +231,15 @@ static Mono beginTransaction(Client client, ConnectionState state, boolean
}
/**
- * Commits or rollbacks current transaction. It will recover statuses of the {@link ConnectionState} in
- * the initial connection state.
+ * Commits or rollbacks current transaction. It will recover statuses of the {@link ConnectionContext}.
*
* @param client the {@link Client} to exchange messages with.
- * @param state the connection state for checks and resets transaction statuses.
- * @param commit if commit, otherwise rollback.
+ * @param commit if it is commit, otherwise rollback.
* @param batchSupported if connection supports batch query.
* @return receives complete signal.
*/
- static Mono doneTransaction(Client client, ConnectionState state, boolean commit,
- boolean batchSupported) {
- final CommitRollbackState commitState = new CommitRollbackState(state, commit);
+ static Mono doneTransaction(Client client, boolean commit, boolean batchSupported) {
+ final CommitRollbackState commitState = new CommitRollbackState(client, commit);
if (batchSupported) {
return client.exchange(new TransactionBatchExchangeable(commitState)).then();
@@ -275,20 +248,95 @@ static Mono doneTransaction(Client client, ConnectionState state, boolean
return client.exchange(new TransactionMultiExchangeable(commitState)).then();
}
- static Mono createSavepoint(Client client, ConnectionState state, String name,
- boolean batchSupported) {
- final CreateSavepointState savepointState = new CreateSavepointState(state, name);
+ /**
+ * Creates a savepoint with a name. It will begin a new transaction before creating a savepoint if the connection is
+ * not in a transaction.
+ *
+ * @param client the {@link Client} to exchange messages with.
+ * @param name the name of the savepoint.
+ * @param batchSupported if connection supports batch query.
+ * @return a {@link Mono} receives complete signal.
+ */
+ static Mono