From 187265a19591c64aedec01026572527de66b3303 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Wed, 7 Feb 2024 12:02:45 +0000 Subject: [PATCH 01/93] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 9b034e46f..6e697487b 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.asyncer r2dbc-mysql - 1.1.0 + 1.1.1-SNAPSHOT jar Reactive Relational Database Connectivity - MySQL @@ -55,7 +55,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.1.0 + HEAD From 6982accfc85b6bb2aade01267eb7555fd3e497e9 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 8 Feb 2024 00:00:42 +0900 Subject: [PATCH 02/93] Update README (#224) Motivation: Released 1.1.0 Modifications: Released 1.1.0 Result: up-to-date --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 97b52af43..04c5ec123 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Refer to the table below to determine the appropriate version of r2dbc-mysql for | spring-boot-starter-data-r2dbc | spring-data-r2dbc | r2dbc-spi | r2dbc-mysql(recommended) | |--------------------------------|-------------------|---------------|------------------------------| -| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.0.6 | +| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.1.0 | | 2.7.* | 1.5.* | 0.9.1.RELEASE | io.asyncer:r2dbc-mysql:0.9.7 | | 2.6.* and below | 1.4.* and below | 0.8.6.RELEASE | dev.miku:r2dbc-mysql:0.8.2 | @@ -59,7 +59,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so io.asyncer r2dbc-mysql - 1.0.6 + 1.1.0 ``` @@ -69,7 +69,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so ```groovy dependencies { - implementation 'io.asyncer:r2dbc-mysql:1.0.6' + implementation 'io.asyncer:r2dbc-mysql:1.1.0' } ``` @@ -78,7 +78,7 @@ dependencies { ```kotlin dependencies { // Maybe should to use `compile` instead of `implementation` on the lower version of Gradle. - implementation("io.asyncer:r2dbc-mysql:1.0.6") + implementation("io.asyncer:r2dbc-mysql:1.1.0") } ``` @@ -100,7 +100,7 @@ ConnectionFactory connectionFactory = ConnectionFactories.get( "sslKey=%2Fpath%2Fto%2Fmysql%2Fclient-key.pem&" + "sslCert=%2Fpath%2Fto%2Fmysql%2Fclient-cert.pem&" + "sslKeyPassword=key-pem-password-in-here" -) +); // Creating a Mono using Project Reactor Mono connectionMono = Mono.from(connectionFactory.create()); @@ -112,7 +112,7 @@ Or use unix domain socket like following: ```java // Minimum configuration for unix domain socket -ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:mysql://root@unix?unixSocket=%2Fpath%2Fto%2Fmysql.sock") +ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:mysql://root@unix?unixSocket=%2Fpath%2Fto%2Fmysql.sock"); Mono connectionMono = Mono.from(connectionFactory.create()); ``` @@ -140,7 +140,7 @@ ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() .option(Option.valueOf("sslContextBuilderCustomizer"), "com.example.demo.MyCustomizer") // optional, default is no-op customizer .option(Option.valueOf("zeroDate"), "use_null") // optional, default "use_null" .option(Option.valueOf("useServerPrepareStatement"), true) // optional, default false - .option(Option.valueOf("allowLoadLocalInfileInPath"), "/opt") // optional, default null, null means LOCAL INFILE not be allowed + .option(Option.valueOf("allowLoadLocalInfileInPath"), "/opt") // optional, default null, null means LOCAL INFILE not be allowed (since 1.1.0) .option(Option.valueOf("tcpKeepAlive"), true) // optional, default false .option(Option.valueOf("tcpNoDelay"), true) // optional, default false .option(Option.valueOf("autodetectExtensions"), false) // optional, default false From 5474bf26a8aa7c522c63f8f7830e3a79fe91da99 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 8 Feb 2024 13:18:05 +0900 Subject: [PATCH 03/93] Fixed NPE with `SslMode.TUNNEL` Usage (#225) Motivation: A NPE was identified when utilizing `SslMode.TUNNEL`. The issue arises when `ConnectionContext#isMariaDb` is invoked from `SslBridgeHandler#isTls13Enabled`, leading to an NPE due to the `ConnectionContext` not being initialized at that time. Modification: Do not invoke `ConnectionContext#isMariaDb` when it is not initialized. Result: This change addresses the NPE issue, ensuring stability when `SslMode.TUNNEL` is selected. It resolves the problem reported in https://github.com/GoogleCloudPlatform/cloud-sql-jdbc-socket-factory/pull/1828 --------- Signed-off-by: jchrys Co-authored-by: Mirro Mutth --- pom.xml | 12 + .../r2dbc/mysql/ConnectionContext.java | 1 + .../r2dbc/mysql/client/SslBridgeHandler.java | 6 +- .../r2dbc/mysql/SslTunnelIntegrationTest.java | 282 ++++++++++++++++++ 4 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java diff --git a/pom.xml b/pom.xml index 6e697487b..1f009e813 100644 --- a/pom.xml +++ b/pom.xml @@ -79,6 +79,7 @@ 0.3.0.RELEASE 3.0.2 24.1.0 + 1.77 @@ -117,6 +118,12 @@ ${java-annotations.version} provided + + org.bouncycastle + bcpkix-jdk18on + ${bouncy-castle.version} + test + @@ -240,6 +247,11 @@ jackson-annotations test + + org.bouncycastle + bcpkix-jdk18on + test + diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index 3039ea19b..f8f594fdc 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -57,6 +57,7 @@ public final class ConnectionContext implements CodecContext { */ private volatile short serverStatuses = ServerStatuses.AUTO_COMMIT; + @Nullable private volatile Capability capability = null; ConnectionContext(ZeroDateOption zeroDateOption, @Nullable Path localInfilePath, diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java index 823a59187..952be3917 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java @@ -220,8 +220,10 @@ static MySqlSslContextSpec forClient(MySqlSslConfiguration ssl, ConnectionContex .applicationProtocolConfig(null); String[] tlsProtocols = ssl.getTlsVersion(); - if (tlsProtocols.length > 0) { - builder.protocols(tlsProtocols); + if (tlsProtocols.length > 0 || ssl.getSslMode() == SslMode.TUNNEL) { + if (tlsProtocols.length > 0) { + builder.protocols(tlsProtocols); + } } else if (isTls13Enabled(context)) { builder.protocols(TLS_PROTOCOLS); } else { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java new file mode 100644 index 000000000..9261f2514 --- /dev/null +++ b/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java @@ -0,0 +1,282 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + + +import io.asyncer.r2dbc.mysql.constant.SslMode; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.SSLException; +import java.net.InetSocketAddress; +import java.security.cert.CertificateException; +import java.time.Duration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SslTunnelIntegrationTest { + + private SelfSignedCertificate server; + + private SelfSignedCertificate client; + + private SslTunnelServer sslTunnelServer; + + @BeforeEach + void setUp() throws CertificateException, SSLException, InterruptedException { + server = new SelfSignedCertificate(); + client = new SelfSignedCertificate(); + final SslContext sslContext = SslContextBuilder.forServer(server.key(), server.cert()).build(); + sslTunnelServer = new SslTunnelServer("localhost", 3306, sslContext); + sslTunnelServer.setUp(); + } + + @AfterEach + void tearDown() throws InterruptedException { + server.delete(); + client.delete(); + sslTunnelServer.tearDown(); + } + + @Test + void sslTunnelConnectionTest() { + final String password = System.getProperty("test.mysql.password"); + assertThat(password).withFailMessage("Property test.mysql.password must exists and not be empty") + .isNotNull() + .isNotEmpty(); + + final MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration + .builder() + .host("localhost") + .port(sslTunnelServer.getLocalPort()) + .connectTimeout(Duration.ofSeconds(3)) + .user("root") + .password(password) + .database("r2dbc") + .createDatabaseIfNotExist(true) + .sslMode(SslMode.TUNNEL) + .sslKey(client.privateKey().getAbsolutePath()) + .sslCert(client.certificate().getAbsolutePath()) + .sslCa(server.certificate().getAbsolutePath()) + .build(); + + final MySqlConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration); + + final MySqlConnection connection = connectionFactory.create().block(); + assert null != connection; + connection.createStatement("SELECT 3").execute() + .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Long.class))) + .doOnNext(it -> assertThat(it).isEqualTo(3L)) + .blockLast(); + + connection.close().block(); + } + + private static class SslTunnelServer { + + private final String remoteHost; + + private final int remotePort; + + private final SslContext sslContext; + + private volatile ChannelFuture channelFuture; + + + private SslTunnelServer(String remoteHost, int remotePort, SslContext sslContext) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + this.sslContext = sslContext; + } + + void setUp() throws InterruptedException { + // Configure the server. + ServerBootstrap b = new ServerBootstrap(); + b.localAddress(0) + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler(new ProxyInitializer(remoteHost, remotePort, sslContext)) + .childOption(ChannelOption.AUTO_READ, false); + + // Start the server. + channelFuture = b.bind().sync(); + } + + void tearDown() throws InterruptedException { + channelFuture.channel().close().sync(); + } + + int getLocalPort() { + return ((InetSocketAddress) channelFuture.channel().localAddress()).getPort(); + } + + } + + + private static class ProxyInitializer extends ChannelInitializer { + + private final String remoteHost; + + private final int remotePort; + + private final SslContext sslContext; + + ProxyInitializer(String remoteHost, int remotePort, SslContext sslContext) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + this.sslContext = sslContext; + } + + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(sslContext.newHandler(ch.alloc())); + ch.pipeline().addLast(new ProxyFrontendHandler(remoteHost, remotePort)); + } + } + + private static class ProxyFrontendHandler extends ChannelInboundHandlerAdapter { + + private final String remoteHost; + private final int remotePort; + + // As we use inboundChannel.eventLoop() when building the Bootstrap this does not need to be volatile as + // the outboundChannel will use the same EventLoop (and therefore Thread) as the inboundChannel. + private Channel outboundChannel; + + private ProxyFrontendHandler(String remoteHost, int remotePort) { + this.remoteHost = remoteHost; + this.remotePort = remotePort; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + final Channel inboundChannel = ctx.channel(); + + // Start the connection attempt. + Bootstrap b = new Bootstrap(); + b.group(inboundChannel.eventLoop()) + .channel(ctx.channel().getClass()) + .handler(new ProxyBackendHandler(inboundChannel)) + .option(ChannelOption.AUTO_READ, false); + ChannelFuture f = b.connect(remoteHost, remotePort); + outboundChannel = f.channel(); + f.addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + // connection complete start to read first data + inboundChannel.read(); + } else { + // Close the connection if the connection attempt has failed. + inboundChannel.close(); + } + }); + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + if (outboundChannel.isActive()) { + outboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + // was able to flush out data, start to read the next chunk + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + if (outboundChannel != null) { + closeOnFlush(outboundChannel); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + closeOnFlush(ctx.channel()); + } + + /** + * Closes the specified channel after all queued write requests are flushed. + */ + static void closeOnFlush(Channel ch) { + if (ch.isActive()) { + ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + } + } + } + + private static class ProxyBackendHandler extends ChannelInboundHandlerAdapter { + + private final Channel inboundChannel; + + private ProxyBackendHandler(Channel inboundChannel) { + this.inboundChannel = inboundChannel; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + if (!inboundChannel.isActive()) { + ProxyFrontendHandler.closeOnFlush(ctx.channel()); + } else { + ctx.read(); + } + } + + @Override + public void channelRead(final ChannelHandlerContext ctx, Object msg) { + inboundChannel.writeAndFlush(msg).addListener((ChannelFutureListener) future -> { + if (future.isSuccess()) { + ctx.channel().read(); + } else { + future.channel().close(); + } + }); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ProxyFrontendHandler.closeOnFlush(inboundChannel); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ProxyFrontendHandler.closeOnFlush(ctx.channel()); + } + } + +} From cdcfc77899b3d8a6d5786ab68e5ee26af21169f1 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Thu, 8 Feb 2024 04:24:50 +0000 Subject: [PATCH 04/93] [maven-release-plugin] prepare release r2dbc-mysql-1.1.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 1f009e813..bf3925790 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.asyncer r2dbc-mysql - 1.1.1-SNAPSHOT + 1.1.1 jar Reactive Relational Database Connectivity - MySQL @@ -55,7 +55,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.1.1 From 5def47e5e41704b67374650102ec77d4e42ab813 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Thu, 8 Feb 2024 04:24:53 +0000 Subject: [PATCH 05/93] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index bf3925790..233ad4756 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ io.asyncer r2dbc-mysql - 1.1.1 + 1.1.2-SNAPSHOT jar Reactive Relational Database Connectivity - MySQL @@ -55,7 +55,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.1.1 + HEAD From b0d00a0ef398a8102d71f617722e340fd5e0caa6 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 13 Feb 2024 11:47:07 +0900 Subject: [PATCH 06/93] Correct initialization of SSL tunnel tests --- .../r2dbc/mysql/SslTunnelIntegrationTest.java | 94 ++++++++++++------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java index 9261f2514..853b11809 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java @@ -16,7 +16,6 @@ package io.asyncer.r2dbc.mysql; - import io.asyncer.r2dbc.mysql.constant.SslMode; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; @@ -34,9 +33,13 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.r2dbc.spi.ValidationDepth; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import javax.net.ssl.SSLException; import java.net.InetSocketAddress; @@ -53,6 +56,35 @@ public class SslTunnelIntegrationTest { private SslTunnelServer sslTunnelServer; + @BeforeAll + static void initCachingSha2Password() { + // If the server uses caching_sha2_password, the first time a client connects to the server, the + // server will require a native SSL connection. So all the SSL tunnel tests should be run after + // the caching_sha2_password initialization. + String password = System.getProperty("test.mysql.password"); + + assertThat(password).withFailMessage("Property test.mysql.password must exists and not be empty") + .isNotNull() + .isNotEmpty(); + + MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() + .host("localhost") + .port(3306) + .connectTimeout(Duration.ofSeconds(3)) + .user("root") + .password(password) + .database("r2dbc") + .createDatabaseIfNotExist(true) + .build(); + + MySqlConnectionFactory.from(configuration).create() + .flatMap(connection -> connection.validate(ValidationDepth.REMOTE) + .flatMap(it -> connection.close().then(Mono.just(it)))) + .as(StepVerifier::create) + .expectNext(true) + .verifyComplete(); + } + @BeforeEach void setUp() throws CertificateException, SSLException, InterruptedException { server = new SelfSignedCertificate(); @@ -73,32 +105,30 @@ void tearDown() throws InterruptedException { void sslTunnelConnectionTest() { final String password = System.getProperty("test.mysql.password"); assertThat(password).withFailMessage("Property test.mysql.password must exists and not be empty") - .isNotNull() - .isNotEmpty(); - - final MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration - .builder() - .host("localhost") - .port(sslTunnelServer.getLocalPort()) - .connectTimeout(Duration.ofSeconds(3)) - .user("root") - .password(password) - .database("r2dbc") - .createDatabaseIfNotExist(true) - .sslMode(SslMode.TUNNEL) - .sslKey(client.privateKey().getAbsolutePath()) - .sslCert(client.certificate().getAbsolutePath()) - .sslCa(server.certificate().getAbsolutePath()) - .build(); + .isNotNull() + .isNotEmpty(); + + final MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() + .host("localhost") + .port(sslTunnelServer.getLocalPort()) + .connectTimeout(Duration.ofSeconds(3)) + .user("root") + .password(password) + .database("r2dbc") + .sslMode(SslMode.TUNNEL) + .sslKey(client.privateKey().getAbsolutePath()) + .sslCert(client.certificate().getAbsolutePath()) + .sslCa(server.certificate().getAbsolutePath()) + .build(); final MySqlConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration); final MySqlConnection connection = connectionFactory.create().block(); assert null != connection; connection.createStatement("SELECT 3").execute() - .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Long.class))) - .doOnNext(it -> assertThat(it).isEqualTo(3L)) - .blockLast(); + .flatMap(it -> it.map((row, rowMetadata) -> row.get(0, Long.class))) + .doOnNext(it -> assertThat(it).isEqualTo(3L)) + .blockLast(); connection.close().block(); } @@ -113,7 +143,6 @@ private static class SslTunnelServer { private volatile ChannelFuture channelFuture; - private SslTunnelServer(String remoteHost, int remotePort, SslContext sslContext) { this.remoteHost = remoteHost; this.remotePort = remotePort; @@ -124,10 +153,10 @@ void setUp() throws InterruptedException { // Configure the server. ServerBootstrap b = new ServerBootstrap(); b.localAddress(0) - .group(new NioEventLoopGroup()) - .channel(NioServerSocketChannel.class) - .childHandler(new ProxyInitializer(remoteHost, remotePort, sslContext)) - .childOption(ChannelOption.AUTO_READ, false); + .group(new NioEventLoopGroup()) + .channel(NioServerSocketChannel.class) + .childHandler(new ProxyInitializer(remoteHost, remotePort, sslContext)) + .childOption(ChannelOption.AUTO_READ, false); // Start the server. channelFuture = b.bind().sync(); @@ -143,7 +172,6 @@ int getLocalPort() { } - private static class ProxyInitializer extends ChannelInitializer { private final String remoteHost; @@ -168,10 +196,12 @@ public void initChannel(SocketChannel ch) { private static class ProxyFrontendHandler extends ChannelInboundHandlerAdapter { private final String remoteHost; + private final int remotePort; - // As we use inboundChannel.eventLoop() when building the Bootstrap this does not need to be volatile as - // the outboundChannel will use the same EventLoop (and therefore Thread) as the inboundChannel. + // As we use inboundChannel.eventLoop() when building the Bootstrap this does not need to be + // volatile as the outboundChannel will use the same EventLoop (and therefore Thread) as the + // inboundChannel. private Channel outboundChannel; private ProxyFrontendHandler(String remoteHost, int remotePort) { @@ -186,9 +216,9 @@ public void channelActive(ChannelHandlerContext ctx) { // Start the connection attempt. Bootstrap b = new Bootstrap(); b.group(inboundChannel.eventLoop()) - .channel(ctx.channel().getClass()) - .handler(new ProxyBackendHandler(inboundChannel)) - .option(ChannelOption.AUTO_READ, false); + .channel(ctx.channel().getClass()) + .handler(new ProxyBackendHandler(inboundChannel)) + .option(ChannelOption.AUTO_READ, false); ChannelFuture f = b.connect(remoteHost, remotePort); outboundChannel = f.channel(); f.addListener((ChannelFutureListener) future -> { From fbdd900573fc7d479aafc8cb4bb5af2ddceb69af Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 13 Feb 2024 10:46:40 +0900 Subject: [PATCH 07/93] Add support for decoding session state info --- .../io/asyncer/r2dbc/mysql/Capability.java | 12 +- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 1 + .../r2dbc/mysql/message/server/OkMessage.java | 105 +++++++++++++++--- .../mysql/message/server/OkMessageTest.java | 52 +++++++++ 4 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java b/src/main/java/io/asyncer/r2dbc/mysql/Capability.java index 133d52e48..1bff4247f 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java +++ b/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. @@ -171,7 +175,7 @@ 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; private final long bitmap; @@ -377,6 +381,10 @@ void disableSsl() { this.bitmap &= ~SSL; } + void disableSessionTrack() { + this.bitmap &= ~SESSION_TRACK; + } + void disableConnectAttributes() { this.bitmap &= ~CONNECT_ATTRS; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 26721b911..d8caa6a31 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -945,6 +945,7 @@ private AuthResponse createAuthResponse(int envelopeId, String phase) { private Capability clientCapability(Capability serverCapability) { Capability.Builder builder = serverCapability.mutate(); + builder.disableSessionTrack(); builder.disableDatabasePinned(); builder.disableCompression(); builder.disableIgnoreAmbiguitySpace(); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java index da937fe17..a06a00fbe 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java @@ -21,8 +21,12 @@ import io.asyncer.r2dbc.mysql.constant.ServerStatuses; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; +import org.jetbrains.annotations.Nullable; import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; @@ -34,6 +38,8 @@ */ public final class OkMessage implements WarningMessage, ServerStatusMessage, CompleteMessage { + private static final int SESSION_TRACK_SYSTEM_VARIABLES = 0; + private static final int MIN_SIZE = 7; private final boolean isEndOfRows; @@ -51,14 +57,17 @@ public final class OkMessage implements WarningMessage, ServerStatusMessage, Com private final String information; + private final Map systemVariables; + private OkMessage(boolean isEndOfRows, long affectedRows, long lastInsertId, short serverStatuses, - int warnings, String information) { + int warnings, String information, Map systemVariables) { this.isEndOfRows = isEndOfRows; this.affectedRows = affectedRows; this.lastInsertId = lastInsertId; this.serverStatuses = serverStatuses; this.warnings = warnings; this.information = requireNonNull(information, "information must not be null"); + this.systemVariables = requireNonNull(systemVariables, "systemVariables must not be null"); } public boolean isEndOfRows() { @@ -83,6 +92,11 @@ public int getWarnings() { return warnings; } + @Nullable + public String getSystemVariable(String key) { + return systemVariables.get(key); + } + @Override public boolean isDone() { return (serverStatuses & ServerStatuses.MORE_RESULTS_EXISTS) == 0; @@ -104,7 +118,8 @@ public boolean equals(Object o) { lastInsertId == okMessage.lastInsertId && serverStatuses == okMessage.serverStatuses && warnings == okMessage.warnings && - information.equals(okMessage.information); + information.equals(okMessage.information) && + systemVariables.equals(okMessage.systemVariables); } @Override @@ -114,7 +129,8 @@ public int hashCode() { result = 31 * result + (int) (lastInsertId ^ (lastInsertId >>> 32)); result = 31 * result + serverStatuses; result = 31 * result + warnings; - return 31 * result + information.hashCode(); + result = 31 * result + information.hashCode(); + return 31 * result + systemVariables.hashCode(); } @Override @@ -124,7 +140,9 @@ public String toString() { ", affectedRows=" + Long.toUnsignedString(affectedRows) + ", lastInsertId=" + Long.toUnsignedString(lastInsertId) + ", serverStatuses=" + Integer.toHexString(serverStatuses) + - ", information='" + information + "'}"; + ", information='" + information + + "', systemVariables=" + systemVariables + + '}'; } return "OkMessage{isEndOfRows=" + isEndOfRows + @@ -132,7 +150,9 @@ public String toString() { ", lastInsertId=" + Long.toUnsignedString(lastInsertId) + ", serverStatuses=" + Integer.toHexString(serverStatuses) + ", warnings=" + warnings + - ", information='" + information + "'}"; + ", information='" + information + + "', systemVariables=" + systemVariables + + "}"; } static boolean isValidSize(int bytes) { @@ -164,26 +184,79 @@ static OkMessage decode(boolean isEndOfRows, ByteBuf buf, ConnectionContext cont if (sizeAfterVarInt < 0) { return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, - warnings, buf.toString(charset)); + warnings, buf.toString(charset), Collections.emptyMap()); + } + + int oldReaderIndex = buf.readerIndex(); + long infoSize = VarIntUtils.readVarInt(buf); + + if (infoSize > sizeAfterVarInt) { + // Compatible code, the information may be an EOF encoded string at early versions of MySQL. + String info = buf.toString(oldReaderIndex, buf.writerIndex() - oldReaderIndex, charset); + + return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, + info, Collections.emptyMap()); } - int readerIndex = buf.readerIndex(); - long size = VarIntUtils.readVarInt(buf); - String information; + // All the following have lengths should be less than Integer.MAX_VALUE + String information = buf.readCharSequence((int) infoSize, charset).toString(); + Map systemVariables = Collections.emptyMap(); + + while (VarIntUtils.checkNextVarInt(buf) >= 0) { + int stateInfoSize = (int) VarIntUtils.readVarInt(buf); + ByteBuf stateInfo = buf.readSlice(stateInfoSize); + + while (stateInfo.isReadable()) { + if (stateInfo.readByte() == SESSION_TRACK_SYSTEM_VARIABLES) { + systemVariables = readServerVariables(stateInfo, context); + } else { + // Ignore other state info + int skipBytes = (int) VarIntUtils.readVarInt(stateInfo); - if (size > sizeAfterVarInt) { - information = buf.toString(readerIndex, buf.writerIndex() - readerIndex, charset); - } else { - // JVM does NOT support strings longer than Integer.MAX_VALUE - information = buf.toString(buf.readerIndex(), (int) size, charset); + stateInfo.skipBytes(skipBytes); + } + } } // Ignore session track, it is not human-readable and useless for R2DBC client. return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, - information); + information, systemVariables); } // Maybe have no human-readable message - return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, ""); + return new OkMessage(isEndOfRows, affectedRows, lastInsertId, serverStatuses, warnings, "", + Collections.emptyMap()); + } + + private static Map readServerVariables(ByteBuf buf, ConnectionContext context) { + // All lengths should NOT be greater than Integer.MAX_VALUE + Map map = new HashMap<>(); + Charset charset = context.getClientCollation().getCharset(); + int size = (int) VarIntUtils.readVarInt(buf); + ByteBuf sessionVar = buf.readSlice(size); + + while (sessionVar.readableBytes() > 0) { + int variableSize = (int) VarIntUtils.readVarInt(sessionVar); + String variable = sessionVar.toString(sessionVar.readerIndex(), variableSize, charset); + + sessionVar.skipBytes(variableSize); + + int valueSize = (int) VarIntUtils.readVarInt(sessionVar); + String value = sessionVar.toString(sessionVar.readerIndex(), valueSize, charset); + + sessionVar.skipBytes(valueSize); + map.put(variable, value); + } + + switch (map.size()) { + case 0: + return Collections.emptyMap(); + case 1: { + Map.Entry entry = map.entrySet().iterator().next(); + return Collections.singletonMap(entry.getKey(), entry.getValue()); + } + default: + return map; + } } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java b/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java new file mode 100644 index 000000000..02a5fdd31 --- /dev/null +++ b/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.message.server; + +import io.asyncer.r2dbc.mysql.ConnectionContext; +import io.asyncer.r2dbc.mysql.ConnectionContextTest; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link OkMessage}. + */ +class OkMessageTest { + + @Test + void decodeSessionVariables() { + boolean isMariaDb = "mariadb".equalsIgnoreCase(System.getProperty("test.db.type")); + ConnectionContext context = ConnectionContextTest.mock(isMariaDb); + OkMessage message = OkMessage.decode(true, sessionVariablesOk(), context); + + assertThat(message.getAffectedRows()).isOne(); + assertThat(message.getLastInsertId()).isEqualTo(2); + assertThat(message.getServerStatuses()).isEqualTo((short) 0x4000); + assertThat(message.getWarnings()).isEqualTo(3); + assertThat(message.getSystemVariable("autocommit")).isEqualTo("OFF"); + } + + private static ByteBuf sessionVariablesOk() { + return Unpooled.wrappedBuffer(new byte[] { + 0, + 1, 2, 0, 0x40, 3, 0, 0, 0x11, 0, 0xf, 0xa, + 0x61, 0x75, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x3, 0x4f, 0x46, 0x46, + }); + } +} From 1deb113fb45cf691d440a90023b9ccf9790f5440 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Mon, 29 Jan 2024 12:32:39 +0900 Subject: [PATCH 08/93] Add support for compression protocol - Refactor message codecs, move sequence ID handling to MessageDuplexCodec - Add compression codec and support for zstd and zlib - Add configuration options for compression algorithms and zstd compression level - Refactor the configuration way at `IntegrationTestSupport` --- README.md | 9 + pom.xml | 8 + .../io/asyncer/r2dbc/mysql/Capability.java | 39 ++- .../mysql/MySqlConnectionConfiguration.java | 99 ++++++- .../r2dbc/mysql/MySqlConnectionFactory.java | 19 +- .../mysql/MySqlConnectionFactoryProvider.java | 49 +++- .../io/asyncer/r2dbc/mysql/OptionMapper.java | 42 ++- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 109 +++++--- .../r2dbc/mysql/authentication/AuthUtils.java | 2 +- .../CachingSha2FastAuthProvider.java | 2 +- .../CachingSha2FullAuthProvider.java | 2 +- .../MySqlClearAuthProvider.java | 2 +- .../authentication/Sha256AuthProvider.java | 2 +- .../mysql/client/CompressionDuplexCodec.java | 243 ++++++++++++++++++ .../r2dbc/mysql/client/Compressor.java | 45 ++++ .../r2dbc/mysql/client/EnvelopeSlicer.java | 58 ----- .../mysql/client/MessageDuplexCodec.java | 178 ++++++++++--- .../r2dbc/mysql/client/PacketEvent.java | 35 +++ .../mysql/client/ReactorNettyClient.java | 18 +- .../r2dbc/mysql/client/SslBridgeHandler.java | 4 +- .../r2dbc/mysql/client/WriteSubscriber.java | 5 + .../r2dbc/mysql/client/ZlibCompressor.java | 179 +++++++++++++ .../r2dbc/mysql/client/ZstdCompressor.java | 60 +++++ .../mysql/constant/CompressionAlgorithm.java | 42 +++ .../constant/{Envelopes.java => Packets.java} | 21 +- .../mysql/internal/util/FluxEnvelope.java | 47 ++-- .../mysql/internal/util/OperatorUtils.java | 10 +- .../internal/util/ReadCompletionHandler.java | 2 +- .../mysql/message/client/AuthResponse.java | 16 +- .../mysql/message/client/ClientMessage.java | 13 + .../message/client/HandshakeResponse.java | 28 +- .../message/client/HandshakeResponse320.java | 14 +- .../message/client/HandshakeResponse41.java | 37 +-- .../message/client/LocalInfileResponse.java | 22 +- .../mysql/message/client/SslRequest.java | 7 +- .../mysql/message/client/SslRequest320.java | 23 +- .../mysql/message/client/SslRequest41.java | 23 +- .../client/SubsequenceClientMessage.java | 10 +- .../message/server/AuthMoreDataMessage.java | 19 +- .../message/server/ChangeAuthMessage.java | 26 +- .../mysql/message/server/HandshakeHeader.java | 2 +- .../message/server/HandshakeRequest.java | 16 +- .../message/server/HandshakeV10Request.java | 44 ++-- .../message/server/HandshakeV9Request.java | 28 +- .../message/server/LargeFieldReader.java | 4 +- .../message/server/LocalInfileRequest.java | 20 +- .../message/server/ServerMessageDecoder.java | 65 ++--- .../CompressionIntegrationTestSupport.java | 88 +++++++ .../mysql/ConnectionIntegrationTest.java | 5 +- .../r2dbc/mysql/InitDbIntegrationTest.java | 5 +- .../r2dbc/mysql/IntegrationTestSupport.java | 24 +- .../mysql/JacksonPrepareIntegrationTest.java | 3 +- .../mysql/JacksonTextIntegrationTest.java | 2 +- .../mysql/MariaDbIntegrationTestSupport.java | 9 +- .../mysql/MariaDbPrepareIntegrationTest.java | 2 +- .../mysql/MariaDbTextIntegrationTest.java | 2 +- .../MySqlConnectionConfigurationTest.java | 3 + .../MySqlConnectionFactoryProviderTest.java | 54 +++- .../r2dbc/mysql/MySqlPrepareTestKit.java | 3 +- .../asyncer/r2dbc/mysql/MySqlTextTestKit.java | 2 +- .../mysql/PrepareQueryIntegrationTest.java | 2 +- .../mysql/PrepareTimeZoneIntegrationTest.java | 2 +- .../r2dbc/mysql/TextQueryIntegrationTest.java | 2 +- .../mysql/TextTimeZoneIntegrationTest.java | 2 +- .../mysql/TimeZoneIntegrationTestSupport.java | 9 +- .../ZlibCompressionIntegrationTest.java} | 18 +- .../mysql/ZstdCompressionIntegrationTest.java | 48 ++++ .../mysql/client/ZlibCompressorTest.java | 101 ++++++++ .../mysql/internal/util/FluxEnvelopeTest.java | 7 +- .../server/ServerMessageDecoderTest.java | 29 ++- 70 files changed, 1642 insertions(+), 528 deletions(-) create mode 100644 src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java create mode 100644 src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java delete mode 100644 src/main/java/io/asyncer/r2dbc/mysql/client/EnvelopeSlicer.java create mode 100644 src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java create mode 100644 src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java create mode 100644 src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java create mode 100644 src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java rename src/main/java/io/asyncer/r2dbc/mysql/constant/{Envelopes.java => Packets.java} (57%) create mode 100644 src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java rename src/{main/java/io/asyncer/r2dbc/mysql/client/Lifecycle.java => test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java} (62%) create mode 100644 src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java create mode 100644 src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java diff --git a/README.md b/README.md index 04c5ec123..d39cf40e7 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Refer to the table below to determine the appropriate version of r2dbc-mysql for This driver provides the following features: - [x] Unix domain socket. +- [x] Compression protocols, including zstd and zlib. - [x] Execution of simple or batch statements without bindings. - [x] Execution of prepared statements with bindings. - [x] Reactive LOB types (e.g. BLOB, CLOB) @@ -143,6 +144,7 @@ ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() .option(Option.valueOf("allowLoadLocalInfileInPath"), "/opt") // optional, default null, null means LOCAL INFILE not be allowed (since 1.1.0) .option(Option.valueOf("tcpKeepAlive"), true) // optional, default false .option(Option.valueOf("tcpNoDelay"), true) // optional, default false + .option(Option.valueOf("compressionAlgorithms"), "zstd") // optional, default UNCOMPRESSED .option(Option.valueOf("autodetectExtensions"), false) // optional, default false .option(Option.valueOf("passwordPublisher"), Mono.just("password")) // optional, default null, null means has no passwordPublisher (since 1.0.5 / 0.9.6) .build(); @@ -191,6 +193,7 @@ MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builde .allowLoadLocalInfileInPath("/opt") // optional, default null, null means LOCAL INFILE not be allowed .tcpKeepAlive(true) // optional, controls TCP Keep Alive, default is false .tcpNoDelay(true) // optional, controls TCP No Delay, default is false + .compressionAlgorithms(CompressionAlgorithm.ZSTD, CompressionAlgotihm.ZLIB) // optional, default is UNCOMPRESSED .autodetectExtensions(false) // optional, controls extension auto-detect, default is true .extendWith(MyExtension.INSTANCE) // optional, manual extend an extension into extensions, default using auto-detect .passwordPublisher(Mono.just("password")) // optional, default null, null means has no password publisher (since 1.0.5 / 0.9.6) @@ -242,6 +245,7 @@ Mono connectionMono = Mono.from(connectionFactory.create()); | autodetectExtensions | `true` or `false` | Optional, default is `true` | Controls auto-detect `Extension`s | | useServerPrepareStatement | `true`, `false` or `Predicate` | Optional, default is `false` | See following notice | | allowLoadLocalInfileInPath | A path | Optional, default is `null` | The path that allows `LOAD DATA LOCAL INFILE` to load file data | +| compressionAlgorithms | A list of `CompressionAlgorithm` | Optional, default is `UNCOMPRESSED` | The compression algorithms for MySQL connection | | passwordPublisher | A `Publisher` | Optional, default is `null` | The password publisher, see following notice | - `SslMode` Considers security level and verification for SSL, make sure the database server supports SSL before you want change SSL mode to `REQUIRED` or higher. **The Unix Domain Socket only offers "DISABLED" available** @@ -269,6 +273,11 @@ Mono connectionMono = Mono.from(connectionFactory.create()); - The `Extensions` will not remove duplicates, make sure it would be not extended twice or more - The auto-detected `Extension`s will not affect manual extends and will not remove duplicates - `passwordPublisher` Every time the client attempts to authenticate, it will use the password provided by the `passwordPublisher`.(Since `1.0.5` / `0.9.6`) e.g., You can employ this method for IAM-based authentication when connecting to an AWS Aurora RDS database. +- `compressionAlgorithms` Considers compression protocol for MySQL connection, it is **NOT** RECOMMENDED to use compression protocol in the general case, because it will increase the CPU usage and decrease the performance. + - `UNCOMPRESSED` (default) No compression + - `ZLIB` Use Zlib compression protocol, it is available on almost all MySQL versions (`5.x` and above) + - `ZSTD` Use Z-standard compression protocol, it is available since MySQL `8.0.18` or above, requires an extern dependency `com.github.luben:zstd-jni` + - For scenarios where the network environment is poor or the amount of data is always large, using a compression protocol may be useful Should use `enum` in [Programmatic](#programmatic-configuration) configuration that not like discovery configurations, except `TlsVersions` (All elements of `TlsVersions` will be always `String` which is case-sensitive). diff --git a/pom.xml b/pom.xml index 233ad4756..286b61581 100644 --- a/pom.xml +++ b/pom.xml @@ -78,6 +78,7 @@ 2.16.0 0.3.0.RELEASE 3.0.2 + 1.5.5-11 24.1.0 1.77 @@ -153,6 +154,13 @@ provided + + com.github.luben + zstd-jni + ${zstd-jni.version} + true + + ch.qos.logback logback-classic diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java b/src/main/java/io/asyncer/r2dbc/mysql/Capability.java index 1bff4247f..67ebc3711 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/Capability.java @@ -158,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. @@ -175,7 +175,7 @@ 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 | SESSION_TRACK | DEPRECATE_EOF; + PLUGIN_AUTH | CONNECT_ATTRS | VAR_INT_SIZED_AUTH | SESSION_TRACK | DEPRECATE_EOF | ZSTD_COMPRESS; private final long bitmap; @@ -278,6 +278,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. * @@ -362,9 +389,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; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 0eec8645c..7f3bed2b4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -16,6 +16,7 @@ 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; @@ -30,9 +31,12 @@ 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; @@ -97,6 +101,10 @@ public final class MySqlConnectionConfiguration { private final int prepareCacheSize; + private final Set compressionAlgorithms; + + private final int zstdCompressionLevel; + private final Extensions extensions; @Nullable @@ -109,8 +117,9 @@ private MySqlConnectionConfiguration( 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 + int queryCacheSize, int prepareCacheSize, + Set compressionAlgorithms, int zstdCompressionLevel, + Extensions extensions, @Nullable Publisher passwordPublisher ) { this.isHost = isHost; this.domain = domain; @@ -130,6 +139,8 @@ private MySqlConnectionConfiguration( this.localInfileBufferSize = localInfileBufferSize; this.queryCacheSize = queryCacheSize; this.prepareCacheSize = prepareCacheSize; + this.compressionAlgorithms = compressionAlgorithms; + this.zstdCompressionLevel = zstdCompressionLevel; this.extensions = extensions; this.passwordPublisher = passwordPublisher; } @@ -220,6 +231,14 @@ int getPrepareCacheSize() { return prepareCacheSize; } + Set getCompressionAlgorithms() { + return compressionAlgorithms; + } + + int getZstdCompressionLevel() { + return zstdCompressionLevel; + } + Extensions getExtensions() { return extensions; } @@ -256,6 +275,8 @@ public boolean equals(Object o) { localInfileBufferSize == that.localInfileBufferSize && queryCacheSize == that.queryCacheSize && prepareCacheSize == that.prepareCacheSize && + compressionAlgorithms.equals(that.compressionAlgorithms) && + zstdCompressionLevel == that.zstdCompressionLevel && extensions.equals(that.extensions) && Objects.equals(passwordPublisher, that.passwordPublisher); } @@ -265,7 +286,7 @@ 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); + prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, extensions, passwordPublisher); } @Override @@ -280,6 +301,8 @@ public String toString() { ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize + + ", compressionAlgorithms=" + compressionAlgorithms + + ", zstdCompressionLevel=" + zstdCompressionLevel + ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; } @@ -291,8 +314,10 @@ public String toString() { ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + ", queryCacheSize=" + queryCacheSize + - ", prepareCacheSize=" + prepareCacheSize + ", extensions=" + extensions + - ", passwordPublisher=" + passwordPublisher + '}'; + ", prepareCacheSize=" + prepareCacheSize + + ", compressionAlgorithms=" + compressionAlgorithms + + ", zstdCompressionLevel=" + zstdCompressionLevel + + ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; } /** @@ -363,6 +388,11 @@ public static final class Builder { private int prepareCacheSize = 256; + private Set compressionAlgorithms = + Collections.singleton(CompressionAlgorithm.UNCOMPRESSED); + + private int zstdCompressionLevel = 3; + private boolean autodetectExtensions = true; private final List extensions = new ArrayList<>(); @@ -395,6 +425,7 @@ public MySqlConnectionConfiguration build() { connectTimeout, zeroDateOption, serverZoneId, user, password, database, createDatabaseIfNotExist, preferPrepareStatement, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, + compressionAlgorithms, zstdCompressionLevel, Extensions.from(extensions, autodetectExtensions), passwordPublisher); } @@ -822,6 +853,64 @@ 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.0 + */ + 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. + * @since 1.1.0 + * @see + * MySQL Connection Options --zstd-compression-level + */ + public Builder zstdCompressionLevel(int level) { + require(level >= 1 && level <= 22, "level must be between 1 and 22"); + + this.zstdCompressionLevel = level; + return this; + } + /** * Configures whether to use {@link ServiceLoader} to discover and register extensions. Defaults to * {@code true}. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index 6070d195c..4c2d69cd4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -22,6 +22,7 @@ import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.codec.CodecsBuilder; +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.extension.CodecRegistrar; import io.netty.buffer.ByteBufAllocator; @@ -35,6 +36,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Objects; +import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Predicate; @@ -90,12 +92,14 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura String user = configuration.getUser(); CharSequence password = configuration.getPassword(); SslMode sslMode = ssl.getSslMode(); + int zstdCompressionLevel = configuration.getZstdCompressionLevel(); ConnectionContext context = new ConnectionContext( configuration.getZeroDateOption(), configuration.getLoadLocalInfilePath(), configuration.getLocalInfileBufferSize(), configuration.getServerZoneId() ); + Set compressionAlgorithms = configuration.getCompressionAlgorithms(); Extensions extensions = configuration.getExtensions(); Predicate prepare = configuration.getPreferPrepareStatement(); int prepareCacheSize = configuration.getPrepareCacheSize(); @@ -106,8 +110,9 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura configuration, queryCache, ssl, address, database, createDbIfNotExist, - user, sslMode, context, - extensions, prepare, + user, sslMode, + compressionAlgorithms, zstdCompressionLevel, + context, extensions, prepare, prepareCacheSize, token )); } @@ -116,8 +121,9 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura configuration, queryCache, ssl, address, database, createDbIfNotExist, - user, sslMode, context, - extensions, prepare, + user, sslMode, + compressionAlgorithms, zstdCompressionLevel, + context, extensions, prepare, prepareCacheSize, password ); })); @@ -132,6 +138,8 @@ private static Mono getMySqlConnection( final boolean createDbIfNotExist, final String user, final SslMode sslMode, + final Set compressionAlgorithms, + final int zstdCompressionLevel, final ConnectionContext context, final Extensions extensions, @Nullable final Predicate prepare, @@ -142,7 +150,8 @@ private static Mono getMySqlConnection( .flatMap(client -> { // Lazy init database after handshake/login String db = createDbIfNotExist ? "" : database; - return QueryFlow.login(client, sslMode, db, user, password, context); + return QueryFlow.login(client, sslMode, db, user, password, compressionAlgorithms, + zstdCompressionLevel, context); }) .flatMap(client -> { ByteBufAllocator allocator = client.getByteBufAllocator(); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index eba16df09..ef1286b7b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -16,6 +16,7 @@ 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; @@ -181,9 +182,41 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr public static final Option USE_SERVER_PREPARE_STATEMENT = Option.valueOf("useServerPrepareStatement"); + /** + * 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 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.0 + */ + 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.0 + */ + public static final Option ZSTD_COMPRESSION_LEVEL = + Option.valueOf("zstdCompressionLevel"); + /** * Option to set the maximum size of the {@link Query} parsing cache. Default to {@code 256}. * @@ -206,10 +239,9 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr 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. - * + * 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"); @@ -273,6 +305,13 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { .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()), + CompressionAlgorithm[]::new + ).to(builder::compressionAlgorithms); + mapper.optional(ZSTD_COMPRESSION_LEVEL).asInt() + .to(builder::zstdCompressionLevel); mapper.optional(PASSWORD_PUBLISHER).as(Publisher.class) .to(builder::passwordPublisher); @@ -295,7 +334,7 @@ private static void setupHost(MySqlConnectionConfiguration.Builder builder, Opti .to(isSsl -> builder.sslMode(isSsl ? SslMode.REQUIRED : SslMode.DISABLED)); mapper.optional(SSL_MODE).as(SslMode.class, id -> SslMode.valueOf(id.toUpperCase())) .to(builder::sslMode); - mapper.optional(TLS_VERSION).asStrings() + mapper.optional(TLS_VERSION).asArray(String[].class, Function.identity(), String[]::new) .to(builder::tlsVersion); mapper.optional(SSL_HOSTNAME_VERIFIER).as(HostnameVerifier.class) .to(builder::sslHostnameVerifier); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java b/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java index 218cc11df..8a95cea1d 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java +++ b/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; /** @@ -105,21 +106,38 @@ Source as(Class type, Function mapping) { throw new IllegalArgumentException(toMessage(value, type.getTypeName())); } - Source asStrings() { + Source asArray(Class arrayType, Function mapper, 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 = ((String) value).split(","); + + 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 +254,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/QueryFlow.java b/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index d8caa6a31..59675e647 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -20,6 +20,7 @@ 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.constant.CompressionAlgorithm; import io.asyncer.r2dbc.mysql.constant.ServerStatuses; import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; @@ -56,6 +57,7 @@ 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.TransactionDefinition; import org.jetbrains.annotations.Nullable; @@ -74,6 +76,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -191,17 +194,22 @@ 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. + * @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. + * @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)) + @Nullable CharSequence password, + Set compressionAlgorithms, int zstdCompressionLevel, + ConnectionContext context) { + return client.exchange(new LoginExchangeable(client, sslMode, database, user, password, + compressionAlgorithms, zstdCompressionLevel, context)) .onErrorResume(e -> client.forceClose().then(Mono.error(e))) .then(Mono.just(client)); } @@ -327,7 +335,7 @@ public final void accept(ServerMessage message, SynchronousSink s QueryLogger.logLocalInfile(path); requests.emitNext( - new LocalInfileResponse(request.getEnvelopeId() + 1, path, sink), + new LocalInfileResponse(path, sink), Sinks.EmitFailureHandler.FAIL_FAST ); } else { @@ -828,6 +836,10 @@ final class LoginExchangeable extends FluxExchangeable { @Nullable private final CharSequence password; + private final Set compressions; + + private final int zstdCompressionLevel; + private final ConnectionContext context; private boolean handshake = true; @@ -838,15 +850,16 @@ final class LoginExchangeable extends FluxExchangeable { private boolean sslCompleted; - private int lastEnvelopeId; - LoginExchangeable(Client client, SslMode sslMode, String database, String user, - @Nullable CharSequence password, ConnectionContext context) { + @Nullable CharSequence password, Set compressions, + int zstdCompressionLevel, ConnectionContext context) { this.client = client; this.sslMode = sslMode; this.database = database; this.user = user; this.password = password; + this.compressions = compressions; + this.zstdCompressionLevel = zstdCompressionLevel; this.context = context; this.sslCompleted = sslMode == SslMode.TUNNEL; } @@ -870,13 +883,10 @@ public void accept(ServerMessage message, SynchronousSink sink) { HandshakeRequest request = (HandshakeRequest) message; Capability capability = initHandshake(request); - lastEnvelopeId = request.getEnvelopeId() + 1; - if (capability.isSslEnabled()) { - emitNext(SslRequest.from(lastEnvelopeId, capability, - context.getClientCollation().getId()), sink); + emitNext(SslRequest.from(capability, context.getClientCollation().getId()), sink); } else { - emitNext(createHandshakeResponse(lastEnvelopeId, capability), sink); + emitNext(createHandshakeResponse(capability), sink); } } else { sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + @@ -891,10 +901,9 @@ public void accept(ServerMessage message, SynchronousSink sink) { sink.complete(); } else if (message instanceof SyntheticSslResponseMessage) { sslCompleted = true; - emitNext(createHandshakeResponse(++lastEnvelopeId, context.getCapability()), sink); + emitNext(createHandshakeResponse(context.getCapability()), sink); } else if (message instanceof AuthMoreDataMessage) { AuthMoreDataMessage msg = (AuthMoreDataMessage) message; - lastEnvelopeId = msg.getEnvelopeId() + 1; if (msg.isFailed()) { if (logger.isDebugEnabled()) { @@ -902,15 +911,15 @@ public void accept(ServerMessage message, SynchronousSink sink) { context.getConnectionId()); } - emitNext(createAuthResponse(lastEnvelopeId, "full authentication"), sink); + emitNext(createAuthResponse("full authentication"), sink); } // Otherwise success, wait until OK message or Error message. } else if (message instanceof ChangeAuthMessage) { ChangeAuthMessage msg = (ChangeAuthMessage) message; - lastEnvelopeId = msg.getEnvelopeId() + 1; + authProvider = MySqlAuthProvider.build(msg.getAuthType()); salt = msg.getSalt(); - emitNext(createAuthResponse(lastEnvelopeId, "change authentication"), sink); + emitNext(createAuthResponse("change authentication"), sink); } else { sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + message.getClass().getSimpleName() + "' in login phase")); @@ -931,15 +940,14 @@ private void emitNext(SubsequenceClientMessage message, SynchronousSink si } } - private AuthResponse createAuthResponse(int envelopeId, String phase) { + private AuthResponse createAuthResponse(String phase) { MySqlAuthProvider authProvider = getAndNextProvider(); if (authProvider.isSslNecessary() && !sslCompleted) { throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), phase), CLI_SPECIFIC); } - return new AuthResponse(envelopeId, - authProvider.authentication(password, salt, context.getClientCollation())); + return new AuthResponse(authProvider.authentication(password, salt, context.getClientCollation())); } private Capability clientCapability(Capability serverCapability) { @@ -947,7 +955,6 @@ private Capability clientCapability(Capability serverCapability) { builder.disableSessionTrack(); builder.disableDatabasePinned(); - builder.disableCompression(); builder.disableIgnoreAmbiguitySpace(); builder.disableInteractiveTimeout(); @@ -970,6 +977,32 @@ private Capability clientCapability(Capability serverCapability) { } } + 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(); } @@ -1011,7 +1044,7 @@ private MySqlAuthProvider getAndNextProvider() { return authProvider; } - private HandshakeResponse createHandshakeResponse(int envelopeId, Capability capability) { + private HandshakeResponse createHandshakeResponse(Capability capability) { MySqlAuthProvider authProvider = getAndNextProvider(); if (authProvider.isSslNecessary() && !sslCompleted) { @@ -1028,13 +1061,31 @@ private HandshakeResponse createHandshakeResponse(int envelopeId, Capability cap authType = MySqlAuthProvider.CACHING_SHA2_PASSWORD; } - return HandshakeResponse.from(envelopeId, capability, context.getClientCollation().getId(), - user, authorization, authType, database, ATTRIBUTES); + return HandshakeResponse.from(capability, context.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 { + Class.forName("com.github.luben.zstd.Zstd", false, + LoginExchangeable.class.getClassLoader()); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } } abstract class AbstractTransactionState { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java b/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java index 43d18fd32..e3a952ee6 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java @@ -22,7 +22,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; /** * An utility for general authentication hashing algorithm. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java index bf6919701..0d070dd00 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java @@ -19,7 +19,7 @@ import io.asyncer.r2dbc.mysql.collation.CharCollation; import org.jetbrains.annotations.Nullable; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java index e00cf8292..52c92b969 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java @@ -21,7 +21,7 @@ import java.nio.CharBuffer; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java index f2a17047c..cc90da4a2 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java @@ -21,7 +21,7 @@ import java.nio.CharBuffer; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java index bf91b5921..6a88fdebd 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java @@ -21,7 +21,7 @@ import java.nio.CharBuffer; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java b/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java new file mode 100644 index 000000000..f71a85ba6 --- /dev/null +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java @@ -0,0 +1,243 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import io.asyncer.r2dbc.mysql.constant.Packets; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; +import org.jetbrains.annotations.Nullable; + +import java.net.SocketAddress; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A codec that compresses and decompresses packets. + *

    + *
  • Read: compression {@link ByteBuf} -> compression-framed {@link ByteBuf} -> + * decompressed {@link ByteBuf}
  • + *
  • Write: uncompressed-framed {@link ByteBuf} -> compression-framed {@link ByteBuf}
  • + *
+ */ +final class CompressionDuplexCodec extends ByteToMessageDecoder implements ChannelOutboundHandler { + + static final String NAME = "R2dbcMysqlCompressionDuplexCodec"; + + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(CompressionDuplexCodec.class); + + private static final int MIN_COMPRESS_LENGTH = 50; + + /** + * Compression packet sequence id, incremented independently of the normal sequence id. + */ + private final AtomicInteger sequenceId = new AtomicInteger(0); + + private final Compressor compressor; + + @Nullable + private ByteBuf writeCumulated; + + private final Cumulator writeCumulator = MERGE_CUMULATOR; + + private int frameLength = -1; + + CompressionDuplexCodec(Compressor compressor) { + this.compressor = compressor; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + if (msg instanceof ByteBuf) { + ByteBuf cumulated = this.writeCumulated == null ? ctx.alloc().buffer(0, 0) : + this.writeCumulated; + + this.writeCumulated = cumulated = writeCumulator.cumulate(ctx.alloc(), cumulated, (ByteBuf) msg); + + while (cumulated.readableBytes() >= Packets.MAX_PAYLOAD_SIZE) { + logger.trace("Accumulated to the maximum payload, compressing"); + + ByteBuf slice = cumulated.readSlice(Packets.MAX_PAYLOAD_SIZE); + ByteBuf compressed = compressor.compress(slice); + + if (compressed.readableBytes() >= slice.readableBytes()) { + logger.trace("Sending uncompressed due to compressed payload is larger than original"); + compressed.release(); + ctx.write(buildHeader(ctx, slice.readableBytes(), 0)); + ctx.write(slice.retain()); + } else { + logger.trace("Sending compressed payload"); + ctx.write(buildHeader(ctx, compressed.readableBytes(), Packets.MAX_PAYLOAD_SIZE)); + ctx.write(compressed); + } + } + + if (!cumulated.isReadable()) { + this.writeCumulated = null; + cumulated.release(); + } else { + logger.trace("Accumulated writing buffers, waiting for flush"); + } + } else { + ctx.write(msg, promise); + } + } + + private ByteBuf buildHeader(ChannelHandlerContext ctx, int compressedSize, int uncompressedSize) { + return ctx.alloc().ioBuffer(Packets.COMPRESS_HEADER_SIZE) + .writeMediumLE(compressedSize) + .writeByte(sequenceId.getAndIncrement()) + .writeMediumLE(uncompressedSize); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ByteBuf cumulated = this.writeCumulated; + + this.writeCumulated = null; + + if (cumulated == null) { + ctx.flush(); + return; + } + + int uncompressedSize = cumulated.readableBytes(); + + if (uncompressedSize < MIN_COMPRESS_LENGTH) { + logger.trace("flushing, payload is too small to compress, sending uncompressed"); + ctx.write(buildHeader(ctx, uncompressedSize, 0)); + ctx.writeAndFlush(cumulated); + } else { + try { + logger.trace("flushing, compressing payload"); + + ByteBuf compressed = compressor.compress(cumulated); + + if (compressed.readableBytes() >= uncompressedSize) { + logger.trace("Sending uncompressed due to compressed payload is larger than original"); + compressed.release(); + ctx.write(buildHeader(ctx, uncompressedSize, 0)); + ctx.writeAndFlush(cumulated.retain()); + } else { + logger.trace("Sending compressed payload"); + ctx.write(buildHeader(ctx, compressed.readableBytes(), uncompressedSize)); + ctx.writeAndFlush(compressed); + } + } finally { + cumulated.release(); + } + } + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + ByteBuf frame = decode(in); + + if (frame != null) { + out.add(frame); + } + } + + @Nullable + private ByteBuf decode(ByteBuf in) { + if (frameLength == -1) { + // New frame + if (in.readableBytes() < Packets.SIZE_FIELD_SIZE) { + return null; + } + + frameLength = in.getUnsignedMediumLE(in.readerIndex()) + Packets.COMPRESS_HEADER_SIZE; + } + + if (in.readableBytes() < frameLength) { + return null; + } + + in.skipBytes(Packets.SIZE_FIELD_SIZE); + + int sequenceId = in.readUnsignedByte(); + int uncompressedSize = in.readUnsignedMediumLE(); + ByteBuf frame = in.readRetainedSlice(frameLength - Packets.COMPRESS_HEADER_SIZE); + + logger.trace("Decoded frame with sequence id: {}, total size: {}, uncompressed size: {}", + sequenceId, frameLength, uncompressedSize); + this.frameLength = -1; + this.sequenceId.set(sequenceId + 1); + + if (uncompressedSize == 0) { + return frame; + } else { + try { + return compressor.decompress(frame, uncompressedSize); + } finally { + frame.release(); + } + } + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (PacketEvent.RESET_SEQUENCE == evt) { + logger.debug("Reset sequence id"); + this.sequenceId.set(0); + } + + ctx.fireUserEventTriggered(evt); + } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, + ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + protected void handlerRemoved0(ChannelHandlerContext ctx) { + this.compressor.dispose(); + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java b/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java new file mode 100644 index 000000000..ee7adbdf1 --- /dev/null +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import io.netty.buffer.ByteBuf; +import reactor.core.Disposable; + +/** + * An abstraction considers to compress and decompress data. + */ +interface Compressor extends Disposable { + + /** + * Compresses the given {@link ByteBuf}. It does not guarantee that the compressed data is smaller than + * the original. It will not change the reader index of the given {@link ByteBuf}. It may return early if + * the compressed data is not smaller than the original. + * + * @param buf the {@link ByteBuf} to compress + * @return the compressed {@link ByteBuf} + */ + ByteBuf compress(ByteBuf buf); + + /** + * Decompresses the given {@link ByteBuf}. + * + * @param buf the {@link ByteBuf} to decompress + * @param uncompressedSize the size of the uncompressed data + * @return the decompressed {@link ByteBuf} + */ + ByteBuf decompress(ByteBuf buf, int uncompressedSize); +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/EnvelopeSlicer.java b/src/main/java/io/asyncer/r2dbc/mysql/client/EnvelopeSlicer.java deleted file mode 100644 index 5e6c2bb51..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/EnvelopeSlicer.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.client; - -import io.asyncer.r2dbc.mysql.constant.Envelopes; -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.codec.LengthFieldBasedFrameDecoder; - -import java.nio.ByteOrder; - -/** - * Slice server message envelope of MySQL protocol. - */ -final class EnvelopeSlicer extends LengthFieldBasedFrameDecoder { - - static final String NAME = "R2dbcMySqlEnvelopeSlicer"; - - EnvelopeSlicer() { - super(ByteOrder.LITTLE_ENDIAN, Envelopes.MAX_ENVELOPE_SIZE + Envelopes.PART_HEADER_SIZE, 0, - Envelopes.SIZE_FIELD_SIZE, - 1, // byte size of sequence Id field - 0, // do NOT strip header - true - ); - } - - /** - * Override this method because {@code ByteBuf.order(order)} will create temporary {@code SwappedByteBuf}, - * and {@code ByteBuf.order(order)} has also been deprecated. - *

- * {@inheritDoc} - */ - @Override - protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) { - if (length != Envelopes.SIZE_FIELD_SIZE || order != ByteOrder.LITTLE_ENDIAN) { - // impossible length or order, only BUG or hack of reflect - throw new DecoderException("Unsupported lengthFieldLength: " + length + - " (only 3) or byteOrder: " + order + " (only LITTLE_ENDIAN)"); - } - - return buf.getUnsignedMediumLE(offset); - } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java b/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java index 1641e480c..09f231b67 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java @@ -17,9 +17,9 @@ package io.asyncer.r2dbc.mysql.client; import io.asyncer.r2dbc.mysql.ConnectionContext; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.asyncer.r2dbc.mysql.internal.util.OperatorUtils; import io.asyncer.r2dbc.mysql.message.client.ClientMessage; -import io.asyncer.r2dbc.mysql.message.client.SubsequenceClientMessage; import io.asyncer.r2dbc.mysql.message.client.PrepareQueryMessage; import io.asyncer.r2dbc.mysql.message.client.PreparedFetchMessage; import io.asyncer.r2dbc.mysql.message.client.SslRequest; @@ -34,51 +34,60 @@ import io.asyncer.r2dbc.mysql.message.server.SyntheticMetadataMessage; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; +import org.jetbrains.annotations.Nullable; import reactor.core.publisher.Flux; +import java.net.SocketAddress; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** - * Client/server messages encode/decode logic. + * A codec that encodes and decodes MySQL messages. + *

    + *
  • Read: {@link ByteBuf} -> framed {@link ByteBuf} -> {@link ServerMessage}
  • + *
  • Write: {@link ClientMessage} -> framed {@link ByteBuf} with last flush
  • + *
*/ -final class MessageDuplexCodec extends ChannelDuplexHandler { +final class MessageDuplexCodec extends ByteToMessageDecoder implements ChannelOutboundHandler { static final String NAME = "R2dbcMySqlMessageDuplexCodec"; private static final InternalLogger logger = InternalLoggerFactory.getInstance(MessageDuplexCodec.class); + private final AtomicInteger sequenceId = new AtomicInteger(0); + private DecodeContext decodeContext = DecodeContext.login(); private final ConnectionContext context; private final ServerMessageDecoder decoder = new ServerMessageDecoder(); + private int frameLength = -1; + MessageDuplexCodec(ConnectionContext context) { this.context = requireNonNull(context, "context must not be null"); } @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) { - if (msg instanceof ByteBuf) { + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) { + ByteBuf frame = decode(in); + + if (frame != null) { DecodeContext context = this.decodeContext; - ServerMessage message = this.decoder.decode((ByteBuf) msg, this.context, context); + ServerMessage message = this.decoder.decode(frame, this.context, context); if (message != null) { - handleDecoded(ctx, message); + handleDecoded(out, message); } - } else if (msg instanceof ServerMessage) { - ctx.fireChannelRead(msg); - } else { - if (logger.isWarnEnabled()) { - logger.warn("Unknown message type {} on reading", msg.getClass()); - } - ReferenceCountUtil.release(msg); } } @@ -86,22 +95,11 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { if (msg instanceof ClientMessage) { ByteBufAllocator allocator = ctx.alloc(); - Flux encoded; + ClientMessage message = (ClientMessage) msg; + Flux encoded = Flux.from(message.encode(allocator, this.context)); - if (msg instanceof SubsequenceClientMessage) { - SubsequenceClientMessage message = (SubsequenceClientMessage) msg; - - encoded = Flux.from(message.encode(allocator, this.context)); - int envelopeId = message.getEnvelopeId(); - - OperatorUtils.envelope(encoded, allocator, envelopeId, false) - .subscribe(new WriteSubscriber(ctx, promise)); - } else { - encoded = Flux.from(((ClientMessage) msg).encode(allocator, this.context)); - - OperatorUtils.envelope(encoded, allocator, 0, true) - .subscribe(new WriteSubscriber(ctx, promise)); - } + OperatorUtils.envelope(encoded, allocator, sequenceId, message.isCumulative()) + .subscribe(new WriteSubscriber(ctx, promise)); if (msg instanceof PrepareQueryMessage) { setDecodeContext(DecodeContext.prepareQuery()); @@ -118,13 +116,74 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) } } + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof PacketEvent) { + switch ((PacketEvent) evt) { + case RESET_SEQUENCE: + logger.trace("Reset sequence id"); + this.sequenceId.set(0); + break; + case USE_COMPRESSION: + logger.trace("Reset sequence id"); + this.sequenceId.set(0); + + if (context.getCapability().isZstdCompression()) { + enableZstdCompression(ctx); + } else if (context.getCapability().isZlibCompression()) { + enableZlibCompression(ctx); + } else { + logger.warn("Unexpected event compression triggered, no capability found"); + } + break; + default: + // Ignore unknown event + break; + } + } + + ctx.fireUserEventTriggered(evt); + } + + @Override + public void flush(ChannelHandlerContext ctx) { + ctx.flush(); + } + @Override public void channelInactive(ChannelHandlerContext ctx) { decoder.dispose(); ctx.fireChannelInactive(); } - private void handleDecoded(ChannelHandlerContext ctx, ServerMessage msg) { + @Nullable + private ByteBuf decode(ByteBuf in) { + if (frameLength == -1) { + // New frame + if (in.readableBytes() < Packets.SIZE_FIELD_SIZE) { + return null; + } + + frameLength = in.getUnsignedMediumLE(in.readerIndex()) + Packets.NORMAL_HEADER_SIZE; + } + + if (in.readableBytes() < frameLength) { + return null; + } + + in.skipBytes(Packets.SIZE_FIELD_SIZE); + + int sequenceId = in.readUnsignedByte(); + ByteBuf frame = in.readRetainedSlice(frameLength - Packets.NORMAL_HEADER_SIZE); + + logger.trace("Decoded frame with sequence id: {}, total size: {}", sequenceId, frameLength); + this.sequenceId.set(sequenceId + 1); + this.frameLength = -1; + + return frame; + } + + private void handleDecoded(List out, ServerMessage msg) { if (msg instanceof ServerStatusMessage) { this.context.setServerStatuses(((ServerStatusMessage) msg).getServerStatuses()); } @@ -159,7 +218,7 @@ private void handleDecoded(ChannelHandlerContext ctx, ServerMessage msg) { } // Generic handle. - ctx.fireChannelRead(msg); + out.add(msg); } private void setDecodeContext(DecodeContext context) { @@ -168,4 +227,59 @@ private void setDecodeContext(DecodeContext context) { logger.debug("Decode context change to {}", context); } } + + @Override + public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, + ChannelPromise promise) { + ctx.bind(localAddress, promise); + } + + @Override + public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, + ChannelPromise promise) { + ctx.connect(remoteAddress, localAddress, promise); + } + + @Override + public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.disconnect(promise); + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.close(promise); + } + + @Override + public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) { + ctx.deregister(promise); + } + + @Override + public void read(ChannelHandlerContext ctx) { + ctx.read(); + } + + private static void enableZstdCompression(ChannelHandlerContext ctx) { + CompressionDuplexCodec handler = new CompressionDuplexCodec( + new ZstdCompressor(3)); + + if (ctx.pipeline().get(CompressionDuplexCodec.NAME) != null) { + logger.warn("Unexpected event, compression already enabled"); + } else { + logger.debug("Compression zstd enabled for subsequent packets"); + ctx.pipeline().addBefore(NAME, CompressionDuplexCodec.NAME, handler); + } + } + + private static void enableZlibCompression(ChannelHandlerContext ctx) { + CompressionDuplexCodec handler = new CompressionDuplexCodec(new ZlibCompressor()); + + if (ctx.pipeline().get(CompressionDuplexCodec.NAME) != null) { + logger.warn("Unexpected event, compression already enabled"); + } else { + logger.debug("Compression zlib enabled for subsequent packets"); + ctx.pipeline().addBefore(NAME, CompressionDuplexCodec.NAME, handler); + } + } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java b/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java new file mode 100644 index 000000000..c8cd53906 --- /dev/null +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +/** + * A packet event considers how the handler should handle subsequent packets. + */ +enum PacketEvent { + + /** + * Sequence is reset, all sequence IDs should be reset to 0. + */ + RESET_SEQUENCE, + + /** + * Compression is enabled, the handler should decode the next packet as a compression packet. + *

+ * It should just reset the normal sequence ID to 0. + */ + USE_COMPRESSION, +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index e1917aacb..8abf17c10 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -95,9 +95,7 @@ final class ReactorNettyClient implements Client { this.context = context; // Note: encoder/decoder should before reactor bridge. - connection.addHandlerLast(EnvelopeSlicer.NAME, new EnvelopeSlicer()) - .addHandlerLast(MessageDuplexCodec.NAME, - new MessageDuplexCodec(context)); + connection.addHandlerLast(MessageDuplexCodec.NAME, new MessageDuplexCodec(context)); if (ssl.getSslMode().startSsl()) { connection.addHandlerFirst(SslBridgeHandler.NAME, new SslBridgeHandler(context, ssl)); @@ -133,6 +131,10 @@ final class ReactorNettyClient implements Client { logger.debug("Request: {}", message); } + if (message.isSequenceReset()) { + resetSequence(connection); + } + return connection.outbound().sendObject(message); }) .onErrorResume(this::resumeError) @@ -250,7 +252,15 @@ public void sslUnsupported() { @Override public void loginSuccess() { - connection.channel().pipeline().fireUserEventTriggered(Lifecycle.COMMAND); + if (context.getCapability().isCompression()) { + connection.channel().pipeline().fireUserEventTriggered(PacketEvent.USE_COMPRESSION); + } else { + resetSequence(connection); + } + } + + private static void resetSequence(Connection connection) { + connection.channel().pipeline().fireUserEventTriggered(PacketEvent.RESET_SEQUENCE); } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java index 952be3917..6ba3f2844 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java @@ -96,7 +96,7 @@ public void handlerAdded(ChannelHandlerContext ctx) { } @Override - public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { if (evt instanceof SslState) { handleSslState(ctx, (SslState) evt); // Ignore event trigger for next handler, because it used only by this handler. @@ -105,7 +105,7 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc handleSslCompleted(ctx, (SslHandshakeCompletionEvent) evt); } - super.userEventTriggered(ctx, evt); + ctx.fireUserEventTriggered(evt); } private void handleSslCompleted(ChannelHandlerContext ctx, SslHandshakeCompletionEvent evt) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java b/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java index cc5e14cbb..ee085cfa3 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java @@ -27,6 +27,11 @@ * streaming {@link ByteBuf}s. *

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

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

+ * Note: uncompressed size is already known, so the buffer should be allocated with the exact size. + */ + private static final int MAX_CHUNK_SIZE = 65536; + + private final Deflater deflater = new Deflater(); + + private final Inflater inflater = new Inflater(); + + @Override + public ByteBuf compress(ByteBuf buf) { + int len = buf.readableBytes(); + + if (len == 0) { + return buf.alloc().buffer(0, 0); + } + + try { + if (buf.hasArray()) { + byte[] input = buf.array(); + int offset = buf.arrayOffset() + buf.readerIndex(); + ByteBuf out = buf.alloc().heapBuffer(Math.min(len, MAX_CHUNK_SIZE)); + + deflater.setInput(input, offset, len); + deflater.finish(); + deflateAll(out, len); + + return out; + } else { + byte[] input = new byte[Math.min(len, MAX_CHUNK_SIZE)]; + int readerIndex = buf.readerIndex(); + int writerIndex = buf.writerIndex(); + ByteBuf out = buf.alloc().heapBuffer(Math.min(len, MAX_CHUNK_SIZE)); + + while (writerIndex - readerIndex > 0) { + int numBytes = Math.min(input.length, writerIndex - readerIndex); + + buf.getBytes(readerIndex, input, 0, numBytes); + deflater.setInput(input, 0, numBytes); + readerIndex += numBytes; + deflateAll(out, len); + } + + deflater.finish(); + deflateAll(out, len); + + return out; + } + } finally { + deflater.reset(); + } + } + + @Override + public ByteBuf decompress(ByteBuf buf, int uncompressedSize) { + int len = buf.readableBytes(); + + if (len == 0) { + return buf.alloc().buffer(0, 0); + } + + try { + if (buf.hasArray()) { + byte[] input = buf.array(); + int offset = buf.arrayOffset() + buf.readerIndex(); + ByteBuf out = buf.alloc().heapBuffer(uncompressedSize); + + inflater.setInput(input, offset, len); + inflateAll(out); + + return out; + } else { + byte[] input = new byte[Math.min(len, MAX_CHUNK_SIZE)]; + + int readerIndex = buf.readerIndex(); + int writerIndex = buf.writerIndex(); + ByteBuf out = buf.alloc().heapBuffer(uncompressedSize); + + while (writerIndex - readerIndex > 0) { + int numBytes = Math.min(input.length, writerIndex - readerIndex); + + buf.getBytes(readerIndex, input, 0, numBytes); + inflater.setInput(input, 0, numBytes); + readerIndex += numBytes; + inflateAll(out); + } + + return out; + } + } catch (DataFormatException e) { + throw new DecoderException("zlib decompress failed", e); + } finally { + inflater.reset(); + } + } + + @Override + public void dispose() { + deflater.end(); + inflater.end(); + } + + private void deflateAll(ByteBuf out, int maxSize) { + while (true) { + deflate(out); + + if (!out.isWritable()) { + int size = out.readableBytes(); + + if (size >= maxSize) { + break; + } + + // Capacity = written size * 2 + if (size > (maxSize >> 1)) { + out.ensureWritable(maxSize - size); + } else { + out.ensureWritable(size); + } + } else if (deflater.needsInput()) { + break; + } + } + } + + private void inflateAll(ByteBuf out) throws DataFormatException { + while (out.isWritable() && !inflater.finished()) { + int wid = out.writerIndex(); + int numBytes = inflater.inflate(out.array(), out.arrayOffset() + wid, out.writableBytes()); + + out.writerIndex(wid + numBytes); + } + } + + private void deflate(ByteBuf out) { + int wid = out.writerIndex(); + int written = deflater.deflate(out.array(), out.arrayOffset() + wid, out.writableBytes()); + + while (written > 0) { + wid += written; + out.writerIndex(wid); + written = deflater.deflate(out.array(), out.arrayOffset() + wid, out.writableBytes()); + } + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java b/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java new file mode 100644 index 000000000..b25c8ed0c --- /dev/null +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import com.github.luben.zstd.Zstd; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; + +import java.nio.ByteBuffer; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; + +/** + * An implementation of {@link Compressor} that uses the Z-standard compression algorithm. + * + * @see Zstandard + */ +final class ZstdCompressor implements Compressor { + + private final int compressionLevel; + + ZstdCompressor(int compressionLevel) { + require( + compressionLevel >= Zstd.minCompressionLevel() && compressionLevel <= Zstd.maxCompressionLevel(), + "compressionLevel must be a value of Z standard compression levels"); + + this.compressionLevel = compressionLevel; + } + + @Override + public ByteBuf compress(ByteBuf buf) { + ByteBuffer buffer = Zstd.compress(buf.nioBuffer(), compressionLevel); + return Unpooled.wrappedBuffer(buffer); + } + + @Override + public ByteBuf decompress(ByteBuf buf, int uncompressedSize) { + ByteBuffer buffer = Zstd.decompress(buf.nioBuffer(), uncompressedSize); + return Unpooled.wrappedBuffer(buffer); + } + + @Override + public void dispose() { + // Do nothing + } +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java b/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java new file mode 100644 index 000000000..05945bf74 --- /dev/null +++ b/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.constant; + +/** + * The compression algorithm for client/server communication. + */ +public enum CompressionAlgorithm { + + /** + * Do not use compression protocol. + */ + UNCOMPRESSED, + + /** + * Use zlib compression algorithm for client/server communication. + *

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

+ * If zstd is not available, the connection will throw an exception when logging in. + */ + ZSTD, +} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/Envelopes.java b/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java similarity index 57% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/Envelopes.java rename to src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java index 0bf5db0fa..b36a9d77f 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/constant/Envelopes.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java @@ -17,11 +17,11 @@ package io.asyncer.r2dbc.mysql.constant; /** - * Constants for MySQL protocol envelopes (e.g. business layer packages). + * Constants for MySQL protocol packets. *

* WARNING: do NOT use it outer than {@literal r2dbc-mysql}. */ -public final class Envelopes { +public final class Packets { /** * The length of the byte size field, it is 3 bytes. @@ -29,19 +29,26 @@ public final class Envelopes { public static final int SIZE_FIELD_SIZE = 3; /** - * The byte size of header part. + * The max bytes size of payload, value is 16777215. (i.e. max value of int24, (2 ** 24) - 1) */ - public static final int PART_HEADER_SIZE = SIZE_FIELD_SIZE + 1; + public static final int MAX_PAYLOAD_SIZE = 0xFFFFFF; /** - * The max bytes size of each envelope, value is 16777215. (i.e. max value of int24, (2 ** 24) - 1) + * The header size of a compression frame, which includes entire frame size (unsigned int24), compression + * sequence id (unsigned int8) and compressed size (unsigned int24). */ - public static final int MAX_ENVELOPE_SIZE = (1 << (SIZE_FIELD_SIZE << 3)) - 1; + public static final int COMPRESS_HEADER_SIZE = SIZE_FIELD_SIZE + 1 + SIZE_FIELD_SIZE; + + /** + * The header size of a normal frame, which includes entire frame size (unsigned int24) and normal + * sequence id (unsigned int8). + */ + public static final int NORMAL_HEADER_SIZE = SIZE_FIELD_SIZE + 1; /** * The terminal of C-style string or C-style binary data. */ public static final byte TERMINAL = 0; - private Envelopes() { } + private Packets() { } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java index 47aa878ed..895b64f27 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.internal.util; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import org.jetbrains.annotations.Nullable; @@ -28,6 +28,8 @@ import reactor.core.publisher.Operators; import reactor.util.context.Context; +import java.util.concurrent.atomic.AtomicInteger; + /** * An implementation of {@link Flux}{@code <}{@link ByteBuf}{@code >} that considers cumulate buffers as * envelopes of the MySQL socket protocol. @@ -38,26 +40,26 @@ final class FluxEnvelope extends FluxOperator { private final int size; - private final int start; + private final AtomicInteger sequenceId; private final boolean cumulate; - FluxEnvelope(Flux source, ByteBufAllocator alloc, int size, int start, + FluxEnvelope(Flux source, ByteBufAllocator alloc, int size, AtomicInteger sequenceId, boolean cumulate) { super(source); this.alloc = alloc; this.size = size; - this.start = start; + this.sequenceId = sequenceId; this.cumulate = cumulate; } @Override public void subscribe(CoreSubscriber actual) { if (cumulate) { - this.source.subscribe(new CumulateEnvelopeSubscriber(actual, alloc, size, start)); + this.source.subscribe(new CumulateEnvelopeSubscriber(actual, alloc, size, sequenceId)); } else { - this.source.subscribe(new DirectEnvelopeSubscriber(actual, alloc, start)); + this.source.subscribe(new DirectEnvelopeSubscriber(actual, alloc, sequenceId)); } } } @@ -68,16 +70,17 @@ final class DirectEnvelopeSubscriber implements CoreSubscriber, Scannab private final ByteBufAllocator alloc; + private final AtomicInteger sequenceId; + private boolean done; private Subscription s; - private int envelopeId; - - DirectEnvelopeSubscriber(CoreSubscriber actual, ByteBufAllocator alloc, int start) { + DirectEnvelopeSubscriber(CoreSubscriber actual, ByteBufAllocator alloc, + AtomicInteger sequenceId) { this.actual = actual; this.alloc = alloc; - this.envelopeId = start; + this.sequenceId = sequenceId; } @Override @@ -97,9 +100,9 @@ public void onNext(ByteBuf buf) { } try { - ByteBuf header = this.alloc.buffer(Envelopes.PART_HEADER_SIZE) + ByteBuf header = this.alloc.ioBuffer(Packets.NORMAL_HEADER_SIZE) .writeMediumLE(buf.readableBytes()) - .writeByte(this.envelopeId++); + .writeByte(this.sequenceId.getAndIncrement()); this.actual.onNext(header); this.actual.onNext(buf); @@ -172,20 +175,20 @@ final class CumulateEnvelopeSubscriber implements CoreSubscriber, Scann private final int size; + private final AtomicInteger sequenceId; + private boolean done; private Subscription s; private ByteBuf cumulated; - private int envelopeId; - CumulateEnvelopeSubscriber(CoreSubscriber actual, ByteBufAllocator alloc, int size, - int start) { + AtomicInteger sequenceId) { this.actual = actual; this.alloc = alloc; this.size = size; - this.envelopeId = start; + this.sequenceId = sequenceId; } @Override @@ -217,9 +220,9 @@ public void onNext(ByteBuf buf) { while (cumulated.readableBytes() >= this.size) { // It will make the cumulated be shared (e.g. refCnt() > 1), that means // the reallocation of the cumulated may not be safe, see cumulate(...). - this.actual.onNext(this.alloc.buffer(Envelopes.PART_HEADER_SIZE) + this.actual.onNext(this.alloc.ioBuffer(Packets.NORMAL_HEADER_SIZE) .writeMediumLE(this.size) - .writeByte(this.envelopeId++)); + .writeByte(this.sequenceId.getAndIncrement())); this.actual.onNext(cumulated.readRetainedSlice(this.size)); } @@ -275,8 +278,8 @@ public void onComplete() { ByteBuf header = null; try { - header = this.alloc.buffer(Envelopes.PART_HEADER_SIZE); - header.writeMediumLE(size).writeByte(this.envelopeId++); + header = this.alloc.ioBuffer(Packets.NORMAL_HEADER_SIZE); + header.writeMediumLE(size).writeByte(this.sequenceId.getAndIncrement()); } catch (Throwable e) { if (cumulated != null) { cumulated.release(); @@ -356,8 +359,8 @@ private static ByteBuf cumulate(ByteBufAllocator alloc, @Nullable ByteBuf cumula int oldBytes = cumulated.readableBytes(); int bufBytes = buf.readableBytes(); int newBytes = oldBytes + bufBytes; - ByteBuf result = releasing = alloc.buffer(alloc.calculateNewCapacity(newBytes, - Integer.MAX_VALUE)); + int newCapacity = alloc.calculateNewCapacity(newBytes, Integer.MAX_VALUE); + ByteBuf result = releasing = alloc.ioBuffer(newCapacity); // Avoid to calling writeBytes(...) with redundancy check and stack depth comparison. result.setBytes(0, cumulated, cumulated.readerIndex(), oldBytes) diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java index 299f82790..a0fdce06b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java @@ -16,12 +16,14 @@ package io.asyncer.r2dbc.mysql.internal.util; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import reactor.core.Fuseable; import reactor.core.publisher.Flux; +import java.util.concurrent.atomic.AtomicInteger; + import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -56,12 +58,12 @@ public static Flux discardOnCancel(Flux source) { } public static Flux envelope(Flux source, ByteBufAllocator allocator, - int envelopeIdStart, boolean cumulate) { + AtomicInteger sequenceId, boolean cumulate) { requireNonNull(source, "source must not be null"); requireNonNull(allocator, "allocator must not be null"); + requireNonNull(sequenceId, "sequenceId must not be null"); - return new FluxEnvelope(source, allocator, Envelopes.MAX_ENVELOPE_SIZE, - envelopeIdStart & 0xFF, cumulate); + return new FluxEnvelope(source, allocator, Packets.MAX_PAYLOAD_SIZE, sequenceId, cumulate); } private OperatorUtils() { } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java index 1c72a49df..0d8e89294 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java @@ -80,7 +80,7 @@ private void tryRead() { } private void read() { - ByteBuf buf = this.allocator.buffer(this.bufferSize); + ByteBuf buf = this.allocator.ioBuffer(this.bufferSize); ByteBuffer byteBuffer = buf.nioBuffer(buf.writerIndex(), buf.writableBytes()); this.channel.read(byteBuffer, this.position.get(), buf, this); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java index 4c6d2b1de..3aa96cd70 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java @@ -28,20 +28,12 @@ */ public final class AuthResponse extends SizedClientMessage implements SubsequenceClientMessage { - private final int envelopeId; - private final byte[] authentication; - public AuthResponse(int envelopeId, byte[] authentication) { - this.envelopeId = envelopeId; + public AuthResponse(byte[] authentication) { this.authentication = requireNonNull(authentication, "authentication must not be null"); } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override protected int size() { return authentication.length; @@ -58,17 +50,17 @@ public boolean equals(Object o) { AuthResponse that = (AuthResponse) o; - return envelopeId == that.envelopeId && Arrays.equals(authentication, that.authentication); + return Arrays.equals(authentication, that.authentication); } @Override public int hashCode() { - return 31 * envelopeId + Arrays.hashCode(authentication); + return Arrays.hashCode(authentication); } @Override public String toString() { - return "AuthResponse{envelopeId=" + envelopeId + ", authentication=REDACTED}"; + return "AuthResponse{authentication=REDACTED}"; } @Override diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java index 3080da66f..7d969d376 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java @@ -27,6 +27,19 @@ */ public interface ClientMessage { + default boolean isSequenceReset() { + return true; + } + + /** + * Returns whether the encoded buffers can be cumulated to maximize the payload size. + * + * @return {@code true} if can be cumulated. + */ + default boolean isCumulative() { + return true; + } + /** * Encode a message into {@link ByteBuf}s. * diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java index ca05f9831..c7b135a34 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java @@ -22,7 +22,7 @@ import java.nio.charset.Charset; import java.util.Map; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; /** * An abstraction of {@link SubsequenceClientMessage} considers handshake response. @@ -33,24 +33,24 @@ public interface HandshakeResponse extends SubsequenceClientMessage { * Construct an instance of {@link HandshakeResponse}, it is implemented by the protocol version that is * given by {@link Capability}. * - * @param envelopeId the beginning envelope ID of this message. - * @param capability the current {@link Capability}. - * @param collationId the {@code CharCollation} ID, or 0 if server does not return a collation ID. - * @param user the username for login. - * @param authentication the password authentication for login. - * @param authType the authentication plugin type. - * @param database the connecting database, may be empty. - * @param attributes the connecting attributes. + * @param capability the current {@link Capability}. + * @param collationId the {@code CharCollation} ID, or 0 if server does not return. + * @param user the username for login. + * @param authentication the password authentication for login. + * @param authType the authentication plugin type. + * @param database the connecting database, may be empty. + * @param attributes the connecting attributes. + * @param zstdCompressionLevel the Zstd compression level. * @return the instance implemented by the specified protocol version. */ - static HandshakeResponse from(int envelopeId, Capability capability, int collationId, String user, - byte[] authentication, String authType, String database, Map attributes) { + static HandshakeResponse from(Capability capability, int collationId, String user, byte[] authentication, + String authType, String database, Map attributes, int zstdCompressionLevel) { if (capability.isProtocol41()) { - return new HandshakeResponse41(envelopeId, capability, collationId, user, authentication, - authType, database, attributes); + return new HandshakeResponse41(capability, collationId, user, authentication, authType, database, + attributes, zstdCompressionLevel); } - return new HandshakeResponse320(envelopeId, capability, user, authentication, database); + return new HandshakeResponse320(capability, user, authentication, database); } /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java index c93705ea6..e3547faeb 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java @@ -23,7 +23,7 @@ import java.nio.charset.Charset; import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -41,19 +41,14 @@ final class HandshakeResponse320 extends ScalarClientMessage implements Handshak private final String database; - HandshakeResponse320(int envelopeId, Capability capability, String user, byte[] authentication, + HandshakeResponse320(Capability capability, String user, byte[] authentication, String database) { - this.header = new SslRequest320(envelopeId, capability); + this.header = new SslRequest320(capability); this.user = requireNonNull(user, "user must not be null"); this.authentication = requireNonNull(authentication, "authentication must not be null"); this.database = requireNonNull(database, "database must not be null"); } - @Override - public int getEnvelopeId() { - return header.getEnvelopeId(); - } - @Override public boolean equals(Object o) { if (this == o) { @@ -79,8 +74,7 @@ public int hashCode() { @Override public String toString() { - return "HandshakeResponse320{envelopeId=" + header.getEnvelopeId() + - ", capability=" + header.getCapability() + ", user='" + user + + return "HandshakeResponse320{capability=" + header.getCapability() + ", user='" + user + "', authentication=REDACTED, database='" + database + "'}"; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java index f727364f5..810126bba 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java @@ -49,21 +49,17 @@ final class HandshakeResponse41 extends ScalarClientMessage implements Handshake private final Map attributes; - // private final byte zStdCompressionLevel; // When Z-Standard compression supporting + private final int zstdCompressionLevel; - HandshakeResponse41(int envelopeId, Capability capability, int collationId, String user, - byte[] authentication, String authType, String database, Map attributes) { - this.header = new SslRequest41(envelopeId, capability, collationId); + HandshakeResponse41(Capability capability, int collationId, String user, byte[] authentication, + String authType, String database, Map attributes, int zstdCompressionLevel) { + this.header = new SslRequest41(capability, collationId); this.user = requireNonNull(user, "user must not be null"); this.authentication = requireNonNull(authentication, "authentication must not be null"); this.database = requireNonNull(database, "database must not be null"); this.authType = requireNonNull(authType, "authType must not be null"); this.attributes = requireNonNull(attributes, "attributes must not be null"); - } - - @Override - public int getEnvelopeId() { - return header.getEnvelopeId(); + this.zstdCompressionLevel = zstdCompressionLevel; } @Override @@ -71,15 +67,16 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof HandshakeResponse41)) { return false; } HandshakeResponse41 that = (HandshakeResponse41) o; - return header.equals(that.header) && user.equals(that.user) && - Arrays.equals(authentication, that.authentication) && authType.equals(that.authType) && - database.equals(that.database) && attributes.equals(that.attributes); + return zstdCompressionLevel == that.zstdCompressionLevel && header.equals(that.header) && + user.equals(that.user) && Arrays.equals(authentication, that.authentication) && + authType.equals(that.authType) && database.equals(that.database) && + attributes.equals(that.attributes); } @Override @@ -89,16 +86,18 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(authentication); result = 31 * result + authType.hashCode(); result = 31 * result + database.hashCode(); - return 31 * result + attributes.hashCode(); + result = 31 * result + attributes.hashCode(); + return 31 * result + zstdCompressionLevel; } @Override public String toString() { - return "HandshakeResponse41{envelopeId=" + header.getEnvelopeId() + - ", capability=" + header.getCapability() + + return "HandshakeResponse41{capability=" + header.getCapability() + ", collationId=" + header.getCollationId() + ", user='" + user + "', authentication=REDACTED, authType='" + authType + - "', database='" + database + "', attributes=" + attributes + '}'; + "', database='" + database + "', attributes=" + attributes + + ", zstdCompressionLevel=" + zstdCompressionLevel + + '}'; } @Override @@ -131,6 +130,10 @@ protected void writeTo(ByteBuf buf, ConnectionContext context) { if (capability.isConnectionAttributesAllowed()) { writeAttrs(buf, charset); } + + if (capability.isZstdCompression()) { + buf.writeByte(zstdCompressionLevel); + } } private void writeAttrs(ByteBuf buf, Charset charset) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java index c9ca2419f..63a360f49 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java @@ -38,20 +38,22 @@ */ public final class LocalInfileResponse implements SubsequenceClientMessage { - private final int envelopeId; - private final String path; private final SynchronousSink errorSink; - public LocalInfileResponse(int envelopeId, String path, SynchronousSink errorSink) { + public LocalInfileResponse(String path, SynchronousSink errorSink) { requireNonNull(path, "path must not be null"); - this.envelopeId = envelopeId; this.path = path; this.errorSink = errorSink; } + @Override + public boolean isCumulative() { + return false; + } + @Override public Flux encode(ByteBufAllocator allocator, ConnectionContext context) { return Flux.defer(() -> { @@ -93,11 +95,6 @@ public Flux encode(ByteBufAllocator allocator, ConnectionContext contex }); } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -109,17 +106,16 @@ public boolean equals(Object o) { LocalInfileResponse that = (LocalInfileResponse) o; - return envelopeId == that.envelopeId && path.equals(that.path); + return path.equals(that.path); } @Override public int hashCode() { - return 31 * envelopeId + path.hashCode(); + return path.hashCode(); } @Override public String toString() { - return "LocalInfileResponse{envelopeId=" + envelopeId + - ", path='" + path + "'}"; + return "LocalInfileResponse{path='" + path + "'}"; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java index 16420a412..81cf4eebf 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java @@ -36,18 +36,17 @@ public interface SslRequest extends SubsequenceClientMessage { * Construct an instance of {@link SslRequest}, it is implemented by the protocol version that is given by * {@link Capability}. * - * @param envelopeId the beginning envelope ID of this message. * @param capability the current {@link Capability}. * @param collationId the {@code CharCollation} ID, or 0 if server does not return a collation ID. * @return the instance implemented by the specified protocol version. */ - static SslRequest from(int envelopeId, Capability capability, int collationId) { + static SslRequest from(Capability capability, int collationId) { require(capability.isSslEnabled(), "capability must be SSL enabled"); if (capability.isProtocol41()) { - return new SslRequest41(envelopeId, capability, collationId); + return new SslRequest41(capability, collationId); } - return new SslRequest320(envelopeId, capability); + return new SslRequest320(capability); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java index 096b2c46e..216476189 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.client; import io.asyncer.r2dbc.mysql.Capability; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; @@ -27,24 +27,16 @@ */ final class SslRequest320 extends SizedClientMessage implements SslRequest { - private static final int SIZE = Short.BYTES + Envelopes.SIZE_FIELD_SIZE; - - private final int envelopeId; + private static final int SIZE = Short.BYTES + Packets.SIZE_FIELD_SIZE; private final Capability capability; - SslRequest320(int envelopeId, Capability capability) { + SslRequest320(Capability capability) { require(!capability.isProtocol41(), "protocol 4.1 capability should never be set"); - this.envelopeId = envelopeId; this.capability = capability; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public Capability getCapability() { return capability; @@ -61,18 +53,17 @@ public boolean equals(Object o) { SslRequest320 that = (SslRequest320) o; - return envelopeId == that.envelopeId && capability.equals(that.capability); + return capability.equals(that.capability); } @Override public int hashCode() { - return 31 * envelopeId + capability.hashCode(); + return capability.hashCode(); } @Override public String toString() { - return "SslRequest320{envelopeId=" + envelopeId + - ", capability=" + capability + '}'; + return "SslRequest320{capability=" + capability + '}'; } @Override @@ -84,6 +75,6 @@ protected int size() { protected void writeTo(ByteBuf buf) { // Protocol 3.20 only allows low 16-bits capabilities. buf.writeShortLE(capability.getBaseBitmap() & 0xFFFF) - .writeMediumLE(Envelopes.MAX_ENVELOPE_SIZE); + .writeMediumLE(Packets.MAX_PAYLOAD_SIZE); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java index bca7e099c..2e270ae29 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.client; import io.asyncer.r2dbc.mysql.Capability; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; @@ -34,25 +34,17 @@ final class SslRequest41 extends SizedClientMessage implements SslRequest { private static final int BUF_SIZE = Integer.BYTES + Integer.BYTES + Byte.BYTES + RESERVED_SIZE + MARIA_DB_CAPABILITY_SIZE; - private final int envelopeId; - private final Capability capability; private final int collationId; - SslRequest41(int envelopeId, Capability capability, int collationId) { + SslRequest41(Capability capability, int collationId) { require(collationId > 0, "collationId must be a positive integer"); - this.envelopeId = envelopeId; this.capability = capability; this.collationId = collationId; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public boolean equals(Object o) { if (this == o) { @@ -64,22 +56,19 @@ public boolean equals(Object o) { SslRequest41 that = (SslRequest41) o; - return envelopeId == that.envelopeId && - collationId == that.collationId && + return collationId == that.collationId && capability.equals(that.capability); } @Override public int hashCode() { - int result = 31 * envelopeId + capability.hashCode(); + int result = capability.hashCode(); return 31 * result + collationId; } @Override public String toString() { - return "SslRequest41{envelopeId=" + envelopeId + - ", capability=" + capability + - ", collationId=" + collationId + '}'; + return "SslRequest41{capability=" + capability + ", collationId=" + collationId + '}'; } @Override @@ -95,7 +84,7 @@ protected int size() { @Override protected void writeTo(ByteBuf buf) { buf.writeIntLE(capability.getBaseBitmap()) - .writeIntLE(Envelopes.MAX_ENVELOPE_SIZE) + .writeIntLE(Packets.MAX_PAYLOAD_SIZE) .writeByte(collationId & 0xFF); // only low 8-bits if (capability.isMariaDb()) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java index 091e7a57b..fb2364b92 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java @@ -24,10 +24,8 @@ */ public interface SubsequenceClientMessage extends ClientMessage { - /** - * Gets the current envelope ID used to serialize subsequent request messages. - * - * @return the current envelope ID. - */ - int getEnvelopeId(); + @Override + default boolean isSequenceReset() { + return false; + } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java index 543cbb8e3..ba987287e 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java @@ -25,27 +25,20 @@ public final class AuthMoreDataMessage implements ServerMessage { private static final byte AUTH_SUCCEED = 3; - private final int envelopeId; - private final boolean failed; - private AuthMoreDataMessage(int envelopeId, boolean failed) { - this.envelopeId = envelopeId; + private AuthMoreDataMessage(boolean failed) { this.failed = failed; } - public int getEnvelopeId() { - return envelopeId; - } - public boolean isFailed() { return failed; } - static AuthMoreDataMessage decode(int envelopeId, ByteBuf buf) { + static AuthMoreDataMessage decode(ByteBuf buf) { buf.skipBytes(1); // auth more data message header, 0x01 - return new AuthMoreDataMessage(envelopeId, buf.readByte() != AUTH_SUCCEED); + return new AuthMoreDataMessage(buf.readByte() != AUTH_SUCCEED); } @Override @@ -59,16 +52,16 @@ public boolean equals(Object o) { AuthMoreDataMessage that = (AuthMoreDataMessage) o; - return envelopeId == that.envelopeId && failed == that.failed; + return failed == that.failed; } @Override public int hashCode() { - return (envelopeId << 1) | (failed ? 1 : 0); + return (failed ? 1 : 0); } @Override public String toString() { - return "AuthMoreDataMessage{envelopeId=" + envelopeId + ", failed=" + failed + '}'; + return "AuthMoreDataMessage{failed=" + failed + '}'; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java index dc3a1a142..ba1d38479 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java @@ -21,7 +21,7 @@ import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -29,22 +29,15 @@ */ public final class ChangeAuthMessage implements ServerMessage { - private final int envelopeId; - private final String authType; private final byte[] salt; - private ChangeAuthMessage(int envelopeId, String authType, byte[] salt) { - this.envelopeId = envelopeId; + private ChangeAuthMessage(String authType, byte[] salt) { this.authType = requireNonNull(authType, "authType must not be null"); this.salt = requireNonNull(salt, "salt must not be null"); } - public int getEnvelopeId() { - return envelopeId; - } - public String getAuthType() { return authType; } @@ -58,29 +51,26 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof ChangeAuthMessage)) { return false; } ChangeAuthMessage that = (ChangeAuthMessage) o; - return envelopeId == that.envelopeId && authType.equals(that.authType) && - Arrays.equals(salt, that.salt); + return authType.equals(that.authType) && Arrays.equals(salt, that.salt); } @Override public int hashCode() { - int result = envelopeId; - result = 31 * result + authType.hashCode(); - return 31 * result + Arrays.hashCode(salt); + return 31 * authType.hashCode() + Arrays.hashCode(salt); } @Override public String toString() { - return "ChangeAuthMessage{envelopeId=" + envelopeId + ", authType='" + authType + "', salt=REDACTED}"; + return "ChangeAuthMessage{authType='" + authType + "', salt=REDACTED}"; } - static ChangeAuthMessage decode(int envelopeId, ByteBuf buf) { + static ChangeAuthMessage decode(ByteBuf buf) { buf.skipBytes(1); // skip generic header 0xFE of change authentication messages String authType = HandshakeHeader.readCStringAscii(buf); @@ -90,6 +80,6 @@ static ChangeAuthMessage decode(int envelopeId, ByteBuf buf) { ByteBufUtil.getBytes(buf); // The terminal character has been removed from salt. - return new ChangeAuthMessage(envelopeId, authType, salt); + return new ChangeAuthMessage(authType, salt); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java index 6ad92fea9..6785a930f 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java @@ -21,7 +21,7 @@ import java.nio.charset.StandardCharsets; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java index 81cb22eb1..eed96afca 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java @@ -32,13 +32,6 @@ public interface HandshakeRequest extends ServerMessage { */ HandshakeHeader getHeader(); - /** - * Get the envelope identifier of this message packet. - * - * @return envelope identifier. - */ - int getEnvelopeId(); - /** * Get the server-side capability. * @@ -63,19 +56,18 @@ public interface HandshakeRequest extends ServerMessage { /** * Decode a {@link HandshakeRequest} from a envelope {@link ByteBuf}. * - * @param envelopeId envelope identifier. - * @param buf the {@link ByteBuf}. + * @param buf the {@link ByteBuf}. * @return decoded {@link HandshakeRequest}. */ - static HandshakeRequest decode(int envelopeId, ByteBuf buf) { + static HandshakeRequest decode(ByteBuf buf) { HandshakeHeader header = HandshakeHeader.decode(buf); int version = header.getProtocolVersion(); switch (version) { case 10: - return HandshakeV10Request.decode(envelopeId, buf, header); + return HandshakeV10Request.decode(buf, header); case 9: - return HandshakeV9Request.decode(envelopeId, buf, header); + return HandshakeV9Request.decode(buf, header); } throw new R2dbcPermissionDeniedException("Does not support handshake protocol version " + version); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java index 47cd0bdc4..5f5a1de67 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java @@ -24,7 +24,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** @@ -42,8 +42,6 @@ final class HandshakeV10Request implements HandshakeRequest, ServerStatusMessage private final HandshakeHeader header; - private final int envelopeId; - private final byte[] salt; private final Capability serverCapability; @@ -52,10 +50,9 @@ final class HandshakeV10Request implements HandshakeRequest, ServerStatusMessage private final String authType; - private HandshakeV10Request(HandshakeHeader header, int envelopeId, byte[] salt, + private HandshakeV10Request(HandshakeHeader header, byte[] salt, Capability serverCapability, short serverStatuses, String authType) { this.header = requireNonNull(header, "header must not be null"); - this.envelopeId = envelopeId; this.salt = requireNonNull(salt, "salt must not be null"); this.serverCapability = requireNonNull(serverCapability, "serverCapability must not be null"); this.serverStatuses = serverStatuses; @@ -67,11 +64,6 @@ public HandshakeHeader getHeader() { return header; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public byte[] getSalt() { return salt; @@ -97,35 +89,35 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof HandshakeV10Request)) { return false; } HandshakeV10Request that = (HandshakeV10Request) o; - return envelopeId == that.envelopeId && serverStatuses == that.serverStatuses && - header.equals(that.header) && Arrays.equals(salt, that.salt) && - serverCapability.equals(that.serverCapability) && authType.equals(that.authType); + return serverStatuses == that.serverStatuses && header.equals(that.header) && + Arrays.equals(salt, that.salt) && serverCapability.equals(that.serverCapability) && + authType.equals(that.authType); } @Override public int hashCode() { - int hash = 31 * header.hashCode() + envelopeId; - hash = 31 * hash + Arrays.hashCode(salt); - hash = 31 * hash + serverCapability.hashCode(); - hash = 31 * hash + serverStatuses; - return 31 * hash + authType.hashCode(); + int result = header.hashCode(); + result = 31 * result + Arrays.hashCode(salt); + result = 31 * result + serverCapability.hashCode(); + result = 31 * result + (int) serverStatuses; + return 31 * result + authType.hashCode(); } @Override public String toString() { - return "HandshakeV10Request{header=" + header + ", envelopeId=" + envelopeId + + return "HandshakeV10Request{header=" + header + ", salt=REDACTED, serverCapability=" + serverCapability + ", serverStatuses=" + serverStatuses + ", authType='" + authType + "'}"; } - static HandshakeV10Request decode(int envelopeId, ByteBuf buf, HandshakeHeader header) { - Builder builder = new Builder(envelopeId, header); + static HandshakeV10Request decode(ByteBuf buf, HandshakeHeader header) { + Builder builder = new Builder(header); ByteBuf salt = buf.alloc().buffer(); try { @@ -194,8 +186,6 @@ static HandshakeV10Request decode(int envelopeId, ByteBuf buf, HandshakeHeader h private static final class Builder { - private final int envelopeId; - private final HandshakeHeader header; private String authType; @@ -206,14 +196,12 @@ private static final class Builder { private short serverStatuses; - private Builder(int envelopeId, HandshakeHeader header) { - this.envelopeId = envelopeId; + private Builder(HandshakeHeader header) { this.header = header; } HandshakeV10Request build() { - return new HandshakeV10Request(header, envelopeId, salt, serverCapability, serverStatuses, - authType); + return new HandshakeV10Request(header, salt, serverCapability, serverStatuses, authType); } void authType(String authType) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java index ea34e1c55..b92d29256 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java @@ -23,7 +23,7 @@ import java.util.Arrays; -import static io.asyncer.r2dbc.mysql.constant.Envelopes.TERMINAL; +import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_BYTES; @@ -36,13 +36,10 @@ final class HandshakeV9Request implements HandshakeRequest { private final HandshakeHeader header; - private final int envelopeId; - private final byte[] salt; - private HandshakeV9Request(HandshakeHeader header, int envelopeId, byte[] salt) { + private HandshakeV9Request(HandshakeHeader header, byte[] salt) { this.header = requireNonNull(header, "header must not be null"); - this.envelopeId = envelopeId; this.salt = requireNonNull(salt, "salt must not be null"); } @@ -51,11 +48,6 @@ public HandshakeHeader getHeader() { return header; } - @Override - public int getEnvelopeId() { - return envelopeId; - } - @Override public Capability getServerCapability() { return SERVER_CAPABILITY; @@ -76,31 +68,31 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof HandshakeV9Request)) { return false; } HandshakeV9Request that = (HandshakeV9Request) o; - return envelopeId == that.envelopeId && header.equals(that.header) && Arrays.equals(salt, that.salt); + return header.equals(that.header) && Arrays.equals(salt, that.salt); } @Override public int hashCode() { - int hash = 31 * header.hashCode() + envelopeId; - return 31 * hash + Arrays.hashCode(salt); + int result = header.hashCode(); + return 31 * result + Arrays.hashCode(salt); } @Override public String toString() { - return "HandshakeV9Request{header=" + header + ", envelopeId=" + envelopeId + ", salt=REDACTED}"; + return "HandshakeV9Request{header=" + header + ", salt=REDACTED}"; } - static HandshakeV9Request decode(int envelopeId, ByteBuf buf, HandshakeHeader header) { + static HandshakeV9Request decode(ByteBuf buf, HandshakeHeader header) { int bytes = buf.readableBytes(); if (bytes <= 0) { - return new HandshakeV9Request(header, envelopeId, EMPTY_BYTES); + return new HandshakeV9Request(header, EMPTY_BYTES); } byte[] salt; @@ -111,6 +103,6 @@ static HandshakeV9Request decode(int envelopeId, ByteBuf buf, HandshakeHeader he salt = ByteBufUtil.getBytes(buf); } - return new HandshakeV9Request(header, envelopeId, salt); + return new HandshakeV9Request(header, salt); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java index 9842687b1..c649f99bd 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.message.server; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.asyncer.r2dbc.mysql.message.FieldValue; @@ -136,7 +136,7 @@ protected void deallocate() { private List readSlice(ByteBuf current, long length) { ByteBuf buf = current; List results = new ArrayList<>(Math.max( - (int) Math.min(Long.divideUnsigned(length, Envelopes.MAX_ENVELOPE_SIZE) + 2, Byte.MAX_VALUE), + (int) Math.min(Long.divideUnsigned(length, Packets.MAX_PAYLOAD_SIZE) + 2, Byte.MAX_VALUE), 10 )); long totalSize = 0; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java index e493e2bf8..433059f1b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java @@ -25,26 +25,19 @@ */ public final class LocalInfileRequest implements ServerMessage { - private final int envelopeId; - private final String path; - private LocalInfileRequest(int envelopeId, String path) { - this.envelopeId = envelopeId; + private LocalInfileRequest(String path) { this.path = path; } - public int getEnvelopeId() { - return envelopeId; - } - public String getPath() { return path; } - static LocalInfileRequest decode(int envelopeId, ByteBuf buf, ConnectionContext context) { + static LocalInfileRequest decode(ByteBuf buf, ConnectionContext context) { buf.skipBytes(1); // Constant 0xFB - return new LocalInfileRequest(envelopeId, buf.toString(context.getClientCollation().getCharset())); + return new LocalInfileRequest(buf.toString(context.getClientCollation().getCharset())); } @Override @@ -58,17 +51,16 @@ public boolean equals(Object o) { LocalInfileRequest that = (LocalInfileRequest) o; - return envelopeId == that.envelopeId && path.equals(that.path); + return path.equals(that.path); } @Override public int hashCode() { - return 31 * envelopeId + path.hashCode(); + return path.hashCode(); } @Override public String toString() { - return "LocalInfileRequest{envelopeId=" + envelopeId + - ", path='" + path + "'}"; + return "LocalInfileRequest{path='" + path + "'}"; } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java index f81a06200..1c8577ab9 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.server; import io.asyncer.r2dbc.mysql.ConnectionContext; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -58,24 +58,25 @@ public final class ServerMessageDecoder { /** * Decode a server-side message from {@link #parts} and current envelope. * - * @param envelope the current envelope. + * @param payload the payload of the current packet. * @param context the connection context. * @param decodeContext the decode context. * @return the server-side message, or {@code null} if {@code envelope} is not last packet. */ @Nullable - public ServerMessage decode(ByteBuf envelope, ConnectionContext context, DecodeContext decodeContext) { - requireNonNull(envelope, "envelope must not be null"); + public ServerMessage decode(ByteBuf payload, ConnectionContext context, DecodeContext decodeContext) { + requireNonNull(payload, "payload must not be null"); requireNonNull(context, "context must not be null"); requireNonNull(decodeContext, "decodeContext must not be null"); - List buffers = this.parts; - Byte id = readNotFinish(buffers, envelope); - if (id == null) { + parts.add(payload); + + if (payload.readableBytes() == Packets.MAX_PAYLOAD_SIZE) { + // Not last packet. return null; } - return decodeMessage(buffers, id.intValue() & 0xFF, context, decodeContext); + return decodeMessage(parts, context, decodeContext); } /** @@ -91,8 +92,8 @@ public void dispose() { } @Nullable - private static ServerMessage decodeMessage(List buffers, int envelopeId, - ConnectionContext context, DecodeContext decodeContext) { + private static ServerMessage decodeMessage(List buffers, ConnectionContext context, + DecodeContext decodeContext) { if (decodeContext instanceof ResultDecodeContext) { return decodeResult(buffers, context, (ResultDecodeContext) decodeContext); } @@ -104,14 +105,14 @@ private static ServerMessage decodeMessage(List buffers, int envelopeId try { if (decodeContext instanceof CommandDecodeContext) { - return decodeCommandMessage(envelopeId, combined, context); + return decodeCommandMessage(combined, context); } else if (decodeContext instanceof PreparedMetadataDecodeContext) { return decodePreparedMetadata(combined, context, (PreparedMetadataDecodeContext) decodeContext); } else if (decodeContext instanceof PrepareQueryDecodeContext) { return decodePrepareQuery(combined); } else if (decodeContext instanceof LoginDecodeContext) { - return decodeLogin(envelopeId, combined, context); + return decodeLogin(combined, context); } } finally { combined.release(); @@ -194,8 +195,7 @@ private static ServerMessage decodePrepareQuery(ByteBuf buf) { " on prepare query phase"); } - private static ServerMessage decodeCommandMessage(int envelopeId, ByteBuf buf, - ConnectionContext context) { + private static ServerMessage decodeCommandMessage(ByteBuf buf, ConnectionContext context) { short header = buf.getUnsignedByte(buf.readerIndex()); switch (header) { case ERROR: @@ -221,8 +221,9 @@ private static ServerMessage decodeCommandMessage(int envelopeId, ByteBuf buf, } case LOCAL_INFILE: if (buf.readableBytes() > 1) { - return LocalInfileRequest.decode(envelopeId, buf, context); + return LocalInfileRequest.decode(buf, context); } + break; } if (VarIntUtils.checkNextVarInt(buf) == 0) { @@ -236,7 +237,7 @@ private static ServerMessage decodeCommandMessage(int envelopeId, ByteBuf buf, " on command phase"); } - private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, ConnectionContext context) { + private static ServerMessage decodeLogin(ByteBuf buf, ConnectionContext context) { short header = buf.getUnsignedByte(buf.readerIndex()); switch (header) { case OK: @@ -246,10 +247,10 @@ private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, Connection break; case AUTH_MORE_DATA: // Auth more data - return AuthMoreDataMessage.decode(envelopeId, buf); + return AuthMoreDataMessage.decode(buf); case HANDSHAKE_V9: case HANDSHAKE_V10: // Handshake V9 (not supported) or V10 - return HandshakeRequest.decode(envelopeId, buf); + return HandshakeRequest.decode(buf); case ERROR: // Error return ErrorMessage.decode(buf); case EOF: // Auth exchange message or EOF message @@ -257,7 +258,7 @@ private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, Connection return EofMessage.decode(buf); } - return ChangeAuthMessage.decode(envelopeId, buf); + return ChangeAuthMessage.decode(buf); } throw new R2dbcPermissionDeniedException("Unknown message header 0x" + @@ -265,32 +266,6 @@ private static ServerMessage decodeLogin(int envelopeId, ByteBuf buf, Connection " on connection phase"); } - @Nullable - private static Byte readNotFinish(List buffers, ByteBuf envelope) { - try { - int size = envelope.readUnsignedMediumLE(); - if (size < Envelopes.MAX_ENVELOPE_SIZE) { - Byte envelopeId = envelope.readByte(); - - buffers.add(envelope); - // success, no need release - envelope = null; - return envelopeId; - } - - // skip the sequence Id - envelope.skipBytes(1); - buffers.add(envelope); - // success, no need release - envelope = null; - return null; - } finally { - if (envelope != null) { - envelope.release(); - } - } - } - private static boolean isRow(List buffers, ByteBuf firstBuf, short header) { switch (header) { case RowMessage.NULL_VALUE: diff --git a/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java b/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java new file mode 100644 index 000000000..3e7c5bdb7 --- /dev/null +++ b/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Base class for compression integration tests. + */ +abstract class CompressionIntegrationTestSupport extends IntegrationTestSupport { + + CompressionIntegrationTestSupport(CompressionAlgorithm algorithm) { + super(configuration(builder -> builder.compressionAlgorithms(algorithm))); + } + + @Test + void simpleQuery() { + byte[] hello = "Hello".getBytes(StandardCharsets.US_ASCII); + byte[] repeatedBytes = new byte[hello.length * 50]; + + for (int i = 0; i < 50; i++) { + System.arraycopy(hello, 0, repeatedBytes, i * hello.length, hello.length); + } + + String repeated = new String(repeatedBytes, StandardCharsets.US_ASCII); + + complete(connection -> connection.createStatement("SELECT REPEAT('Hello', 50)").execute() + .flatMap(result -> result.map((row, rowMetadata) -> row.get(0, String.class))) + .collectList() + .doOnNext(actual -> assertThat(actual).isEqualTo(Collections.singletonList(repeated)))); + } + + @ParameterizedTest + @ValueSource(strings = { "stations", "users" }) + @SuppressWarnings("SqlSourceToSinkFlow") + void loadDataLocalInfile(String name) throws URISyntaxException, IOException { + URL tdlUrl = Objects.requireNonNull(getClass().getResource(String.format("/local/%s.sql", name))); + URL csvUrl = Objects.requireNonNull(getClass().getResource(String.format("/local/%s.csv", name))); + String tdl = new String(Files.readAllBytes(Paths.get(tdlUrl.toURI())), StandardCharsets.UTF_8); + String path = Paths.get(csvUrl.toURI()).toString(); + String loadData = String.format("LOAD DATA LOCAL INFILE '%s' INTO TABLE `%s` " + + "FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'", path, name); + String select = String.format("SELECT * FROM `%s` ORDER BY `id`", name); + AtomicInteger count = new AtomicInteger(-1); + + complete(conn -> conn.createStatement(tdl) + .execute() + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(conn.createStatement(loadData).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .reduce(0L, Long::sum) + .doOnNext(it -> count.set(it.intValue())) + .doOnNext(it -> assertThat(it).isGreaterThan(0)) + .thenMany(conn.createStatement(select).execute()) + .flatMap(result -> result.map(r -> 1)) + .reduce(0, Integer::sum) + .doOnNext(it -> assertThat(it).isEqualTo(count.get()))); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index ed66f34ab..48d63c0ed 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -53,11 +53,8 @@ */ class ConnectionIntegrationTest extends IntegrationTestSupport { - private static final MySqlConnectionConfiguration config = configuration( - "r2dbc", false, false, null, null); - ConnectionIntegrationTest() { - super(config); + super(configuration(builder -> builder)); } @Test diff --git a/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java index afece8130..66fb46e5a 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java @@ -15,10 +15,7 @@ class InitDbIntegrationTest extends IntegrationTestSupport { private static final String DATABASE = "test-" + ThreadLocalRandom.current().nextInt(10000); InitDbIntegrationTest() { - super(configuration( - DATABASE, true, false, - null, null - )); + super(configuration(builder -> builder.database(DATABASE).createDatabaseIfNotExist(true))); } @Test diff --git a/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java b/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java index 2489d5732..fb83de493 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java @@ -19,7 +19,6 @@ import io.r2dbc.spi.R2dbcBadGrammarException; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Result; -import org.jetbrains.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,10 +29,8 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.Duration; -import java.time.ZoneId; import java.util.Objects; import java.util.function.Function; -import java.util.function.Predicate; import static org.assertj.core.api.Assertions.assertThat; @@ -82,8 +79,7 @@ static Mono extractRowsUpdated(Result result) { } static MySqlConnectionConfiguration configuration( - String database, boolean createDatabaseIfNotExist, boolean autodetectExtensions, - @Nullable ZoneId serverZoneId, @Nullable Predicate preferPrepared + Function customizer ) { String password = System.getProperty("test.mysql.password"); @@ -106,22 +102,10 @@ static MySqlConnectionConfiguration configuration( .connectTimeout(Duration.ofSeconds(3)) .user("root") .password(password) - .database(database) - .createDatabaseIfNotExist(createDatabaseIfNotExist) - .allowLoadLocalInfileInPath(localInfilePath) - .autodetectExtensions(autodetectExtensions); + .database("r2dbc") + .allowLoadLocalInfileInPath(localInfilePath); - if (serverZoneId != null) { - builder.serverZoneId(serverZoneId); - } - - if (preferPrepared == null) { - builder.useClientPrepareStatement(); - } else { - builder.useServerPrepareStatement(preferPrepared); - } - - return builder.build(); + return customizer.apply(builder).build(); } boolean envIsLessThanMySql56() { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java index 7205a82bf..f78c5f11b 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java @@ -22,7 +22,8 @@ class JacksonPrepareIntegrationTest extends JacksonIntegrationTestSupport { JacksonPrepareIntegrationTest() { - super(configuration("r2dbc", false, true, null, sql -> false)); + super(configuration(builder -> builder.autodetectExtensions(true) + .useServerPrepareStatement())); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java index 6d666e520..0b114e033 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java @@ -22,6 +22,6 @@ class JacksonTextIntegrationTest extends JacksonIntegrationTestSupport { JacksonTextIntegrationTest() { - super(configuration("r2dbc", false, true, null, null)); + super(configuration(builder -> builder.autodetectExtensions(true))); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java b/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java index 04c5fac2e..8b08b3ead 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java @@ -17,11 +17,10 @@ package io.asyncer.r2dbc.mysql; import io.r2dbc.spi.Readable; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import java.time.ZonedDateTime; -import java.util.function.Predicate; +import java.util.function.Function; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; @@ -31,8 +30,10 @@ */ abstract class MariaDbIntegrationTestSupport extends IntegrationTestSupport { - MariaDbIntegrationTestSupport(@Nullable Predicate preferPrepared) { - super(configuration("r2dbc", false, false, null, preferPrepared)); + MariaDbIntegrationTestSupport( + Function customizer + ) { + super(configuration(customizer)); } @Test diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java index b7ac81a8b..8f7ba2998 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java @@ -25,6 +25,6 @@ class MariaDbPrepareIntegrationTest extends MariaDbIntegrationTestSupport { MariaDbPrepareIntegrationTest() { - super(sql -> true); + super(builder -> builder.useServerPrepareStatement(sql -> true)); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java index 0ab886c5f..fc285ddb3 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java @@ -25,6 +25,6 @@ class MariaDbTextIntegrationTest extends MariaDbIntegrationTestSupport { MariaDbTextIntegrationTest() { - super(null); + super(builder -> builder); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java index aa3067a1e..e0baef7d0 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.constant.TlsVersions; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; @@ -240,6 +241,8 @@ private static MySqlConnectionConfiguration filledUp() { .sslKey("/path/to/mysql/client-key.pem") .sslKeyPassword("pem-password-in-here") .tlsVersion(TlsVersions.TLS1_1, TlsVersions.TLS1_2, TlsVersions.TLS1_3) + .compressionAlgorithms(CompressionAlgorithm.ZSTD, CompressionAlgorithm.ZLIB, + CompressionAlgorithm.UNCOMPRESSED) .serverZoneId(ZoneId.systemDefault()) .zeroDateOption(ZeroDateOption.USE_NULL) .sslHostnameVerifier((host, s) -> true) diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index 17f515530..6c48a4e92 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -16,6 +16,7 @@ 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; @@ -24,6 +25,8 @@ import io.r2dbc.spi.Option; import org.assertj.core.api.Assert; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -34,6 +37,7 @@ import java.time.Duration; import java.time.ZoneId; import java.util.Collections; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -177,8 +181,8 @@ void validProgrammaticHost() { @Test void invalidProgrammatic() { - assertThatIllegalStateException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalStateException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(PORT, 3307) .option(USER, "root") @@ -198,8 +202,8 @@ void invalidProgrammatic() { .build())) .withMessageContaining("host"); - assertThatIllegalStateException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalStateException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(HOST, "127.0.0.1") .option(PORT, 3307) @@ -207,8 +211,8 @@ void invalidProgrammatic() { .build())) .withMessageContaining("user"); - assertThatIllegalArgumentException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalArgumentException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(HOST, "127.0.0.1") .option(PORT, 3307) @@ -220,8 +224,8 @@ void invalidProgrammatic() { .build())) .withMessageContaining("sslCert and sslKey"); - assertThatIllegalArgumentException().isThrownBy(() -> - MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() + assertThatIllegalArgumentException() + .isThrownBy(() -> MySqlConnectionFactoryProvider.setup(ConnectionFactoryOptions.builder() .option(DRIVER, "mysql") .option(HOST, "127.0.0.1") .option(PORT, 3307) @@ -394,6 +398,40 @@ void invalidServerPreparing() { .build())); } + @ParameterizedTest + @ValueSource(strings = { + "uncompressed", + "zlib", + "zstd", + "zlib,uncompressed", + "zstd,uncompressed", + "zstd,zlib", + "zstd,zlib,uncompressed", + }) + void validCompressionAlgorithms(String name) { + Set algorithms = MySqlConnectionFactoryProvider.setup( + ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(Option.valueOf("compressionAlgorithms"), name) + .build()).getCompressionAlgorithms(); + + assertThat(algorithms).hasSize(name.split(",").length); + } + + @ParameterizedTest + @ValueSource(strings = { "", "gzip", "lz4", "lz4hc", "none", "snappy", "zlib,none", "zstd,none" }) + void invalidCompressionAlgorithms(String name) { + assertThatIllegalArgumentException().isThrownBy(() -> MySqlConnectionFactoryProvider.setup( + ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(Option.valueOf("compressionAlgorithms"), name) + .build())); + } + @Test void validPasswordSupplier() { final Publisher passwordSupplier = Mono.just("123456"); diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java index 4a8878b92..08ed1fe28 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java @@ -22,7 +22,8 @@ class MySqlPrepareTestKit extends MySqlTestKitSupport { MySqlPrepareTestKit() { - super(IntegrationTestSupport.configuration("r2dbc", false, false, null, sql -> true)); + super(IntegrationTestSupport.configuration(builder -> + builder.useServerPrepareStatement(sql -> true))); } @Override diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java index 04c32c719..68fe276fb 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java @@ -22,6 +22,6 @@ class MySqlTextTestKit extends MySqlTestKitSupport { MySqlTextTestKit() { - super(IntegrationTestSupport.configuration("r2dbc", false, false, null, null)); + super(IntegrationTestSupport.configuration(builder -> builder)); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java index 45d5a94d7..3d53c2965 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java @@ -29,7 +29,7 @@ class PrepareQueryIntegrationTest extends QueryIntegrationTestSupport { PrepareQueryIntegrationTest() { - super(configuration("r2dbc", false, false, null, sql -> true)); + super(configuration(builder -> builder.useServerPrepareStatement(sql -> true))); } @Test diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java index ee6fbc391..6b53312e5 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java @@ -22,6 +22,6 @@ class PrepareTimeZoneIntegrationTest extends TimeZoneIntegrationTestSupport { PrepareTimeZoneIntegrationTest() { - super(sql -> true); + super(builder -> builder.useServerPrepareStatement(sql -> true)); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java index a4e7152b7..76f3b95c6 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java @@ -22,6 +22,6 @@ class TextQueryIntegrationTest extends QueryIntegrationTestSupport { TextQueryIntegrationTest() { - super(configuration("r2dbc", false, false, null, null)); + super(configuration(builder -> builder)); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java index 6b58ae1d4..336a0d5c1 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java @@ -22,6 +22,6 @@ class TextTimeZoneIntegrationTest extends TimeZoneIntegrationTestSupport { TextTimeZoneIntegrationTest() { - super(null); + super(builder -> builder); } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java b/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java index 5bfb769f1..7e32d07e4 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java @@ -16,7 +16,6 @@ package io.asyncer.r2dbc.mysql; -import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -31,7 +30,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.TimeZone; -import java.util.function.Predicate; +import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; @@ -65,8 +64,10 @@ abstract class TimeZoneIntegrationTestSupport extends IntegrationTestSupport { .isEqualTo(DST.atZone(SERVER_ZONE).plusHours(1)); } - TimeZoneIntegrationTestSupport(@Nullable Predicate preferPrepared) { - super(configuration("r2dbc", false, false, SERVER_ZONE, preferPrepared)); + TimeZoneIntegrationTestSupport( + Function customizer + ) { + super(configuration(builder -> customizer.apply(builder.serverZoneId(SERVER_ZONE)))); } @Test diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/Lifecycle.java b/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java similarity index 62% rename from src/main/java/io/asyncer/r2dbc/mysql/client/Lifecycle.java rename to src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java index 2ad0f6efd..df0e3c639 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/Lifecycle.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 asyncer.io projects + * Copyright 2024 asyncer.io projects * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,16 +14,16 @@ * limitations under the License. */ -package io.asyncer.r2dbc.mysql.client; +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; /** - * The lifecycle of connection. + * Integration tests for zstd compression. */ -enum Lifecycle { - -// CONNECTION, // Useless for signal - - COMMAND, +class ZlibCompressionIntegrationTest extends CompressionIntegrationTestSupport { -// REPLICATION // Useless for r2dbc driver, just ignore + ZlibCompressionIntegrationTest() { + super(CompressionAlgorithm.ZLIB); + } } diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java new file mode 100644 index 000000000..8a0c339e7 --- /dev/null +++ b/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; +import org.junit.jupiter.api.condition.EnabledIf; + +/** + * Integration tests for zstd compression. + */ +@EnabledIf("envIsZstdSupported") +class ZstdCompressionIntegrationTest extends CompressionIntegrationTestSupport { + + ZstdCompressionIntegrationTest() { + super(CompressionAlgorithm.ZSTD); + } + + static boolean envIsZstdSupported() { + String type = System.getProperty("test.db.type"); + + if ("mariadb".equalsIgnoreCase(type)) { + return false; + } + + String version = System.getProperty("test.mysql.version"); + + if (version == null || version.isEmpty()) { + return true; + } + + ServerVersion ver = ServerVersion.parse(version); + return ver.isGreaterThanOrEqualTo(ServerVersion.create(8, 0, 18)); + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java b/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java new file mode 100644 index 000000000..9ae947959 --- /dev/null +++ b/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.client; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.DecoderException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Stream; +import java.util.zip.DeflaterOutputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for {@link ZlibCompressor}. + */ +class ZlibCompressorTest { + + private final ZlibCompressor compressor = new ZlibCompressor(); + + @ParameterizedTest + @MethodSource("uncompressedData") + void compress(String input) throws IOException { + byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + ByteBuf compressed = compressor.compress(Unpooled.wrappedBuffer(bytes)); + byte[] nativeCompressed = nativeCompress(bytes); + + // It may return early if the compressed data is not smaller than the original. + assertThat(ByteBufUtil.getBytes(compressed)).hasSizeLessThanOrEqualTo(bytes.length) + .isEqualTo(Arrays.copyOf(nativeCompressed, compressed.readableBytes())); + } + + @ParameterizedTest + @MethodSource("uncompressedData") + void decompress(String input) throws IOException { + byte[] bytes = input.getBytes(StandardCharsets.UTF_8); + ByteBuf compressed = Unpooled.wrappedBuffer(nativeCompress(bytes)); + ByteBuf decompressed = compressor.decompress(compressed, bytes.length); + + assertThat(ByteBufUtil.getBytes(decompressed)).isEqualTo(bytes); + } + + @Test + void badDecompress() { + ByteBuf compressed = Unpooled.wrappedBuffer( + new byte[] { 0x78, 0x7c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }); + + assertThatExceptionOfType(DecoderException.class) + .isThrownBy(() -> compressor.decompress(compressed, compressed.readableBytes() << 1)); + } + + static Stream uncompressedData() { + return Stream.of( + "", " ", + "Hello, world!", + "1234567890", + "ユニコードテスト、유니코드 테스트,Unicode测试,тест Юникода", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " + + "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + + "Ut enim ad minim veniam, quis exercitation ullamco nisi ut aliquip ea commodo consequat. " + + "Duis aute irure dolor in reprehenderit en voluptate esse cillum eu fugiat nulla pariatur." + ); + } + + private static byte[] nativeCompress(byte[] input) throws IOException { + try (ByteArrayOutputStream r = new ByteArrayOutputStream(); + DeflaterOutputStream s = new DeflaterOutputStream(r)) { + + s.write(input); + s.finish(); + s.flush(); + + return r.toByteArray(); + } + } +} diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java b/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java index 02e076a03..8c9d7325e 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.internal.util; -import io.asyncer.r2dbc.mysql.constant.Envelopes; +import io.asyncer.r2dbc.mysql.constant.Packets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; @@ -30,6 +30,7 @@ import java.util.Arrays; import java.util.List; import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -262,7 +263,7 @@ void mergeIntegralWithLargeCrossIntegral() { } private Flux envelopes(Flux source, int envelopeSize) { - return new FluxEnvelope(source, allocator, envelopeSize, 0, true); + return new FluxEnvelope(source, allocator, envelopeSize, new AtomicInteger(0), true); } private Consumer> assertBuffers(String origin, int envelopeSize, int lastSize, @@ -273,7 +274,7 @@ private Consumer> assertBuffers(String origin, int envelopeSize, i for (int i = 0, n = originBuffers.size(); i < n; i += 2) { ByteBuf header = originBuffers.get(i); - assertThat(header.readableBytes()).isEqualTo(Envelopes.PART_HEADER_SIZE); + assertThat(header.readableBytes()).isEqualTo(Packets.NORMAL_HEADER_SIZE); int size = header.readMediumLE(); if (size > 0) { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java b/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java index c27dac59c..dd47e4678 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java @@ -21,7 +21,10 @@ import io.netty.buffer.Unpooled; import org.assertj.core.api.AbstractObjectAssert; import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -30,21 +33,21 @@ */ class ServerMessageDecoderTest { - @Test - void okAndPreparedOk() { - AbstractObjectAssert ok = assertThat(decode(okLike(), DecodeContext.command())) - .isExactlyInstanceOf(OkMessage.class) - .extracting(message -> (OkMessage) message); + @ParameterizedTest + @MethodSource(value = { "okLikePayload" }) + void okAndPreparedOk(byte[] okLike) { + AbstractObjectAssert ok = assertThat(decode( + Unpooled.wrappedBuffer(okLike), DecodeContext.command() + )).isExactlyInstanceOf(OkMessage.class).extracting(message -> (OkMessage) message); ok.extracting(OkMessage::getAffectedRows).isEqualTo(1L); ok.extracting(OkMessage::getLastInsertId).isEqualTo(0x10000L); // 65536 ok.extracting(OkMessage::getServerStatuses).isEqualTo((short) 0x100); // 256 ok.extracting(OkMessage::getWarnings).isEqualTo(0); - AbstractObjectAssert preparedOk = assertThat(decode(okLike(), - DecodeContext.prepareQuery())) - .isExactlyInstanceOf(PreparedOkMessage.class) - .extracting(message -> (PreparedOkMessage) message); + AbstractObjectAssert preparedOk = assertThat(decode( + Unpooled.wrappedBuffer(okLike), DecodeContext.prepareQuery() + )).isExactlyInstanceOf(PreparedOkMessage.class).extracting(message -> (PreparedOkMessage) message); preparedOk.extracting(PreparedOkMessage::getStatementId).isEqualTo(0xFD01); // 64769 preparedOk.extracting(PreparedOkMessage::getTotalColumns).isEqualTo(1); @@ -56,10 +59,8 @@ private static ServerMessage decode(ByteBuf buf, DecodeContext decodeContext) { return new ServerMessageDecoder().decode(buf, ConnectionContextTest.mock(), decodeContext); } - private static ByteBuf okLike() { - return Unpooled.wrappedBuffer(new byte[] { - 10, 0, 0, // envelope size - 1, // sequence ID + static Stream okLikePayload() { + return Stream.of(new byte[] { 0, // Heading both of OK and Prepared OK 1, // OK: affected rows, Prepared OK: first byte of statement ID (byte) 0xFD, From 4ac8120f3f39272839094326b41c315e6452228d Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 6 Feb 2024 15:29:53 +0900 Subject: [PATCH 09/93] Use access controller for getting class loader --- src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 59675e647..4ad301974 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -70,6 +70,8 @@ import reactor.core.publisher.SynchronousSink; import reactor.util.concurrent.Queues; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; @@ -1079,8 +1081,11 @@ private static String authFails(String authType, String phase) { private static boolean isZstdSupported() { try { - Class.forName("com.github.luben.zstd.Zstd", false, - LoginExchangeable.class.getClassLoader()); + 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; From fcf1f18d1152a17ab1fed1ba232d781a61009762 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 13 Feb 2024 15:23:41 +0900 Subject: [PATCH 10/93] Correct javadoc --- .../io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java | 4 ++-- .../asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java | 4 ++-- .../io/asyncer/r2dbc/mysql/message/client/ClientMessage.java | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 7f3bed2b4..83f11b4cf 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -866,7 +866,7 @@ public Builder prepareCacheSize(int prepareCacheSize) { * @param compressionAlgorithms the list of compression algorithms. * @return {@link Builder this}. * @throws IllegalArgumentException if {@code compressionAlgorithms} is {@code null} or empty. - * @since 1.1.0 + * @since 1.1.2 */ public Builder compressionAlgorithms(CompressionAlgorithm... compressionAlgorithms) { requireNonNull(compressionAlgorithms, "compressionAlgorithms must not be null"); @@ -900,7 +900,7 @@ public Builder compressionAlgorithms(CompressionAlgorithm... compressionAlgorith * @param level the compression level. * @return {@link Builder this}. * @throws IllegalArgumentException if {@code level} is not between 1 and 22. - * @since 1.1.0 + * @since 1.1.2 * @see * MySQL Connection Options --zstd-compression-level */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index ef1286b7b..a0e818664 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -199,7 +199,7 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr *

* Note: zstd requires a dependency {@code com.github.luben:zstd-jni}. * - * @since 1.1.0 + * @since 1.1.2 */ public static final Option COMPRESSION_ALGORITHMS = Option.valueOf("compressionAlgorithms"); @@ -212,7 +212,7 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr * Note: MySQL protocol does not allow to set the zlib compression level of the server, only zstd is * configurable. * - * @since 1.1.0 + * @since 1.1.2 */ public static final Option ZSTD_COMPRESSION_LEVEL = Option.valueOf("zstdCompressionLevel"); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java index 7d969d376..047884a17 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java @@ -27,6 +27,11 @@ */ public interface ClientMessage { + /** + * Returns whether the sequence should be reset before encoding this message. + * + * @return {@code true} if the sequence should be reset. + */ default boolean isSequenceReset() { return true; } From aff4cc531880e1f50601530dbcf3aef427339217 Mon Sep 17 00:00:00 2001 From: jchrys Date: Wed, 14 Feb 2024 12:14:50 +0900 Subject: [PATCH 11/93] Add LoopResources Configuration Support (#232) Motivation: Enhances flexibility by allowing custom `LoopResources` configuration for optimized event loop management. Modifications: Introduced `LoopResources` configuration option in `MysqlConnectionConfiguration` and updated relevant documentation. Result: Enables performance tuning and flexibility, improving adaptability for various deployment scenarios. Resolves #229 --- README.md | 4 +++ .../mysql/MySqlConnectionConfiguration.java | 34 +++++++++++++++++-- .../r2dbc/mysql/MySqlConnectionFactory.java | 2 +- .../mysql/MySqlConnectionFactoryProvider.java | 11 ++++++ .../io/asyncer/r2dbc/mysql/client/Client.java | 8 +++-- 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d39cf40e7..a15310cc9 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() .option(Option.valueOf("tcpKeepAlive"), true) // optional, default false .option(Option.valueOf("tcpNoDelay"), true) // optional, default false .option(Option.valueOf("compressionAlgorithms"), "zstd") // optional, default UNCOMPRESSED + .option(Option.valueOf("loopResources"), LoopResources.create("r2dbc")) // optional, default null, null means uses global tcp resources as loopResources (since 1.1.2) .option(Option.valueOf("autodetectExtensions"), false) // optional, default false .option(Option.valueOf("passwordPublisher"), Mono.just("password")) // optional, default null, null means has no passwordPublisher (since 1.0.5 / 0.9.6) .build(); @@ -194,6 +195,7 @@ MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builde .tcpKeepAlive(true) // optional, controls TCP Keep Alive, default is false .tcpNoDelay(true) // optional, controls TCP No Delay, default is false .compressionAlgorithms(CompressionAlgorithm.ZSTD, CompressionAlgotihm.ZLIB) // optional, default is UNCOMPRESSED + .loopResources(LoopResources.create("r2dbc")) // optional, default null, null means uses global tcp resources as loopResources (since 1.1.2) .autodetectExtensions(false) // optional, controls extension auto-detect, default is true .extendWith(MyExtension.INSTANCE) // optional, manual extend an extension into extensions, default using auto-detect .passwordPublisher(Mono.just("password")) // optional, default null, null means has no password publisher (since 1.0.5 / 0.9.6) @@ -246,6 +248,7 @@ Mono connectionMono = Mono.from(connectionFactory.create()); | useServerPrepareStatement | `true`, `false` or `Predicate` | Optional, default is `false` | See following notice | | allowLoadLocalInfileInPath | A path | Optional, default is `null` | The path that allows `LOAD DATA LOCAL INFILE` to load file data | | compressionAlgorithms | A list of `CompressionAlgorithm` | Optional, default is `UNCOMPRESSED` | The compression algorithms for MySQL connection | +| loopResources | A `LoopResources` | Optional, default is `null` | The loop resources for MySQL connection | | passwordPublisher | A `Publisher` | Optional, default is `null` | The password publisher, see following notice | - `SslMode` Considers security level and verification for SSL, make sure the database server supports SSL before you want change SSL mode to `REQUIRED` or higher. **The Unix Domain Socket only offers "DISABLED" available** @@ -278,6 +281,7 @@ Mono connectionMono = Mono.from(connectionFactory.create()); - `ZLIB` Use Zlib compression protocol, it is available on almost all MySQL versions (`5.x` and above) - `ZSTD` Use Z-standard compression protocol, it is available since MySQL `8.0.18` or above, requires an extern dependency `com.github.luben:zstd-jni` - For scenarios where the network environment is poor or the amount of data is always large, using a compression protocol may be useful +- `loopResources` Considers loop resources for MySQL connection. Should use `enum` in [Programmatic](#programmatic-configuration) configuration that not like discovery configurations, except `TlsVersions` (All elements of `TlsVersions` will be always `String` which is case-sensitive). diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 83f11b4cf..f9c23687d 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -23,6 +23,8 @@ import io.netty.handler.ssl.SslContextBuilder; import org.jetbrains.annotations.Nullable; import org.reactivestreams.Publisher; +import reactor.netty.resources.LoopResources; +import reactor.netty.tcp.TcpResources; import javax.net.ssl.HostnameVerifier; import java.net.Socket; @@ -105,6 +107,8 @@ public final class MySqlConnectionConfiguration { private final int zstdCompressionLevel; + private final LoopResources loopResources; + private final Extensions extensions; @Nullable @@ -119,6 +123,7 @@ private MySqlConnectionConfiguration( @Nullable Path loadLocalInfilePath, int localInfileBufferSize, int queryCacheSize, int prepareCacheSize, Set compressionAlgorithms, int zstdCompressionLevel, + @Nullable LoopResources loopResources, Extensions extensions, @Nullable Publisher passwordPublisher ) { this.isHost = isHost; @@ -141,6 +146,7 @@ private MySqlConnectionConfiguration( this.prepareCacheSize = prepareCacheSize; this.compressionAlgorithms = compressionAlgorithms; this.zstdCompressionLevel = zstdCompressionLevel; + this.loopResources = loopResources == null? TcpResources.get() : loopResources; this.extensions = extensions; this.passwordPublisher = passwordPublisher; } @@ -239,6 +245,10 @@ int getZstdCompressionLevel() { return zstdCompressionLevel; } + LoopResources getLoopResources() { + return loopResources; + } + Extensions getExtensions() { return extensions; } @@ -277,6 +287,7 @@ public boolean equals(Object o) { prepareCacheSize == that.prepareCacheSize && compressionAlgorithms.equals(that.compressionAlgorithms) && zstdCompressionLevel == that.zstdCompressionLevel && + Objects.equals(loopResources, that.loopResources) && extensions.equals(that.extensions) && Objects.equals(passwordPublisher, that.passwordPublisher); } @@ -286,7 +297,8 @@ public int hashCode() { return Objects.hash(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, connectTimeout, serverZoneId, zeroDateOption, user, password, database, createDatabaseIfNotExist, preferPrepareStatement, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, - prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, extensions, passwordPublisher); + prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, + extensions, passwordPublisher); } @Override @@ -303,6 +315,7 @@ public String toString() { ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize + ", compressionAlgorithms=" + compressionAlgorithms + ", zstdCompressionLevel=" + zstdCompressionLevel + + ", loopResources=" + loopResources + ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; } @@ -317,6 +330,7 @@ public String toString() { ", prepareCacheSize=" + prepareCacheSize + ", compressionAlgorithms=" + compressionAlgorithms + ", zstdCompressionLevel=" + zstdCompressionLevel + + ", loopResources=" + loopResources + ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; } @@ -393,6 +407,9 @@ public static final class Builder { private int zstdCompressionLevel = 3; + @Nullable + private LoopResources loopResources; + private boolean autodetectExtensions = true; private final List extensions = new ArrayList<>(); @@ -425,7 +442,7 @@ public MySqlConnectionConfiguration build() { connectTimeout, zeroDateOption, serverZoneId, user, password, database, createDatabaseIfNotExist, preferPrepareStatement, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, - compressionAlgorithms, zstdCompressionLevel, + compressionAlgorithms, zstdCompressionLevel, loopResources, Extensions.from(extensions, autodetectExtensions), passwordPublisher); } @@ -911,6 +928,19 @@ public Builder zstdCompressionLevel(int 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}. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index 4c2d69cd4..41bbc4149 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -146,7 +146,7 @@ private static Mono getMySqlConnection( final int prepareCacheSize, @Nullable final CharSequence password) { return Client.connect(ssl, address, configuration.isTcpKeepAlive(), configuration.isTcpNoDelay(), - context, configuration.getConnectTimeout()) + context, configuration.getConnectTimeout(), configuration.getLoopResources()) .flatMap(client -> { // Lazy init database after handshake/login String db = createDbIfNotExist ? "" : database; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index a0e818664..296e84b5a 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -25,6 +25,7 @@ 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; @@ -217,6 +218,14 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr 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}. * @@ -312,6 +321,8 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { ).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); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java b/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java index 979748779..bf2b8a219 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java @@ -28,6 +28,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; +import reactor.netty.resources.LoopResources; import reactor.netty.tcp.TcpClient; import java.net.InetSocketAddress; @@ -116,17 +117,20 @@ public interface Client { * @param tcpNoDelay if enable the {@link ChannelOption#TCP_NODELAY} * @param context the connection context * @param connectTimeout connect timeout, or {@code null} if it has no timeout + * @param loopResources the loop resources to use * @return A {@link Mono} that will emit a connected {@link Client}. * @throws IllegalArgumentException if {@code ssl}, {@code address} or {@code context} is {@code null}. * @throws ArithmeticException if {@code connectTimeout} milliseconds overflow as an int */ static Mono connect(MySqlSslConfiguration ssl, SocketAddress address, boolean tcpKeepAlive, - boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout) { + boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout, + LoopResources loopResources) { requireNonNull(ssl, "ssl must not be null"); requireNonNull(address, "address must not be null"); requireNonNull(context, "context must not be null"); - TcpClient tcpClient = TcpClient.newConnection(); + TcpClient tcpClient = TcpClient.newConnection() + .runOn(loopResources); if (connectTimeout != null) { tcpClient = tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, From 7db899e1f04aff450755dc1110b99574621cb1d0 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 6 Feb 2024 15:04:06 +0900 Subject: [PATCH 12/93] Add session variables support --- .../asyncer/r2dbc/mysql/MySqlConnection.java | 119 ++++++++---- .../mysql/MySqlConnectionConfiguration.java | 48 ++++- .../r2dbc/mysql/MySqlConnectionFactory.java | 9 +- .../mysql/MySqlConnectionFactoryProvider.java | 179 +++++++++++++++++- .../io/asyncer/r2dbc/mysql/OptionMapper.java | 5 +- .../MySqlConnectionFactoryProviderTest.java | 67 +++++++ .../mysql/SessionStateIntegrationTest.java | 108 +++++++++++ 7 files changed, 479 insertions(+), 56 deletions(-) create mode 100644 src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java index 65569c5d4..049d351ab 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java @@ -37,7 +37,6 @@ import io.r2dbc.spi.TransactionDefinition; import io.r2dbc.spi.ValidationDepth; import org.jetbrains.annotations.Nullable; -import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; @@ -46,6 +45,7 @@ import java.time.Duration; import java.time.ZoneId; import java.time.ZoneOffset; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -447,12 +447,12 @@ public Mono setStatementTimeout(Duration timeout) { } return Mono.error( - new R2dbcNonTransientResourceException( - "Statement timeout is not supported by server version " + serverVersion, - "HY000", - -1, - sql - ) + new R2dbcNonTransientResourceException( + "Statement timeout is not supported by server version " + serverVersion, + "HY000", + -1, + sql + ) ); } @@ -467,38 +467,23 @@ boolean isSessionAutoCommit() { /** * Initialize a {@link MySqlConnection} after login. * - * @param client must be logged-in. - * @param codecs the {@link Codecs}. - * @param context must be initialized. - * @param database the database that should be lazy init. - * @param queryCache the cache of {@link Query}. - * @param prepareCache the cache of server-preparing result. - * @param prepare judging for prefer use prepare statement to execute simple query. + * @param client must be logged-in. + * @param codecs the {@link Codecs}. + * @param context must be initialized. + * @param database the database that should be lazy init. + * @param queryCache the cache of {@link Query}. + * @param prepareCache the cache of server-preparing result. + * @param sessionVariables the session variables to set. + * @param prepare judging for prefer use prepare statement to execute simple query. * @return a {@link Mono} will emit an initialized {@link MySqlConnection}. */ static Mono init( Client client, Codecs codecs, ConnectionContext context, String database, QueryCache queryCache, PrepareCache prepareCache, - @Nullable Predicate prepare + List sessionVariables, @Nullable Predicate prepare ) { - StringBuilder query = new StringBuilder(128) - .append("SELECT ") - .append(transactionIsolationColumn(context)) - .append(",@@innodb_lock_wait_timeout AS l,@@version_comment AS v"); - - Function> handler; - - if (context.shouldSetServerZoneId()) { - query.append(",@@system_time_zone AS s,@@time_zone AS t"); - handler = MySqlConnection::fullInit; - } else { - handler = MySqlConnection::init; - } - - Mono connection = new TextSimpleStatement(client, codecs, context, query.toString()) - .execute() - .flatMap(handler) - .last() + Mono connection = initSessionVariables(client, sessionVariables) + .then(loadSessionVariables(client, codecs, context)) .map(data -> { ZoneId serverZoneId = data.serverZoneId; if (serverZoneId != null) { @@ -514,29 +499,83 @@ static Mono init( return connection; } - requireNonEmpty(database, "database must not be empty"); + return connection.flatMap(c -> initDatabase(client, database).thenReturn(c)); + } + + private static Mono initSessionVariables(Client client, List sessionVariables) { + if (sessionVariables.isEmpty()) { + return Mono.empty(); + } + + StringBuilder query = new StringBuilder(sessionVariables.size() * 32 + 16).append("SET "); + boolean comma = false; + + for (String variable : sessionVariables) { + if (variable.isEmpty()) { + continue; + } + + if (comma) { + query.append(','); + } else { + comma = true; + } + + if (variable.startsWith("@")) { + query.append(variable); + } else { + query.append("SESSION ").append(variable); + } + } + + return QueryFlow.executeVoid(client, query.toString()); + } + + private static Mono loadSessionVariables( + Client client, Codecs codecs, ConnectionContext context + ) { + StringBuilder query = new StringBuilder(160) + .append("SELECT ") + .append(transactionIsolationColumn(context)) + .append(",@@innodb_lock_wait_timeout AS l,@@version_comment AS v"); + + Function> handler; + + if (context.shouldSetServerZoneId()) { + query.append(",@@system_time_zone AS s,@@time_zone AS t"); + handler = MySqlConnection::fullInit; + } else { + handler = MySqlConnection::init; + } + + return new TextSimpleStatement(client, codecs, context, query.toString()) + .execute() + .flatMap(handler) + .last(); + } - return connection.flatMap(conn -> client.exchange(new InitDbMessage(database), INIT_DB) + private static Mono initDatabase(Client client, String database) { + return client.exchange(new InitDbMessage(database), INIT_DB) .last() .flatMap(success -> { if (success) { - return Mono.just(conn); + 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(Mono.just(conn))); - })); + .then(client.exchange(new InitDbMessage(database), INIT_DB_AFTER).then()); + }); } - private static Publisher init(MySqlResult r) { + private static Flux init(MySqlResult r) { return r.map((row, meta) -> new InitData(convertIsolationLevel(row.get(0, String.class)), convertLockWaitTimeout(row.get(1, Long.class)), row.get(2, String.class), null)); } - private static Publisher fullInit(MySqlResult r) { + private static Flux fullInit(MySqlResult r) { return r.map((row, meta) -> { IsolationLevel level = convertIsolationLevel(row.get(0, String.class)); long lockWaitTimeout = convertLockWaitTimeout(row.get(1, Long.class)); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index f9c23687d..fdfce2fd9 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -20,6 +20,7 @@ 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 org.jetbrains.annotations.Nullable; import org.reactivestreams.Publisher; @@ -94,6 +95,8 @@ public final class MySqlConnectionConfiguration { @Nullable private final Predicate preferPrepareStatement; + private final List sessionVariables; + @Nullable private final Path loadLocalInfilePath; @@ -120,6 +123,7 @@ private MySqlConnectionConfiguration( ZeroDateOption zeroDateOption, @Nullable ZoneId serverZoneId, String user, @Nullable CharSequence password, @Nullable String database, boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement, + List sessionVariables, @Nullable Path loadLocalInfilePath, int localInfileBufferSize, int queryCacheSize, int prepareCacheSize, Set compressionAlgorithms, int zstdCompressionLevel, @@ -140,13 +144,14 @@ private MySqlConnectionConfiguration( this.database = database == null || database.isEmpty() ? "" : database; this.createDatabaseIfNotExist = createDatabaseIfNotExist; this.preferPrepareStatement = preferPrepareStatement; + this.sessionVariables = sessionVariables; 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.loopResources = loopResources == null ? TcpResources.get() : loopResources; this.extensions = extensions; this.passwordPublisher = passwordPublisher; } @@ -220,6 +225,10 @@ Predicate getPreferPrepareStatement() { return preferPrepareStatement; } + List getSessionVariables() { + return sessionVariables; + } + @Nullable Path getLoadLocalInfilePath() { return loadLocalInfilePath; @@ -281,6 +290,7 @@ public boolean equals(Object o) { database.equals(that.database) && createDatabaseIfNotExist == that.createDatabaseIfNotExist && Objects.equals(preferPrepareStatement, that.preferPrepareStatement) && + sessionVariables.equals(that.sessionVariables) && Objects.equals(loadLocalInfilePath, that.loadLocalInfilePath) && localInfileBufferSize == that.localInfileBufferSize && queryCacheSize == that.queryCacheSize && @@ -296,9 +306,9 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, connectTimeout, serverZoneId, zeroDateOption, user, password, database, createDatabaseIfNotExist, - preferPrepareStatement, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, - prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, - extensions, passwordPublisher); + preferPrepareStatement, sessionVariables, loadLocalInfilePath, + localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, + zstdCompressionLevel, loopResources, extensions, passwordPublisher); } @Override @@ -310,6 +320,7 @@ public String toString() { ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + + ", sessionVariables=" + sessionVariables + ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize + @@ -324,6 +335,7 @@ public String toString() { ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + + ", sessionVariables=" + sessionVariables + ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + ", queryCacheSize=" + queryCacheSize + @@ -393,6 +405,8 @@ public static final class Builder { @Nullable private Predicate preferPrepareStatement; + private List sessionVariables = Collections.emptyList(); + @Nullable private Path loadLocalInfilePath; @@ -440,7 +454,7 @@ public MySqlConnectionConfiguration build() { sslCa, sslKey, sslKeyPassword, sslCert, sslContextBuilderCustomizer); return new MySqlConnectionConfiguration(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, connectTimeout, zeroDateOption, serverZoneId, user, password, database, - createDatabaseIfNotExist, preferPrepareStatement, loadLocalInfilePath, + createDatabaseIfNotExist, preferPrepareStatement, sessionVariables, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, Extensions.from(extensions, autodetectExtensions), passwordPublisher); @@ -801,6 +815,23 @@ public Builder useServerPrepareStatement(Predicate preferPrepareStatemen return this; } + /** + * Configure 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 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. @@ -917,9 +948,9 @@ public Builder compressionAlgorithms(CompressionAlgorithm... compressionAlgorith * @param level the compression level. * @return {@link Builder this}. * @throws IllegalArgumentException if {@code level} is not between 1 and 22. - * @since 1.1.2 * @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"); @@ -929,8 +960,9 @@ public Builder zstdCompressionLevel(int level) { } /** - * Configures the {@link LoopResources} for the driver. - * Default to {@link TcpResources#get() global tcp resources}. + * 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}. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index 41bbc4149..a8b49a4df 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -35,6 +35,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; @@ -100,6 +101,7 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura configuration.getServerZoneId() ); Set compressionAlgorithms = configuration.getCompressionAlgorithms(); + List sessionVariables = configuration.getSessionVariables(); Extensions extensions = configuration.getExtensions(); Predicate prepare = configuration.getPreferPrepareStatement(); int prepareCacheSize = configuration.getPrepareCacheSize(); @@ -112,7 +114,7 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura database, createDbIfNotExist, user, sslMode, compressionAlgorithms, zstdCompressionLevel, - context, extensions, prepare, + context, extensions, sessionVariables, prepare, prepareCacheSize, token )); } @@ -123,7 +125,7 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura database, createDbIfNotExist, user, sslMode, compressionAlgorithms, zstdCompressionLevel, - context, extensions, prepare, + context, extensions, sessionVariables, prepare, prepareCacheSize, password ); })); @@ -142,6 +144,7 @@ private static Mono getMySqlConnection( final int zstdCompressionLevel, final ConnectionContext context, final Extensions extensions, + final List sessionVariables, @Nullable final Predicate prepare, final int prepareCacheSize, @Nullable final CharSequence password) { @@ -163,7 +166,7 @@ private static Mono getMySqlConnection( registrar.register(allocator, builder)); return MySqlConnection.init(client, builder.build(), context, db, queryCache.get(), - prepareCache, prepare); + prepareCache, sessionVariables, prepare); }); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index 296e84b5a..0dfa0475c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -30,10 +30,14 @@ 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; @@ -183,6 +187,14 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr 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. * @@ -219,8 +231,8 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr Option.valueOf("zstdCompressionLevel"); /** - * Option to set the {@link LoopResources} for the connection. - * Default to {@link reactor.netty.tcp.TcpResources#get() global tcp Resources} + * Option to set the {@link LoopResources} for the connection. Default to + * {@link reactor.netty.tcp.TcpResources#get() global tcp Resources} * * @since 1.1.2 */ @@ -317,6 +329,7 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { 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() @@ -325,6 +338,12 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { .to(builder::loopResources); mapper.optional(PASSWORD_PUBLISHER).as(Publisher.class) .to(builder::passwordPublisher); + mapper.optional(SESSION_VARIABLES).asArray( + String[].class, + Function.identity(), + MySqlConnectionFactoryProvider::splitVariables, + String[]::new + ).to(builder::sessionVariables); return builder.build(); } @@ -345,7 +364,8 @@ private static void setupHost(MySqlConnectionConfiguration.Builder builder, Opti .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(), String[]::new) + 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); @@ -360,4 +380,157 @@ private static void setupHost(MySqlConnectionConfiguration.Builder builder, Opti 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/OptionMapper.java b/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java index 8a95cea1d..62ecede60 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java @@ -106,7 +106,8 @@ Source as(Class type, Function mapping) { throw new IllegalArgumentException(toMessage(value, type.getTypeName())); } - Source asArray(Class arrayType, Function mapper, IntFunction generator) { + Source asArray(Class arrayType, Function mapper, + Function splitter, IntFunction generator) { if (value == null) { return nilSource(); } @@ -116,7 +117,7 @@ Source asArray(Class arrayType, Function mapper, IntFun } else if (value instanceof String[]) { return new Source<>(mapArray((String[]) value, mapper, generator)); } else if (value instanceof String) { - String[] strings = ((String) value).split(","); + String[] strings = splitter.apply((String) value); if (arrayType.isInstance(strings)) { return new Source<>(arrayType.cast(strings)); diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index 6c48a4e92..01439fbee 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -26,6 +26,8 @@ import org.assertj.core.api.Assert; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -36,10 +38,13 @@ import java.net.URLEncoder; import java.time.Duration; import java.time.ZoneId; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Stream; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.PASSWORD_PUBLISHER; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.USE_SERVER_PREPARE_STATEMENT; @@ -445,6 +450,68 @@ void validPasswordSupplier() { assertThat(ConnectionFactories.get(options)).isExactlyInstanceOf(MySqlConnectionFactory.class); } + @ParameterizedTest + @MethodSource + void sessionVariables(String input, List expected) { + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(Option.valueOf("sessionVariables"), input) + .build(); + + assertThat(MySqlConnectionFactoryProvider.setup(options).getSessionVariables()).isEqualTo(expected); + } + + static Stream sessionVariables() { + return Stream.of( + Arguments.of("", Collections.emptyList()), + Arguments.of(" ", Collections.singletonList("")), + Arguments.of("a=b", Collections.singletonList("a=b")), + Arguments.of( + "sql_mode=ANSI_QUOTE,c=d;e=f", + Arrays.asList("sql_mode=ANSI_QUOTE", "c=d", "e=f")), + Arguments.of( + "sql_mode='ANSI_QUOTES,b=c,c=d';c=d,e=f", + Arrays.asList("sql_mode='ANSI_QUOTES,b=c,c=d'", "c=d", "e=f")), + Arguments.of( + "sql_mode=(ANSI_QUOTES,'b=c,c=d,max(');c=(d,e='f)');", + Arrays.asList("sql_mode=(ANSI_QUOTES,'b=c,c=d,max(')", "c=(d,e='f)')", "")), + Arguments.of( + "sql_mode=(ANSI_QUOTES,'b=c,c=d,max(');c=(d,e='f)'); ", + Arrays.asList("sql_mode=(ANSI_QUOTES,'b=c,c=d,max(')", "c=(d,e='f)')", "")), + Arguments.of( + "sql_mode=(ANSI_QUOTES,\"b=c',c=d,max(\");c=(d,'e=\"f)\");',)", + Arrays.asList("sql_mode=(ANSI_QUOTES,\"b=c',c=d,max(\")", "c=(d,'e=\"f)\");',)")), + Arguments.of( + "sql_mode=(((;),);)", + Collections.singletonList("sql_mode=(((;),);)")), + Arguments.of( + "sql_mode=(((';),););',);a=),);d=)", + Arrays.asList("sql_mode=(((';),););',);a=),)", "d=)")), + Arguments.of( + "sql_mode=((\"(';),)\";);',);)a=,)';),b=(();)", + Arrays.asList("sql_mode=((\"(';),)\";);',);)a=,)';)", "b=(();)")), + Arguments.of( + "sql_mode=((\"(';),)\";);',);)a=,)'b=;)\\,c=(();)", + Arrays.asList("sql_mode=((\"(';),)\";);',);)a=,)'b=;)\\", "c=(();)")), + Arguments.of( + "sql_mode='\\','", + Collections.singletonList("sql_mode='\\','")), + Arguments.of( + "sql_mode=\",\\\",'\\\\',',\"", + Collections.singletonList("sql_mode=\",\\\",'\\\\',',\"")), + Arguments.of( + "sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'," + + "transaction_isolation=(SELECT UPPER(`it's ``lvl```) FROM `lvl` WHERE `type` = 'r2dbc')" + + ",`foo``bar`='FOO,BAR'", + Arrays.asList( + "sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES'", + "transaction_isolation=(SELECT UPPER(`it's ``lvl```) FROM `lvl` WHERE `type` = 'r2dbc')", + "`foo``bar`='FOO,BAR'" + )) + ); + } } final class MockException extends RuntimeException { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java b/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java new file mode 100644 index 000000000..49ed0b672 --- /dev/null +++ b/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java @@ -0,0 +1,108 @@ +/* + * 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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Integration tests for session state. + */ +class SessionStateIntegrationTest { + + @ParameterizedTest + @MethodSource + void sessionVariables(Map variables) { + String[] pairs = variables.entrySet().stream() + .map(entry -> entry.getKey() + "=" + entry.getValue()) + .toArray(String[]::new); + String[] keys = variables.keySet().toArray(new String[0]); + String selection = variables.keySet().stream() + .map(it -> "@@session." + it + " AS " + it) + .collect(Collectors.joining(",", "SELECT ", "")); + Map expected = variables.entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().startsWith("'") ? + entry.getValue().substring(1, entry.getValue().length() - 1) : entry.getValue())); + + connectionFactory(builder -> builder.sessionVariables(pairs)) + .create() + .flatMapMany(connection -> connection.createStatement(selection).execute() + .flatMap(result -> result.map((row, metadata) -> { + Map map = new LinkedHashMap<>(); + for (String key : keys) { + map.put(key, row.get(key, String.class)); + } + return map; + })) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(expected) + .verifyComplete(); + } + + static Stream sessionVariables() { + return Stream.of( + Arguments.of(mapOf("sql_mode", "ANSI_QUOTES")), + Arguments.of(mapOf("time_zone", "'+00:00'")), + Arguments.of(mapOf("sql_mode", "'ANSI_QUOTES,STRICT_ALL_TABLES'", "time_zone", "'Asia/Tokyo'")) + ); + } + + private static MySqlConnectionFactory connectionFactory( + Function customizer + ) { + String password = System.getProperty("test.mysql.password"); + + if (password == null || password.isEmpty()) { + throw new IllegalStateException("Property test.mysql.password must exists and not be empty"); + } + + MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() + .host("localhost") + .port(3306) + .user("root") + .password(password) + .database("r2dbc"); + + return MySqlConnectionFactory.from(customizer.apply(builder).build()); + } + + private static Map mapOf(String... paris) { + if (paris.length % 2 != 0) { + throw new IllegalArgumentException("Pairs must be even"); + } + + Map map = new LinkedHashMap<>(); + + for (int i = 0; i < paris.length; i += 2) { + map.put(paris[i], paris[i + 1]); + } + + return map; + } +} From 10b055e6823d1273323b9147d3205d87e999ae4a Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 15 Feb 2024 16:52:20 +0900 Subject: [PATCH 13/93] Slim README --- README.md | 370 +----------------------------------------------------- 1 file changed, 5 insertions(+), 365 deletions(-) diff --git a/README.md b/README.md index a15310cc9..1d1ca09be 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ This implementation is not intended to be used directly, but rather to be used as the backing implementation for a humane client library to delegate to. See [R2DBC Homepage](https://r2dbc.io). +See [R2DBC MySQL wiki](https://github.com/asyncer-io/r2dbc-mysql/wiki) for more information. + ## Spring-framework and R2DBC-SPI Compatibility Refer to the table below to determine the appropriate version of r2dbc-mysql for your project. @@ -36,7 +38,7 @@ This driver provides the following features: ## Maintainer -This project is currently being maintained by [@jchrys](https://github.com/jchrys), since the previous owner has been inactive. We are committed to keeping this project up-to-date and improving it in collaboration with the community. +This project is currently being maintained by [@jchrys](https://github.com/jchrys) ## Version compatibility / Integration tests states ![MySQL 5.5 status](https://img.shields.io/badge/MySQL%205.5-pass-blue) @@ -87,203 +89,15 @@ dependencies { Here is a quick teaser of how to use R2DBC MySQL in Java: -### URL Connection Factory Discovery - ```java // Notice: the query string must be URL encoded -ConnectionFactory connectionFactory = ConnectionFactories.get( - "r2dbcs:mysql://root:database-password-in-here@127.0.0.1:3306/r2dbc?" + - "zeroDate=use_round&" + - "sslMode=verify_identity&" + - "useServerPrepareStatement=true&" + - "tlsVersion=TLSv1.3%2CTLSv1.2%2CTLSv1.1&" + - "sslCa=%2Fpath%2Fto%2Fmysql%2Fca.pem&" + - "sslKey=%2Fpath%2Fto%2Fmysql%2Fclient-key.pem&" + - "sslCert=%2Fpath%2Fto%2Fmysql%2Fclient-cert.pem&" + - "sslKeyPassword=key-pem-password-in-here" -); - -// Creating a Mono using Project Reactor -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -> It is just example, see also Programmatic Connection Factory Discovery for more options. - -Or use unix domain socket like following: - -```java -// Minimum configuration for unix domain socket -ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:mysql://root@unix?unixSocket=%2Fpath%2Fto%2Fmysql.sock"); - -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -### Programmatic Connection Factory Discovery - -```java -ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() - .option(DRIVER, "mysql") - .option(HOST, "127.0.0.1") - .option(USER, "root") - .option(PORT, 3306) // optional, default 3306 - .option(PASSWORD, "database-password-in-here") // optional, default null, null means has no password - .option(DATABASE, "r2dbc") // optional, default null, null means not specifying the database - .option(Option.valueOf("createDatabaseIfNotExist"), true) // optional, default false, create database if not exist (since 1.0.6 / 0.9.7) - .option(CONNECT_TIMEOUT, Duration.ofSeconds(3)) // optional, default null, null means no timeout - .option(SSL, true) // optional, default sslMode is "preferred", it will be ignore if sslMode is set - .option(Option.valueOf("sslMode"), "verify_identity") // optional, default "preferred" - .option(Option.valueOf("sslCa"), "/path/to/mysql/ca.pem") // required when sslMode is verify_ca or verify_identity, default null, null means has no server CA cert - .option(Option.valueOf("sslCert"), "/path/to/mysql/client-cert.pem") // optional, default null, null means has no client cert - .option(Option.valueOf("sslKey"), "/path/to/mysql/client-key.pem") // optional, default null, null means has no client key - .option(Option.valueOf("sslKeyPassword"), "key-pem-password-in-here") // optional, default null, null means has no password for client key (i.e. "sslKey") - .option(Option.valueOf("tlsVersion"), "TLSv1.3,TLSv1.2,TLSv1.1") // optional, default is auto-selected by the server - .option(Option.valueOf("sslHostnameVerifier"), "com.example.demo.MyVerifier") // optional, default is null, null means use standard verifier - .option(Option.valueOf("sslContextBuilderCustomizer"), "com.example.demo.MyCustomizer") // optional, default is no-op customizer - .option(Option.valueOf("zeroDate"), "use_null") // optional, default "use_null" - .option(Option.valueOf("useServerPrepareStatement"), true) // optional, default false - .option(Option.valueOf("allowLoadLocalInfileInPath"), "/opt") // optional, default null, null means LOCAL INFILE not be allowed (since 1.1.0) - .option(Option.valueOf("tcpKeepAlive"), true) // optional, default false - .option(Option.valueOf("tcpNoDelay"), true) // optional, default false - .option(Option.valueOf("compressionAlgorithms"), "zstd") // optional, default UNCOMPRESSED - .option(Option.valueOf("loopResources"), LoopResources.create("r2dbc")) // optional, default null, null means uses global tcp resources as loopResources (since 1.1.2) - .option(Option.valueOf("autodetectExtensions"), false) // optional, default false - .option(Option.valueOf("passwordPublisher"), Mono.just("password")) // optional, default null, null means has no passwordPublisher (since 1.0.5 / 0.9.6) - .build(); -ConnectionFactory connectionFactory = ConnectionFactories.get(options); - -// Creating a Mono using Project Reactor -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -Or use unix domain socket like following: - -```java -// Minimum configuration for unix domain socket -ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() - .option(DRIVER, "mysql") - .option(Option.valueOf("unixSocket"), "/path/to/mysql.sock") - .option(USER, "root") - .build(); -ConnectionFactory connectionFactory = ConnectionFactories.get(options); - -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -### Programmatic Configuration - -```java -MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() - .host("127.0.0.1") - .user("root") - .port(3306) // optional, default 3306 - .password("database-password-in-here") // optional, default null, null means has no password - .database("r2dbc") // optional, default null, null means not specifying the database - .createDatabaseIfNotExist(true) // optional, default false, create database if not exist (since 1.0.6 / 0.9.7) - .serverZoneId(ZoneId.of("Continent/City")) // optional, default null, null means query server time zone when connection init - .connectTimeout(Duration.ofSeconds(3)) // optional, default null, null means no timeout - .sslMode(SslMode.VERIFY_IDENTITY) // optional, default SslMode.PREFERRED - .sslCa("/path/to/mysql/ca.pem") // required when sslMode is VERIFY_CA or VERIFY_IDENTITY, default null, null means has no server CA cert - .sslCert("/path/to/mysql/client-cert.pem") // optional, default has no client SSL certificate - .sslKey("/path/to/mysql/client-key.pem") // optional, default has no client SSL key - .sslKeyPassword("key-pem-password-in-here") // optional, default has no client SSL key password - .tlsVersion(TlsVersions.TLS1_3, TlsVersions.TLS1_2, TlsVersions.TLS1_1) // optional, default is auto-selected by the server - .sslHostnameVerifier(MyVerifier.INSTANCE) // optional, default is null, null means use standard verifier - .sslContextBuilderCustomizer(MyCustomizer.INSTANCE) // optional, default is no-op customizer - .zeroDateOption(ZeroDateOption.USE_NULL) // optional, default ZeroDateOption.USE_NULL - .useServerPrepareStatement() // Use server-preparing statements, default use client-preparing statements - .allowLoadLocalInfileInPath("/opt") // optional, default null, null means LOCAL INFILE not be allowed - .tcpKeepAlive(true) // optional, controls TCP Keep Alive, default is false - .tcpNoDelay(true) // optional, controls TCP No Delay, default is false - .compressionAlgorithms(CompressionAlgorithm.ZSTD, CompressionAlgotihm.ZLIB) // optional, default is UNCOMPRESSED - .loopResources(LoopResources.create("r2dbc")) // optional, default null, null means uses global tcp resources as loopResources (since 1.1.2) - .autodetectExtensions(false) // optional, controls extension auto-detect, default is true - .extendWith(MyExtension.INSTANCE) // optional, manual extend an extension into extensions, default using auto-detect - .passwordPublisher(Mono.just("password")) // optional, default null, null means has no password publisher (since 1.0.5 / 0.9.6) - .build(); -ConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration); +ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbcs:mysql://root:database-password-in-here@127.0.0.1:3306/r2dbc"); // Creating a Mono using Project Reactor Mono connectionMono = Mono.from(connectionFactory.create()); ``` -Or use unix domain socket like following: - -```java -// Minimum configuration for unix domain socket -MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() - .unixSocket("/path/to/mysql.sock") - .user("root") - .build(); -ConnectionFactory connectionFactory = MySqlConnectionFactory.from(configuration); - -Mono connectionMono = Mono.from(connectionFactory.create()); -``` - -### Configuration items - -| name | valid values | required | description | -|---|---|---|---| -| driver | A constant "mysql" | Required in R2DBC discovery | This driver needs to be discovered by name in R2DBC | -| host | A hostname or IP | Required when `unixSocket` does not exists | The host of MySQL database server | -| unixSocket | An absolute or relative path | Required when `host` does not exists | The `.sock` file of Unix Domain Socket | -| port | A positive integer less than 65536 | Optional, default 3306 | The port of MySQL database server | -| user | A valid MySQL username and not be empty | Required | Who wants to connect to the MySQL database | -| password | Any printable string | Optional, default no password | The password of the MySQL database user | -| database | A valid MySQL database name | Optional, default does not initialize database | Database used by the MySQL connection | -| createDatabaseIfNotExist | `true` or `false` | Optional, default `false` | Create database if not exist | -| connectTimeout | A `Duration` which must be positive duration | Optional, default has no timeout | TCP connect timeout | -| serverZoneId | An id of `ZoneId` | Optional, default query time zone when connection init | Server time zone id | -| tcpKeepAlive | `true` or `false` | Optional, default disabled | Controls TCP KeepAlive | -| tcpNoDelay | `true` or `false` | Optional, default disabled | Controls TCP NoDelay | -| sslMode | A value of `SslMode` | Optional, default `PREFERRED` when using hosting connection, `DISABLED` when using Unix Domain Socket | SSL mode, see following notice | -| sslCa | A path of local file which type is `PEM` | Required when `sslMode` is `VERIFY_CA` or `VERIFY_IDENTITY` | The CA cert of MySQL database server | -| sslCert | A path of local file which type is `PEM` | Required when `sslKey` exists | The SSL cert of client | -| sslKey | A path of local file which type is `PEM` | Required when `sslCert` exists | The SSL key of client | -| sslKeyPassword | Any valid password for `PEM` file | Optional, default `sslKey` has no password | The password for client SSL key (i.e. `sslKey`) | -| tlsVersion | Any value list of `TlsVersions` | Optional, default is auto-selected by the server | The TLS version for SSL, see following notice | -| sslHostnameVerifier | A `HostnameVerifier` | Optional, default use RFC standard | Used only if `SslMode` is `VERIFY_CA` or higher | -| sslContextBuilderCustomizer | A `Function` | Optional, default is NO-OP function | Used only if `SslMode` is not `DISABLED` | -| zeroDateOption | Any value of `ZeroDateOption` | Optional, default `USE_NULL` | The option indicates "zero date" handling, see following notice | -| autodetectExtensions | `true` or `false` | Optional, default is `true` | Controls auto-detect `Extension`s | -| useServerPrepareStatement | `true`, `false` or `Predicate` | Optional, default is `false` | See following notice | -| allowLoadLocalInfileInPath | A path | Optional, default is `null` | The path that allows `LOAD DATA LOCAL INFILE` to load file data | -| compressionAlgorithms | A list of `CompressionAlgorithm` | Optional, default is `UNCOMPRESSED` | The compression algorithms for MySQL connection | -| loopResources | A `LoopResources` | Optional, default is `null` | The loop resources for MySQL connection | -| passwordPublisher | A `Publisher` | Optional, default is `null` | The password publisher, see following notice | - -- `SslMode` Considers security level and verification for SSL, make sure the database server supports SSL before you want change SSL mode to `REQUIRED` or higher. **The Unix Domain Socket only offers "DISABLED" available** - - `DISABLED` I don't care about security and don't want to pay the overhead for encryption - - `PREFERRED` I don't care about encryption but will pay the overhead of encryption if the server supports it. **Unavailable on Unix Domain Socket** - - `REQUIRED` I want my data to be encrypted, and I accept the overhead. I trust that the network will make sure I always connect to the server I want. **Unavailable on Unix Domain Socket** - - `VERIFY_CA` I want my data encrypted, and I accept the overhead. I want to be sure I connect to a server that I trust. **Unavailable on Unix Domain Socket** - - `VERIFY_IDENTITY` (the highest level, most like web browser): I want my data encrypted, and I accept the overhead. I want to be sure I connect to a server I trust, and that it's the one I specify. **Unavailable on Unix Domain Socket** - - `TUNNEL` Use SSL tunnel to connect to MySQL, it may be useful for some RDS that's using SSL proxy **Unavailable on Unix Domain Socket** -- `TlsVersions` Considers TLS version names for SSL, can be **multi-values** in the configuration, make sure the database server supports selected TLS versions. Usually sorted from higher to lower. **Unavailable on Unix Domain Socket** - - `TLS1` (i.e. "TLSv1") Under generic circumstances, MySQL database supports it if database supports SSL - - `TLS1_1` (i.e. "TLSv1.1") Under generic circumstances, MySQL database supports it if database supports SSL - - `TLS1_2` (i.e. "TLSv1.2") Supported only in Community Edition `8.0.4` or higher, and Enterprise Edition `5.6.0` or higher - - `TLS1_3` (i.e. "TLSv1.3") Supported only available as of MySQL `8.0.16` or higher, and requires compiling MySQL using OpenSSL `1.1.1` or higher -- `ZeroDateOption` Considers special handling when MySQL database server returning "zero date" (i.e. `0000-00-00 00:00:00`) - - `EXCEPTION` Just throw an exception when MySQL database server return "zero date" - - `USE_NULL` Use `null` when MySQL database server return "zero date" - - `USE_ROUND` **NOT** RECOMMENDED, only for compatibility. Use "round" date (i.e. `0001-01-01 00:00:00`) when MySQL database server return "zero date" -- Prepare Statement: Considers based on server-preparing or client-preparing, some database server maybe not support server-preparing binary-query, such as Vitess - - `useClientPrepareStatement()` default preparing mode, use client-preparing text-query for parametrized statements - - `useServerPrepareStatement()` use server-preparing binary-query for parametrized statements - - `useServerPrepareStatement(Predicate)` use server-preparing binary-query for parametrized statements, and enforce server-preparing usage for simple query (not parametrized statements). The usage is judged by `Predicate`, it's parameter is the simple SQL statement, enforce server-preparing if return `true` -- `extendWith` Manual extend `Extension`, only available in **programmatic configuration** - - It is **NOT** RECOMMENDED, enable the `autodetectExtensions` is the best way for extensions - - The `Extensions` will not remove duplicates, make sure it would be not extended twice or more - - The auto-detected `Extension`s will not affect manual extends and will not remove duplicates -- `passwordPublisher` Every time the client attempts to authenticate, it will use the password provided by the `passwordPublisher`.(Since `1.0.5` / `0.9.6`) e.g., You can employ this method for IAM-based authentication when connecting to an AWS Aurora RDS database. -- `compressionAlgorithms` Considers compression protocol for MySQL connection, it is **NOT** RECOMMENDED to use compression protocol in the general case, because it will increase the CPU usage and decrease the performance. - - `UNCOMPRESSED` (default) No compression - - `ZLIB` Use Zlib compression protocol, it is available on almost all MySQL versions (`5.x` and above) - - `ZSTD` Use Z-standard compression protocol, it is available since MySQL `8.0.18` or above, requires an extern dependency `com.github.luben:zstd-jni` - - For scenarios where the network environment is poor or the amount of data is always large, using a compression protocol may be useful -- `loopResources` Considers loop resources for MySQL connection. - -Should use `enum` in [Programmatic](#programmatic-configuration) configuration that not like discovery configurations, except `TlsVersions` (All elements of `TlsVersions` will be always `String` which is case-sensitive). +See [Getting Started](https://github.com/asyncer-io/r2dbc-mysql/wiki/getting-started) and [Configuration Options](https://github.com/asyncer-io/r2dbc-mysql/wiki/Configuration-Options) wiki for more information. ### Pooling @@ -400,180 +214,6 @@ The log format may be different for server-preparing and client-preparing. This generated statements and commands are different in these two modes. For example, a server-preparing statement has its statement ID that's generated by server, but client-preparing does not. -## Add a `Codec` - -This is an extension of a highly customized driver behavior of encoding parameter or decoding field data. - -Example for an extending `Codec` of JSON based-on Jackson. - -First, implement a `Codec`, `ParametrizedCodec`, `MassiveCodec` or `MassiveParametrizedCodec`. - -- `Codec` the normal codec - - Data type is `Class` - - Data buffer size is less than or equal to `Integer.MAX_VALUE` -- `ParametrizedCodec` - - Data type is `Class` or `ParametrizedType` - - Data buffer size is less than or equal to `Integer.MAX_VALUE` -- `MassiveCodec` - - Data type is `Class` - - Data buffer size is less than or equal to `UnsignedInteger.MAX_VALUE` (Java does not have unsigned integers, it just only represents the range) -- `MassiveParametrizedCodec` - - Data type is `Class` or `ParametrizedType` - - Data buffer size is less than or equal to `UnsignedInteger.MAX_VALUE` - -Actually, `JSON` can store large json data, and its byte size can be `UnsignedInteger.MAX_VALUE`. However, this is just an example. - -```java -public final class JacksonCodec implements Codec { - - /** - * JUST for example, should configure it in real applications. - */ - private static final ObjectMapper MAPPER = new ObjectMapper(); - - private final ByteBufAllocator allocator; - - /** - * Used for encoding/decoding mode, see also registrar in second step. - */ - private final boolean encoding; - - public JacksonCodec(ByteBufAllocator allocator, boolean encoding) { - this.allocator = allocator; - this.encoding = encoding; - } - - @Override - public Object decode(ByteBuf value, FieldInformation info, Class target, boolean binary, CodecContext context) { - // If you ensure server is using UTF-8, you can just use InputStream - try (Reader r = new InputStreamReader(new ByteBufInputStream(value), CharCollation.fromId(info.getCollationId(), context.getServerVersion()).getCharset())) { - return MAPPER.readValue(r, target); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public Parameter encode(Object value, CodecContext context) { - return new JacksonParameter(allocator, value, context); - } - - @Override - public boolean canDecode(FieldInformation info, Class target) { - return !encoding && info.getType() == DataTypes.JSON && info.getCollationId() != CharCollation.BINARY_ID; - } - - @Override - public boolean canEncode(Object value) { - return encoding; - } - - private static final class JacksonParameter implements MySqlParameter { - - private final ByteBufAllocator allocator; - - private final Object value; - - private final CodecContext context; - - private JacksonParameter(ByteBufAllocator allocator, Object value, CodecContext context) { - this.allocator = allocator; - this.value = value; - this.context = context; - } - - @Override - public Mono publishBinary() { - // JSON in binary protocol should be a var-integer sized encoded string. - // That means we should write a var-integer as a size of following content - // bytes firstly, then write the encoded string as content. - // - // Binary protocol may be different for each type of encoding, so if do not - // use binary protocol, just return a Mono.error() instead. - return Mono.fromSupplier(() -> { - Charset charset = context.getClientCollation().getCharset(); - ByteBuf content = allocator.buffer(); - - // Encode and calculate content bytes first, we should know bytes size. - try (Writer w = new OutputStreamWriter(new ByteBufOutputStream(content), charset)) { - MAPPER.writeValue(w, value); - } catch (IOException e) { - content.release(); - throw new CustomRuntimeException(e); - } catch (Throwable e) { - content.release(); - throw e; - } - - ByteBuf buf = null; - try { - buf = allocator.buffer(); - // VarIntUtils is an unstable, internal utility. - VarIntUtils.writeVarInt(buf, content.readableBytes()); - return buf.writeBytes(content); - } catch (Throwable e) { - if (buf != null) { - buf.release(); - } - throw e; - } finally { - content.release(); - } - }); - } - - @Override - public Mono publishText(ParameterWriter writer) { - return Mono.fromRunnable(() -> { - try { - MAPPER.writeValue(writer, value); - } catch (IOException e) { - throw new CustomRuntimeException(e); - } - }); - } - - @Override - public short getType() { - return DataTypes.VARCHAR; - } - - /** - * Optional, for statements/parameters logging. - */ - @Override - public String toString() { - return value.toString(); - } - } -} -``` - -Second, implement a `CodecRegistrar`. - -```java -// It is just an example of package name and does not represent any company, individual or organization. -package org.example.demo.json; - -// Some imports... - -public final class JacksonCodecRegistrar implements CodecRegistrar { - - @Override - public void register(ByteBufAllocator allocator, CodecRegistry registry) { - // Decoding JSON by highest priority, encoding anything by lowest priority. - registry.addFirst(new JacksonCodec(allocator, false)) - .addLast(new JacksonCodec(allocator, true)); - } -} -``` - -Finally, create a file in `META-INF/services`, which file name is `io.asyncer.r2dbc.mysql.extension.Extension`, it contains this line: - -``` -org.example.demo.json.JacksonCodecRegistrar -``` - ## Reporting Issues The R2DBC MySQL Implementation uses GitHub as issue tracking system to record bugs and feature requests. From ed09a327fafb3b16d0780afa8c8c167f4e911ba9 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 15 Feb 2024 20:39:18 +0900 Subject: [PATCH 14/93] Slim README and link to wiki (#234) Motivation: See also #213 . Modification: Simplify README and migrate cumbersome content to wiki. Result: Slimed README --- README.md | 130 +----------------------------------------------------- 1 file changed, 2 insertions(+), 128 deletions(-) diff --git a/README.md b/README.md index 1d1ca09be..0badecc56 100644 --- a/README.md +++ b/README.md @@ -103,116 +103,14 @@ See [Getting Started](https://github.com/asyncer-io/r2dbc-mysql/wiki/getting-sta See [r2dbc-pool](https://github.com/r2dbc/r2dbc-pool). -### Simple statement +### Usage ```java connection.createStatement("INSERT INTO `person` (`first_name`, `last_name`) VALUES ('who', 'how')") .execute(); // return a Publisher include one Result ``` -### Parametrized statement - -```java -connection.createStatement("INSERT INTO `person` (`birth`, `nickname`, `show_name`) VALUES (?, ?name, ?name)") - .bind(0, LocalDateTime.of(2019, 6, 25, 12, 12, 12)) - .bind("name", "Some one") // Not one-to-one binding, call twice of native index-bindings, or call once of name-bindings. - .add() - .bind(0, LocalDateTime.of(2009, 6, 25, 12, 12, 12)) - .bind(1, "My Nickname") - .bind(2, "Naming show") - .returnGeneratedValues("generated_id") - .execute(); // return a Publisher include two Results. -``` - -- All parameters must be bound before execute, even parameter is `null` (use `bindNull` to bind `null`). -- It will be using client-preparing by default, see `useServerPrepareStatement` in configuration. -- In one-to-one binding, because native MySQL prepared statements use index-based parameters, *index-bindings* will have **better** performance than *name-bindings*. - -### Batch statement - -```java -connection.createBatch() - .add("INSERT INTO `person` (`first_name`, `last_name`) VALUES ('who', 'how')") - .add("UPDATE `earth` SET `count` = `count` + 1 WHERE `id` = 'human'") - .execute(); // return a Publisher include two Results. -``` - -> The last `;` will be removed if and only if last statement contains ';', and statement has only whitespace follow the last `;`. - -### Transactions - -```java -connection.beginTransaction() - .then(Mono.from(connection.createStatement("INSERT INTO `person` (`first_name`, `last_name`) VALUES ('who', 'how')").execute())) - .flatMap(Result::getRowsUpdated) - .thenMany(connection.createStatement("INSERT INTO `person` (`birth`, `nickname`, `show_name`) VALUES (?, ?name, ?name)") - .bind(0, LocalDateTime.of(2019, 6, 25, 12, 12, 12)) - .bind("name", "Some one") - .add() - .bind(0, LocalDateTime.of(2009, 6, 25, 12, 12, 12)) - .bind(1, "My Nickname") - .bind(2, "Naming show") - .returnGeneratedValues("generated_id") - .execute()) - .flatMap(Result::getRowsUpdated) - .then(connection.commitTransaction()); -``` - -## Data Type Mapping - -The default built-in `Codec`s reference table shows the type mapping between [MySQL][m] and Java data types: - -| MySQL Type | Unsigned | Support Data Type | -|---|---|---| -| `INT` | `UNSIGNED` | [**`Long`**][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `INT` | `SIGNED` | [**`Integer`**][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `TINYINT` | `UNSIGNED` | [**`Short`**][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref], [`Boolean`][java-Boolean-ref] (Size is 1) | -| `TINYINT` | `SIGNED` | [**`Byte`**][java-Byte-ref], [`Short`][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref], [`Boolean`][java-Boolean-ref] (Size is 1) | -| `SMALLINT` | `UNSIGNED` | [**`Integer`**][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `SMALLINT` | `SIGNED` | [**`Short`**][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `MEDIUMINT` | `SIGNED/UNSIGNED` | [**`Integer`**][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `BIGINT` | `UNSIGNED` | [**`BigInteger`**][java-BigInteger-ref], [`Long`][java-Long-ref] (Not check overflow) | -| `BIGINT` | `SIGNED` | [**`Long`**][java-Long-ref], [`BigInteger`][java-BigInteger-ref] | -| `FLOAT` | `SIGNED` / `UNSIGNED` | [**`Float`**][java-Float-ref], [`BigDecimal`][java-BigDecimal-ref] | -| `DOUBLE` | `SIGNED` / `UNSIGNED` | [**`Double`**][java-Double-ref], [`BigDecimal`][java-BigDecimal-ref] | -| `DECIMAL` | `SIGNED` / `UNSIGNED` | [**`BigDecimal`**][java-BigDecimal-ref], [`Float`][java-Float-ref] (Size less than 7), [`Double`][java-Double-ref] (Size less than 16) | -| `BIT` | - | [**`ByteBuffer`**][java-ByteBuffer-ref], [`BitSet`][java-BitSet-ref], [`Boolean`][java-Boolean-ref] (Size is 1), `byte[]` | -| `DATETIME` / `TIMESTAMP` | - | [**`LocalDateTime`**][java-LocalDateTime-ref], [`ZonedDateTime`][java-ZonedDateTime-ref], [`OffsetDateTime`][java-OffsetDateTime-ref], [`Instant`][java-Instant-ref] | -| `DATE` | - | [**`LocalDate`**][java-LocalDate-ref] | -| `TIME` | - | [**`LocalTime`**][java-LocalTime-ref], [`Duration`][java-Duration-ref], [`OffsetTime`][java-OffsetTime-ref] | -| `YEAR` | - | [**`Short`**][java-Short-ref], [`Integer`][java-Integer-ref], [`Long`][java-Long-ref], [`BigInteger`][java-BigInteger-ref], [`Year`][java-Year-ref] | -| `VARCHAR` / `NVARCHAR` | - | [**`String`**][java-String-ref] | -| `VARBINARY` | - | [**`ByteBuffer`**][java-ByteBuffer-ref], `Blob`, `byte[]` | -| `CHAR` / `NCHAR` | - | [**`String`**][java-String-ref] | -| `ENUM` | - | [**`String`**][java-String-ref], [`Enum`][java-Enum-ref] | -| `SET` | - | **`String[]`**, [`String`][java-String-ref], [`Set`][java-Set-ref] and [`Set>`][java-Set-ref] ([`Set`][java-Set-ref] need use [`ParameterizedType`][java-ParameterizedType-ref]) | -| `BLOB`s (`LONGBLOB`, etc.) | - | [**`ByteBuffer`**][java-ByteBuffer-ref], `Blob`, `byte[]` | -| `TEXT`s (`LONGTEXT`, etc.) | - | [**`String`**][java-String-ref], `Clob` | -| `JSON` | - | [**`String`**][java-String-ref], `Clob` | -| `GEOMETRY` | - | **`byte[]`**, `Blob` | - -## Statements Logging/Debugging - -Use the `io.asyncer.r2dbc.mysql.QUERY` logger and the `DEBUG` log level to log statements and their bound -parameters (if it is prepared statement). - -For example, in `logback.xml`: - -```xml - - - - - - -``` - -Note that it will print the SQL statement and all parameters, so this may be a security risk. Don't use it -in an environment with sensitive data. It should be used for debugging purposes only. - -The log format may be different for server-preparing and client-preparing. This is because the actual -generated statements and commands are different in these two modes. For example, a server-preparing statement -has its statement ID that's generated by server, but client-preparing does not. +See [Usage](https://github.com/asyncer-io/r2dbc-mysql/wiki/usage) wiki for more information. ## Reporting Issues @@ -255,27 +153,3 @@ Thanks a lot for your support! projects. [m]: https://www.mysql.com -[java-BigDecimal-ref]: https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html -[java-BigInteger-ref]: https://docs.oracle.com/javase/8/docs/api/java/math/BigInteger.html -[java-BitSet-ref]: https://docs.oracle.com/javase/8/docs/api/java/util/BitSet.html -[java-Boolean-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Boolean.html -[java-Byte-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Byte.html -[java-ByteBuffer-ref]: https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html -[java-Double-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Double.html -[java-Float-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Float.html -[java-Integer-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Integer.html -[java-Long-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Long.html -[java-LocalDateTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDateTime.html -[java-ZonedDateTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/ZonedDateTime.html -[java-OffsetDateTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/OffsetDateTime.html -[java-Instant-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/Instant.html -[java-LocalDate-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalDate.html -[java-Duration-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/Duration.html -[java-LocalTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/LocalTime.html -[java-OffsetTime-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/OffsetTime.html -[java-Year-ref]: https://docs.oracle.com/javase/8/docs/api/java/time/Year.html -[java-Short-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Short.html -[java-Enum-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/Enum.html -[java-String-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/String.html -[java-Set-ref]: https://docs.oracle.com/javase/8/docs/api/java/util/Set.html -[java-ParameterizedType-ref]: https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/ParameterizedType.html From 087f412c18f12beef6cca1239efeb650ae2bcea2 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Fri, 16 Feb 2024 11:38:57 +0900 Subject: [PATCH 15/93] Polishing and fix typo --- .../io/asyncer/r2dbc/mysql/Capability.java | 6 +- .../asyncer/r2dbc/mysql/ColumnDefinition.java | 2 +- .../r2dbc/mysql/ConnectionContext.java | 4 +- .../asyncer/r2dbc/mysql/MySqlConnection.java | 10 +-- .../mysql/MySqlConnectionConfiguration.java | 55 ++++++++------- .../r2dbc/mysql/MySqlConnectionFactory.java | 2 +- .../io/asyncer/r2dbc/mysql/MySqlNames.java | 20 +++--- .../asyncer/r2dbc/mysql/MySqlParameter.java | 6 +- .../r2dbc/mysql/MySqlSslConfiguration.java | 2 +- .../asyncer/r2dbc/mysql/ParameterWriter.java | 10 +-- .../mysql/ParametrizedStatementSupport.java | 2 +- .../java/io/asyncer/r2dbc/mysql/Query.java | 2 +- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 2 +- .../r2dbc/mysql/authentication/AuthUtils.java | 2 +- .../authentication/MySqlAuthProvider.java | 2 +- .../mysql/client/DefaultHostnameVerifier.java | 4 +- .../mysql/client/ReactorNettyClient.java | 2 +- .../io/asyncer/r2dbc/mysql/codec/Codec.java | 10 +-- .../io/asyncer/r2dbc/mysql/codec/Codecs.java | 5 +- .../asyncer/r2dbc/mysql/codec/DateTimes.java | 2 +- .../r2dbc/mysql/codec/DefaultCodecs.java | 14 ++-- .../r2dbc/mysql/codec/ParametrizedCodec.java | 6 +- .../r2dbc/mysql/codec/PrimitiveCodec.java | 8 +-- .../r2dbc/mysql/collation/CharsetTarget.java | 2 +- .../mysql/constant/ColumnDefinitions.java | 69 ------------------- .../r2dbc/mysql/constant/MySqlType.java | 11 ++- .../mysql/internal/util/FluxEnvelope.java | 4 +- .../mysql/internal/util/VarIntUtils.java | 2 +- .../r2dbc/mysql/message/FieldValue.java | 6 +- .../r2dbc/mysql/message/LargeFieldValue.java | 2 +- .../message/client/HandshakeResponse41.java | 2 +- .../mysql/message/client/ParamWriter.java | 4 +- .../mysql/message/server/Eof320Message.java | 2 +- .../mysql/message/server/Eof41Message.java | 2 +- .../message/server/HandshakeRequest.java | 10 +-- .../message/server/LargeFieldReader.java | 2 +- .../server/PrepareQueryDecodeContext.java | 2 +- .../message/server/ServerMessageDecoder.java | 8 +-- .../PrepareParametrizedStatementTest.java | 3 +- .../mysql/QueryIntegrationTestSupport.java | 1 - .../mysql/TextParametrizedStatementTest.java | 3 +- .../mysql/client/ZlibCompressorTest.java | 2 - .../asyncer/r2dbc/mysql/codec/CodecsTest.java | 3 +- 43 files changed, 115 insertions(+), 203 deletions(-) delete mode 100644 src/main/java/io/asyncer/r2dbc/mysql/constant/ColumnDefinitions.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java b/src/main/java/io/asyncer/r2dbc/mysql/Capability.java index 67ebc3711..50b965ad4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/Capability.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/Capability.java @@ -216,9 +216,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; @@ -236,7 +236,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; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java b/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java index 2ab077c0c..62a565159 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java @@ -79,7 +79,7 @@ public boolean isNotNull() { /** * 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 + * Note: IEEE-754 floating types (e.g. DOUBLE/FLOAT) do not support it in MySQL 8.0+. When creating a * column as an unsigned floating type, the server may report a warning. * * @return if value is an unsigned number. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index f8f594fdc..0445ff914 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -30,8 +30,8 @@ /** * The MySQL connection context considers the behavior of server or client. *

- * WARNING: Do NOT change any data outside of this project, try configure {@code ConnectionFactoryOptions} or - * {@code MySqlConnectionConfiguration} to control connection context and client behavior. + * 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 { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java index 049d351ab..199f92cf1 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java @@ -618,17 +618,13 @@ private static ZoneId convertZoneId(String id) { try { switch (realId) { case "Factory": - // Looks like the "Factory" time zone is UTC. + // It seems like UTC. return ZoneOffset.UTC; case "America/Nuuk": - // They are same timezone including DST. + // America/Godthab is the same as America/Nuuk, with DST. return ZoneId.of("America/Godthab"); case "ROC": - // Republic of China, 1912-1949, very very old time zone. - // Even the ZoneId.SHORT_IDS does not support it. - // Is there anyone using this time zone, really? - // Don't think so, but should support it for compatible. - // Just use GMT+8, id is equal to +08:00. + // It is equal to +08:00. return ZoneId.of("+8"); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index fdfce2fd9..60ea6ae3b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -48,7 +48,7 @@ import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_STRINGS; /** - * MySQL configuration of connection. + * A configuration of MySQL connection. */ public final class MySqlConnectionConfiguration { @@ -461,7 +461,7 @@ public MySqlConnectionConfiguration build() { } /** - * 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}. @@ -473,7 +473,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. @@ -486,7 +486,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}. @@ -500,7 +500,7 @@ public Builder unixSocket(String unixSocket) { } /** - * Configure the host. + * Configures the host. * * @param host the host. * @return this {@link Builder}. @@ -514,7 +514,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. * @@ -528,7 +528,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}. @@ -543,9 +543,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 */ @@ -555,7 +555,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}. @@ -580,8 +580,7 @@ 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 of server. Default to query server time zone in initialization. * * @param serverZoneId the {@link ZoneId}, or {@code null} if query in initialization. * @return this {@link Builder}. @@ -593,8 +592,8 @@ public Builder serverZoneId(@Nullable ZoneId serverZoneId) { } /** - * 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}. @@ -607,7 +606,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}. @@ -620,7 +619,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}. @@ -643,7 +642,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. *

@@ -661,7 +660,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. @@ -676,7 +675,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}. * @@ -690,7 +689,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}. * @@ -704,7 +703,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. * @@ -719,7 +718,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 @@ -739,7 +738,7 @@ public Builder sslContextBuilderCustomizer( } /** - * Configure TCP KeepAlive. + * Configures TCP KeepAlive. * * @param enabled whether to enable TCP KeepAlive * @return this {@link Builder} @@ -752,7 +751,7 @@ public Builder tcpKeepAlive(boolean enabled) { } /** - * Configure TCP NoDelay. + * Configures TCP NoDelay. * * @param enabled whether to enable TCP NoDelay * @return this {@link Builder} @@ -765,7 +764,7 @@ public Builder tcpNoDelay(boolean enabled) { } /** - * Configure the protocol of parametrized statements to the text protocol. + * Configures the protocol of parametrized statements to the text protocol. *

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

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

* The {@code preferPrepareStatement} configures whether to prefer prepare execution on a @@ -816,7 +815,7 @@ public Builder useServerPrepareStatement(Predicate preferPrepareStatemen } /** - * Configure the session variables, used to set session variables immediately after login. Default no + * 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"]}. * diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index a8b49a4df..d29bdb968 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -158,7 +158,7 @@ private static Mono getMySqlConnection( }) .flatMap(client -> { ByteBufAllocator allocator = client.getByteBufAllocator(); - CodecsBuilder builder = Codecs.builder(allocator); + CodecsBuilder builder = Codecs.builder(); PrepareCache prepareCache = Caches.createPrepareCache(prepareCacheSize); String db = createDbIfNotExist ? database : ""; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java index 40027ea04..29d85dfe3 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java @@ -67,7 +67,7 @@ private static int binarySearch(String[] names, String name, boolean ignoreCase) left = middle + 1; if (compared == -2) { - // Match succeed if case insensitive, always use last + // Match succeed if case-insensitive, always use last // matched result that will be closer to `name`. ciResult = middle; } @@ -75,12 +75,12 @@ private static int binarySearch(String[] names, String name, boolean ignoreCase) right = middle - 1; if (compared == 2) { - // Match succeed if case insensitive, always use last + // Match succeed if case-insensitive, always use last // matched result that will be closer to `name`. ciResult = middle; } } else { - // Match succeed when case sensitive, just return. + // Match succeed when case-sensitive. return middle; } } @@ -90,9 +90,9 @@ private static int binarySearch(String[] names, String name, boolean ignoreCase) /** * Compares double strings and return an integer of both difference. If the integer is {@code 0} means - * both strings equals even case sensitive, absolute value is {@code 2} means it is equals by case - * insensitive but not equals when case sensitive, absolute value is {@code 4} means it is not equals even - * case insensitive. + * both strings equals even case-sensitive, absolute value is {@code 2} means it is equals by + * case-insensitive but not equals when case-sensitive, absolute value is {@code 4} means it is not equals + * even case-insensitive. *

* Note: visible for unit tests. * @@ -117,7 +117,7 @@ private static int compare0(String left, String right, int start, int end) { if (leftCh != rightCh) { if (csCompared == 0) { - // Compare end if is case sensitive comparator. + // Compare end if is case-sensitive comparator. csCompared = leftCh - rightCh; } @@ -126,18 +126,18 @@ private static int compare0(String left, String right, int start, int end) { rightCh = Character.toLowerCase(rightCh); if (leftCh != rightCh) { - // Not equals even case insensitive. + // Not equals even case-insensitive. return leftCh < rightCh ? -4 : 4; } } } - // Length not equals means both strings not equals even case insensitive. + // Length not equals means both strings not equals even case-insensitive. if (leftSize != rightSize) { return leftSize < rightSize ? -4 : 4; } - // Equals when case insensitive, use case sensitive. + // Equals when case-insensitive, use case-sensitive. return csCompared < 0 ? -2 : (csCompared > 0 ? 2 : 0); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java index 08df22a4c..25088dff4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java +++ b/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/MySqlSslConfiguration.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java index 2a4b1c0fa..d76662f40 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java +++ b/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/ParameterWriter.java b/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java index 62ac43e29..fa52a1f69 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java @@ -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/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java index ffe203077..98ce359e4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java @@ -34,7 +34,7 @@ /** * Base class considers parametrized {@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 { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/Query.java b/src/main/java/io/asyncer/r2dbc/mysql/Query.java index 1fe94ee54..49c063e61 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/Query.java +++ b/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/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 4ad301974..5efe0be34 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -270,7 +270,7 @@ static Mono beginTransaction(Client client, ConnectionState state, boolean * * @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. */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java b/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java index e3a952ee6..2a9cd84e8 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java @@ -25,7 +25,7 @@ import static io.asyncer.r2dbc.mysql.constant.Packets.TERMINAL; /** - * An utility for general authentication hashing algorithm. + * A utility for general authentication hashing algorithm. */ final class AuthUtils { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java index 41f640d0a..2ae157271 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java @@ -68,7 +68,7 @@ public interface MySqlAuthProvider { /** * Get the built-in authentication plugin provider through the specified {@code type}. * - * @param type the type name of a authentication plugin provider + * @param type the type name of an authentication plugin provider * @return the authentication plugin provider * @throws R2dbcPermissionDeniedException the {@code type} name not found */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java b/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java index 8b58a7547..39dc784c1 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java @@ -124,7 +124,7 @@ private static boolean matchIpv6(String ip, List sans) { String host = normaliseIpv6(ip); for (San san : sans) { - // IP must be case sensitive. + // IP must be case-sensitive. if (San.IP == san.getType() && host.equals(normaliseIpv6(san.getValue()))) { if (LOG_DEBUG) { logger.debug("Certificate for '{}' matched IPv6 '{}' of the Subject Alternative Names", @@ -231,7 +231,7 @@ private static boolean matchHost(String host, String pattern) { int remainderIndex = host.length() - postfixSize; if (remainderIndex <= asteriskIndex) { - // Asterisk must to match least one character. + // The asterisk must match at least one character. // In other words: groups.*.example.com can not match groups..example.com return false; } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index 8abf17c10..b9a12f3cc 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -266,7 +266,7 @@ private static void resetSequence(Connection connection) { @Override public String toString() { return String.format("ReactorNettyClient(%s){connectionId=%d}", - isConnected() ? "activating" : "clsoing or closed", context.getConnectionId()); + isConnected() ? "activating" : "closing or closed", context.getConnectionId()); } private void emitNextRequest(ClientMessage request) { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java b/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java index daaf6b64f..eb184fc20 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java @@ -31,7 +31,7 @@ public interface Codec { /** - * Decode a {@link ByteBuf} as specified {@link Class}. + * Decodes a {@link ByteBuf} as specified {@link Class}. * * @param value the {@link ByteBuf}. * @param metadata the metadata of the column. @@ -45,19 +45,19 @@ T decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean b CodecContext context); /** - * Check if can decode the field value as specified {@link Class}. + * Checks if the field value can be decoded as specified {@link Class}. * * @param metadata the metadata of the column. * @param target the specified {@link Class}. - * @return if can decode. + * @return if it can decode. */ boolean canDecode(MySqlColumnMetadata metadata, Class target); /** - * Check if can encode the specified value. + * Checks if it can encode the specified value. * * @param value the specified value. - * @return if can encode. + * @return if it can encode. */ boolean canEncode(Object value); diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java b/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java index aff4799b0..45129a26f 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java @@ -93,10 +93,9 @@ T decode(FieldValue value, MySqlColumnMetadata metadata, ParameterizedType t /** * Create a builder from a {@link ByteBufAllocator}. * - * @param allocator the {@link ByteBufAllocator}. * @return a {@link CodecsBuilder}. */ - static CodecsBuilder builder(ByteBufAllocator allocator) { - return new DefaultCodecs.Builder(allocator); + static CodecsBuilder builder() { + return new DefaultCodecs.Builder(); } } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java b/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java index 480eafa06..3d4a23e43 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java @@ -27,7 +27,7 @@ import java.time.temporal.Temporal; /** - * An utility considers date/time generic logic for {@link Codec} implementations. + * A utility considers date/time generic logic for {@link Codec} implementations. */ final class DateTimes { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index c8049308a..9bdbe5787 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -275,7 +275,7 @@ private static Class chooseClass(MySqlColumnMetadata metadata, Class type) return type.isAssignableFrom(javaType) ? javaType : type; } - private static Codec[] defaultCodecs(ByteBufAllocator allocator) { + private static Codec[] defaultCodecs() { return new Codec[] { ByteCodec.INSTANCE, ShortCodec.INSTANCE, @@ -318,23 +318,17 @@ private static Codec[] defaultCodecs(ByteBufAllocator allocator) { static final class Builder implements CodecsBuilder { - private final ByteBufAllocator allocator; - @GuardedBy("lock") private final ArrayList> codecs = new ArrayList<>(); private final ReentrantLock lock = new ReentrantLock(); - Builder(ByteBufAllocator allocator) { - this.allocator = allocator; - } - @Override public CodecsBuilder addFirst(Codec codec) { lock.lock(); try { if (codecs.isEmpty()) { - Codec[] defaultCodecs = defaultCodecs(allocator); + Codec[] defaultCodecs = defaultCodecs(); codecs.ensureCapacity(defaultCodecs.length + 1); // Add first. @@ -354,7 +348,7 @@ public CodecsBuilder addLast(Codec codec) { lock.lock(); try { if (codecs.isEmpty()) { - codecs.addAll(InternalArrays.asImmutableList(defaultCodecs(allocator))); + codecs.addAll(InternalArrays.asImmutableList(defaultCodecs())); } codecs.add(codec); } finally { @@ -369,7 +363,7 @@ public Codecs build() { try { try { if (codecs.isEmpty()) { - return new DefaultCodecs(defaultCodecs(allocator)); + return new DefaultCodecs(defaultCodecs()); } return new DefaultCodecs(codecs.toArray(new Codec[0])); } finally { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java b/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java index 966dfe189..9c43bedb4 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java @@ -32,7 +32,7 @@ public interface ParametrizedCodec extends Codec { /** - * Decode a {@link ByteBuf} as specified {@link ParameterizedType}. + * Decodes a {@link ByteBuf} as specified {@link ParameterizedType}. * * @param value the {@link ByteBuf}. * @param metadata the metadata of the column. @@ -46,11 +46,11 @@ Object decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedType tar CodecContext context); /** - * Check if can decode the field value as specified {@link ParameterizedType}. + * Checks if the field value can be decoded as specified {@link ParameterizedType}. * * @param metadata the metadata of the column. * @param target the specified {@link ParameterizedType}. - * @return if can decode. + * @return if it can decode. */ boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java b/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java index c5bfb2408..3e2c90691 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java @@ -30,7 +30,7 @@ interface PrimitiveCodec extends Codec { /** - * Decode a {@link ByteBuf} as specified {@link Class}. + * Decodes a {@link ByteBuf} as specified {@link Class}. * * @param value the {@link ByteBuf}. * @param metadata the metadata of the column. @@ -44,15 +44,15 @@ T decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean b CodecContext context); /** - * Check if can decode the field value as a primitive data. + * Checks if the field value can be decoded as a primitive data. * * @param metadata the metadata of the column. - * @return if can decode. + * @return if it can decode. */ boolean canPrimitiveDecode(MySqlColumnMetadata metadata); /** - * Get the primitive {@link Class}, such as {@link Integer#TYPE}, etc. + * Gets the primitive {@link Class}, such as {@link Integer#TYPE}, etc. * * @return the primitive {@link Class}. */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java b/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java index 8b78cb24b..2a17502a0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java @@ -20,7 +20,7 @@ import java.nio.charset.UnsupportedCharsetException; /** - * MySQL character collation target of {@link Charset}. + * A character collation {@link Charset} target of MySQL. */ interface CharsetTarget { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/ColumnDefinitions.java b/src/main/java/io/asyncer/r2dbc/mysql/constant/ColumnDefinitions.java deleted file mode 100644 index a5e98d257..000000000 --- a/src/main/java/io/asyncer/r2dbc/mysql/constant/ColumnDefinitions.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.constant; - -/** - * Column definition flags. - */ -public final class ColumnDefinitions { - - /** - * The data is not null. - */ - public static final int NOT_NULL = 1; - -// public static final int PRIMARY_PART = 1 << 1; // This field is a part of the primary key -// public static final int UNIQUE_PART = 1 << 2; // This field is a part of a unique key -// public static final int KEY_PART = 1 << 3; // This field is a part of a normal key -// public static final int BLOB = 1 << 4; - - /** - * The data is an unsigned number. Only applicable to numeric types, like BIGINT UNSIGNED, INT UNSIGNED, - * etc. - *

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

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

- * Note: in order to be compatible with older drivers, MySQL server will send type as VARCHAR for type - * SET. If this flag is enabled, change data type to SET. - */ - public static final int SET = 1 << 11; // type is set - -// public static final int NO_DEFAULT = 1 << 12; // column has no default value -// public static final int ON_UPDATE_NOW = 1 << 13; // field will be set to NOW() in UPDATE statement - - private ColumnDefinitions() { } -} diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java b/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java index 8628966db..196b70cf0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java @@ -146,7 +146,7 @@ public int getBinarySize() { }, /** - * A IEEE-754 single-precision floating point number type. It cannot be unsigned when the server version + * An IEEE-754 single-precision floating point number type. It cannot be unsigned when the server version * is 8.0 or higher. Otherwise, the server will report a warning when defining the column. */ FLOAT(MySqlType.ID_FLOAT, Float.class) { @@ -167,7 +167,7 @@ public int getBinarySize() { }, /** - * A IEEE-754 double-precision floating point number type. It cannot be unsigned when the server version + * An IEEE-754 double-precision floating point number type. It cannot be unsigned when the server version * is 8.0 or higher. Otherwise, the server will report a warning when defining the column. */ DOUBLE(MySqlType.ID_DOUBLE, Double.class) { @@ -332,7 +332,7 @@ public boolean isBinary() { }, /** - * A enumerable string type. It is a virtual type, server will enabled {@code ENUMERABLE} in column + * An enumerable string type. It is a virtual type, server will enabled {@code ENUMERABLE} in column * definitions and type is as {@link #VARCHAR}. */ ENUM(MySqlType.ID_ENUM, String.class) { @@ -661,7 +661,7 @@ public boolean isBinary() { } /** - * Get the fixed byte size of the data type in the binary protocol, otherwise {@literal 0} means that + * Gets the fixed byte size of the data type in the binary protocol, otherwise {@literal 0} means that * there is no fixed size. * * @return the fixed size in binary protocol. @@ -729,10 +729,9 @@ public static MySqlType of(int id, ColumnDefinition definition) { case ID_BLOB: return definition.isBinary() ? BLOB : TEXT; case ID_GEOMETRY: - // Most of Geometry libraries were using byte[] to encode/decode which based on WKT + // Most Geometry libraries were using byte[] to encode/decode which based on WKT // (includes Extended-WKT) or WKB // MySQL using WKB for encoding/decoding, so use byte[] instead of ByteBuffer by default type. - // It maybe change after R2DBC SPI specify default type for GEOMETRY. return GEOMETRY; default: return UNKNOWN; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java index 895b64f27..bfcb57a0b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java @@ -269,7 +269,7 @@ public void onComplete() { ByteBuf cumulated = this.cumulated; this.cumulated = null; - // The protocol need least one envelope, and the last must small than maximum size of envelopes. + // The protocol need least one envelope, and the last must smaller than maximum size of envelopes. // - If there has no previous envelope, then the cumulated is null, should produce an empty // envelope header. // - If previous envelope is a max-size envelope, then the cumulated is null, should produce an @@ -367,7 +367,7 @@ private static ByteBuf cumulate(ByteBufAllocator alloc, @Nullable ByteBuf cumula .setBytes(oldBytes, buf, buf.readerIndex(), bufBytes) .writerIndex(newBytes); buf.readerIndex(buf.writerIndex()); - // Release the old cumulated If write succeed (return will be succeed). + // Release the old cumulated If write succeed. releasing = cumulated; return result; diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java index 76bcec9fb..ac6959c40 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java @@ -178,7 +178,7 @@ public static int varIntBytes(int value) { * Reserve a seat of an unknown var integer in {@code buf} header. *

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

- * WARNING: it is sealed interface, should NEVER extends or implemented by another interface or class. + * WARNING: it is sealed interface, should NEVER extend or implemented by another interface or class. */ public interface FieldValue extends ReferenceCounted { /** - * Check if value is {@code null}. + * Checks if value is {@code null}. * * @return if value is {@code null}. */ @@ -36,7 +36,7 @@ default boolean isNull() { } /** - * Get an instance for {@code null} value. + * Gets an instance for {@code null} value. * * @return a field contains a {@code null} value. */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java b/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java index f9d334f48..2459cf272 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java @@ -28,7 +28,7 @@ /** * An implementation of {@link FieldValue} considers large field value which bytes width/size is greater than - * {@link Integer#MAX_VALUE}, it would be exists when MySQL server return LOB types (i.e. BLOB, CLOB), + * {@link Integer#MAX_VALUE}, it is used by the MySQL server returns LOB types (i.e. BLOB, CLOB), e.g. * LONGTEXT length can be unsigned int32. * * @see FieldValue diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java index 810126bba..40bbd5ce0 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java @@ -123,7 +123,7 @@ protected void writeTo(ByteBuf buf, ConnectionContext context) { } if (capability.isPluginAuthAllowed()) { - // This must be an UTF-8 string. + // This must be a UTF-8 string. HandshakeResponse.writeCString(buf, authType, StandardCharsets.UTF_8); } diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java b/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java index 04a99754e..875e5e18d 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java @@ -34,7 +34,7 @@ /** * A default implementation of {@link ParameterWriter}. *

- * WARNING: It is not safe for multithreaded access. + * WARNING: It is not thread safe */ final class ParamWriter extends ParameterWriter { @@ -324,7 +324,7 @@ private void escape(char c) { break; case '\'': // MySQL will auto-combine consecutive strings, like '1''2' -> '12'. - // Sure, there can use "\\'", but this will be better. (For some logging systems) + // Sure, there can use '1\'2', but this will be better. (For some logging systems) builder.append('\'').append('\''); break; // Maybe useful in the future, keep '"' here. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java index a06751fcd..83aa23306 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.server; /** - * A EOF message for current context in protocol 3.20. + * An EOF message for current context in protocol 3.20. *

* Note: It is also Old Change Authentication Request. */ diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java index c134f804b..489870fe8 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java @@ -20,7 +20,7 @@ import io.netty.buffer.ByteBuf; /** - * A EOF message for current context in protocol 4.1. + * An EOF message for current context in protocol 4.1. */ final class Eof41Message implements EofMessage, WarningMessage, ServerStatusMessage { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java index eed96afca..c74c1a3ff 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java @@ -26,35 +26,35 @@ public interface HandshakeRequest extends ServerMessage { /** - * Get the handshake request header. + * Gets the handshake request header. * * @return the header. */ HandshakeHeader getHeader(); /** - * Get the server-side capability. + * Gets the server-side capability. * * @return the server-side capability. */ Capability getServerCapability(); /** - * Get the authentication plugin type name. + * Gets the authentication plugin type name. * * @return the authentication plugin type. */ String getAuthType(); /** - * Get the challenge salt for authentication. + * Gets the challenge salt for authentication. * * @return the challenge salt. */ byte[] getSalt(); /** - * Decode a {@link HandshakeRequest} from a envelope {@link ByteBuf}. + * Decodes a {@link HandshakeRequest} from a payload {@link ByteBuf} of a normal packet. * * @param buf the {@link ByteBuf}. * @return decoded {@link HandshakeRequest}. diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java index c649f99bd..d26975823 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java @@ -36,7 +36,7 @@ /** * An implementation of {@link FieldReader} for large result which bytes more than {@link Integer#MAX_VALUE}, - * it would be exists when MySQL server return LOB types (i.e. BLOB, CLOB), LONGTEXT length can be unsigned + * it is used by the MySQL server returns LOB types (i.e. BLOB, CLOB), e.g. LONGTEXT length can be unsigned * int32. */ final class LargeFieldReader extends AbstractReferenceCounted implements FieldReader { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java index b5acdf728..cea0cf63c 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql.message.server; /** - * A implementation of {@link DecodeContext} for waiting {@code PreparedOkMessage}. + * An implementation of {@link DecodeContext} for waiting {@code PreparedOkMessage}. */ final class PrepareQueryDecodeContext implements DecodeContext { diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java index 1c8577ab9..61876759b 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java @@ -172,7 +172,7 @@ private static ServerMessage decodeResult(List buffers, ConnectionConte } finally { combined.release(); } - // Should not has other messages when metadata reading. + // Should not have other messages when metadata reading. } return decodeRow(buffers, firstBuf, header, context, "result"); @@ -211,9 +211,9 @@ private static ServerMessage decodeCommandMessage(ByteBuf buf, ConnectionContext // Maybe OK, maybe column count (unsupported EOF on command phase) if (OkMessage.isValidSize(byteSize)) { - // MySQL has hard limit of 4096 columns per-table, - // so if readable bytes upper than 7, it means if it is column count, - // column count is already upper than (1 << 24) - 1 = 16777215, it is impossible. + // MySQL has hard limited of 4096 columns per-table, + // so if readable bytes is greater than 7, it means if it is column count, + // column count is already greater than (1 << 24) - 1 = 16777215, it is impossible. // So it must be OK message, not be column count. return OkMessage.decode(false, buf, context); } else if (EofMessage.isValidSize(byteSize)) { diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java b/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java index 74bb3ee92..c44bc5b28 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java @@ -19,7 +19,6 @@ import io.asyncer.r2dbc.mysql.cache.Caches; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.netty.buffer.UnpooledByteBufAllocator; import java.lang.reflect.Field; @@ -32,7 +31,7 @@ class PrepareParametrizedStatementTest implements StatementTestSupport Date: Fri, 16 Feb 2024 14:26:55 +0900 Subject: [PATCH 16/93] Add missing option localInfileBufferSize --- .../mysql/MySqlConnectionFactoryProvider.java | 10 +++++ .../MySqlConnectionFactoryProviderTest.java | 40 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index 0dfa0475c..27b0c6842 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -203,6 +203,14 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr 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}]. *

@@ -314,6 +322,8 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { 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() diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index 01439fbee..d34a947bf 100644 --- a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -35,6 +35,8 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.net.URLEncoder; import java.time.Duration; import java.time.ZoneId; @@ -44,6 +46,7 @@ import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.PASSWORD_PUBLISHER; @@ -450,6 +453,43 @@ void validPasswordSupplier() { assertThat(ConnectionFactories.get(options)).isExactlyInstanceOf(MySqlConnectionFactory.class); } + @Test + void allConfigurationOptions() { + List exceptConfigs = Arrays.asList( + "extendWith", + "username", + "zeroDateOption"); + List exceptOptions = Arrays.asList( + "driver", + "ssl", + "protocol", + "zeroDate", + "lockWaitTimeout", + "statementTimeout"); + Set allOptions = Stream.concat( + Arrays.stream(ConnectionFactoryOptions.class.getFields()), + Arrays.stream(MySqlConnectionFactoryProvider.class.getFields()) + ) + .filter(field -> Modifier.isStatic(field.getModifiers()) && field.getType() == Option.class) + .map(field -> { + try { + return ((Option) field.get(null)).name(); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + }) + .filter(name -> !exceptOptions.contains(name)) + .collect(Collectors.toSet()); + Set allBuilderOptions = Arrays.stream(MySqlConnectionConfiguration.Builder.class.getMethods()) + .filter(method -> method.getParameterCount() >= 1 && + method.getReturnType() == MySqlConnectionConfiguration.Builder.class && + !exceptConfigs.contains(method.getName())) + .map(Method::getName) + .collect(Collectors.toSet()); + + assertThat(allBuilderOptions).containsExactlyInAnyOrderElementsOf(allOptions); + } + @ParameterizedTest @MethodSource void sessionVariables(String input, List expected) { From 26aa227f32ad3a8eb542d3beb75733f3120529e6 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Fri, 16 Feb 2024 19:20:22 +0900 Subject: [PATCH 17/93] Correct an unexpected fall through --- .../r2dbc/mysql/message/server/ServerMessageDecoder.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java index 61876759b..b48bc4f74 100644 --- a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java +++ b/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java @@ -219,10 +219,13 @@ private static ServerMessage decodeCommandMessage(ByteBuf buf, ConnectionContext } else if (EofMessage.isValidSize(byteSize)) { return EofMessage.decode(buf); } + + break; case LOCAL_INFILE: if (buf.readableBytes() > 1) { return LocalInfileRequest.decode(buf, context); } + break; } From 98741f2f5f39d6bfbd8531fdd89587e033db8b61 Mon Sep 17 00:00:00 2001 From: jchrys Date: Mon, 19 Feb 2024 11:25:41 +0900 Subject: [PATCH 18/93] Ensure GraalVM Native Build Compatibility (#239) Motivation: Ensure GraalVM Native Build Compatibility Modifications: Add Native build test Result: Ensured GraalVM Native Build Compatibility Resolves #194 --- .github/workflows/cd-release.yml | 4 +- .github/workflows/cd-snapshot.yml | 4 +- .github/workflows/ci-graalvm-tests.yml | 53 ++++++ pom.xml | 160 +++--------------- r2dbc-mysql/pom.xml | 154 +++++++++++++++++ .../asyncer/r2dbc/mysql/BenchmarkSupport.java | 0 .../mysql/MySqlNamesCompareBenchmark.java | 0 .../r2dbc/mysql/SelectOneBenchmark.java | 0 .../r2dbc/mysql/ServerVersionBenchmark.java | 0 .../java/io/asyncer/r2dbc/mysql/Binding.java | 0 .../io/asyncer/r2dbc/mysql/Capability.java | 0 .../asyncer/r2dbc/mysql/ColumnDefinition.java | 0 .../io/asyncer/r2dbc/mysql/ColumnNameSet.java | 0 .../r2dbc/mysql/ConnectionContext.java | 0 .../asyncer/r2dbc/mysql/ConnectionState.java | 0 .../r2dbc/mysql/ConsistentSnapshotEngine.java | 0 .../io/asyncer/r2dbc/mysql/Extensions.java | 0 .../r2dbc/mysql/InsertSyntheticRow.java | 0 .../io/asyncer/r2dbc/mysql/MySqlBatch.java | 0 .../r2dbc/mysql/MySqlBatchingBatch.java | 0 .../r2dbc/mysql/MySqlColumnDescriptor.java | 0 .../r2dbc/mysql/MySqlColumnMetadata.java | 0 .../asyncer/r2dbc/mysql/MySqlConnection.java | 0 .../mysql/MySqlConnectionConfiguration.java | 0 .../r2dbc/mysql/MySqlConnectionFactory.java | 0 .../mysql/MySqlConnectionFactoryMetadata.java | 0 .../mysql/MySqlConnectionFactoryProvider.java | 0 .../r2dbc/mysql/MySqlConnectionMetadata.java | 0 .../io/asyncer/r2dbc/mysql/MySqlNames.java | 0 .../asyncer/r2dbc/mysql/MySqlParameter.java | 0 .../io/asyncer/r2dbc/mysql/MySqlResult.java | 0 .../java/io/asyncer/r2dbc/mysql/MySqlRow.java | 0 .../asyncer/r2dbc/mysql/MySqlRowMetadata.java | 0 .../r2dbc/mysql/MySqlSslConfiguration.java | 0 .../asyncer/r2dbc/mysql/MySqlStatement.java | 0 .../r2dbc/mysql/MySqlStatementSupport.java | 0 .../r2dbc/mysql/MySqlSyntheticBatch.java | 0 .../mysql/MySqlTransactionDefinition.java | 0 .../r2dbc/mysql/MySqlTypeMetadata.java | 0 .../io/asyncer/r2dbc/mysql/OptionMapper.java | 0 .../asyncer/r2dbc/mysql/ParameterIndex.java | 0 .../asyncer/r2dbc/mysql/ParameterWriter.java | 0 .../mysql/ParametrizedStatementSupport.java | 0 .../io/asyncer/r2dbc/mysql/PingStatement.java | 0 .../mysql/PrepareParametrizedStatement.java | 0 .../r2dbc/mysql/PrepareSimpleStatement.java | 0 .../java/io/asyncer/r2dbc/mysql/Query.java | 0 .../io/asyncer/r2dbc/mysql/QueryFlow.java | 0 .../io/asyncer/r2dbc/mysql/QueryLogger.java | 0 .../io/asyncer/r2dbc/mysql/ServerVersion.java | 0 .../r2dbc/mysql/SimpleStatementSupport.java | 0 .../mysql/TextParametrizedStatement.java | 0 .../r2dbc/mysql/TextSimpleStatement.java | 0 .../r2dbc/mysql/authentication/AuthUtils.java | 0 .../CachingSha2FastAuthProvider.java | 0 .../CachingSha2FullAuthProvider.java | 0 .../authentication/MySqlAuthProvider.java | 0 .../MySqlClearAuthProvider.java | 0 .../MySqlNativeAuthProvider.java | 0 .../mysql/authentication/NoAuthProvider.java | 0 .../mysql/authentication/OldAuthProvider.java | 0 .../authentication/Sha256AuthProvider.java | 0 .../mysql/authentication/package-info.java | 0 .../io/asyncer/r2dbc/mysql/cache/Caches.java | 0 .../asyncer/r2dbc/mysql/cache/FreqSketch.java | 0 .../io/asyncer/r2dbc/mysql/cache/Lru.java | 0 .../mysql/cache/PrepareBoundedCache.java | 0 .../r2dbc/mysql/cache/PrepareCache.java | 0 .../mysql/cache/PrepareDisabledCache.java | 0 .../mysql/cache/PrepareUnboundedCache.java | 0 .../r2dbc/mysql/cache/QueryBoundedCache.java | 0 .../asyncer/r2dbc/mysql/cache/QueryCache.java | 0 .../r2dbc/mysql/cache/QueryDisabledCache.java | 0 .../mysql/cache/QueryUnboundedCache.java | 0 .../asyncer/r2dbc/mysql/cache/RingBuffer.java | 0 .../r2dbc/mysql/cache/package-info.java | 0 .../io/asyncer/r2dbc/mysql/client/Client.java | 0 .../r2dbc/mysql/client/ClientExceptions.java | 0 .../mysql/client/CompressionDuplexCodec.java | 0 .../r2dbc/mysql/client/Compressor.java | 0 .../mysql/client/DefaultHostnameVerifier.java | 0 .../r2dbc/mysql/client/FluxExchangeable.java | 0 .../mysql/client/MessageDuplexCodec.java | 0 .../r2dbc/mysql/client/PacketEvent.java | 0 .../mysql/client/ReactorNettyClient.java | 0 .../r2dbc/mysql/client/RequestQueue.java | 0 .../r2dbc/mysql/client/RequestTask.java | 0 .../io/asyncer/r2dbc/mysql/client/San.java | 0 .../r2dbc/mysql/client/SslBridgeHandler.java | 0 .../asyncer/r2dbc/mysql/client/SslState.java | 0 .../r2dbc/mysql/client/WriteSubscriber.java | 0 .../r2dbc/mysql/client/ZlibCompressor.java | 0 .../r2dbc/mysql/client/ZstdCompressor.java | 0 .../r2dbc/mysql/client/package-info.java | 0 .../mysql/codec/AbstractClassedCodec.java | 0 .../codec/AbstractLobMySqlParameter.java | 0 .../mysql/codec/AbstractMySqlParameter.java | 0 .../mysql/codec/AbstractPrimitiveCodec.java | 0 .../r2dbc/mysql/codec/BigDecimalCodec.java | 0 .../r2dbc/mysql/codec/BigIntegerCodec.java | 0 .../r2dbc/mysql/codec/BitSetCodec.java | 0 .../asyncer/r2dbc/mysql/codec/BlobCodec.java | 0 .../r2dbc/mysql/codec/BooleanCodec.java | 0 .../r2dbc/mysql/codec/ByteArrayCodec.java | 0 .../r2dbc/mysql/codec/ByteBufferCodec.java | 0 .../asyncer/r2dbc/mysql/codec/ByteCodec.java | 0 .../asyncer/r2dbc/mysql/codec/ClobCodec.java | 0 .../io/asyncer/r2dbc/mysql/codec/Codec.java | 0 .../r2dbc/mysql/codec/CodecContext.java | 0 .../r2dbc/mysql/codec/CodecRegistry.java | 0 .../asyncer/r2dbc/mysql/codec/CodecUtils.java | 0 .../io/asyncer/r2dbc/mysql/codec/Codecs.java | 0 .../r2dbc/mysql/codec/CodecsBuilder.java | 0 .../asyncer/r2dbc/mysql/codec/DateTimes.java | 0 .../r2dbc/mysql/codec/DefaultCodecs.java | 0 .../r2dbc/mysql/codec/DoubleCodec.java | 0 .../r2dbc/mysql/codec/DurationCodec.java | 0 .../asyncer/r2dbc/mysql/codec/EnumCodec.java | 0 .../asyncer/r2dbc/mysql/codec/FloatCodec.java | 0 .../r2dbc/mysql/codec/InstantCodec.java | 0 .../r2dbc/mysql/codec/IntegerCodec.java | 0 .../r2dbc/mysql/codec/LocalDateCodec.java | 0 .../r2dbc/mysql/codec/LocalDateTimeCodec.java | 0 .../r2dbc/mysql/codec/LocalTimeCodec.java | 0 .../asyncer/r2dbc/mysql/codec/LongCodec.java | 0 .../r2dbc/mysql/codec/MassiveCodec.java | 0 .../mysql/codec/MassiveParametrizedCodec.java | 0 .../r2dbc/mysql/codec/NullMySqlParameter.java | 0 .../mysql/codec/OffsetDateTimeCodec.java | 0 .../r2dbc/mysql/codec/OffsetTimeCodec.java | 0 .../r2dbc/mysql/codec/ParametrizedCodec.java | 0 .../r2dbc/mysql/codec/PrimitiveCodec.java | 0 .../asyncer/r2dbc/mysql/codec/SetCodec.java | 0 .../asyncer/r2dbc/mysql/codec/ShortCodec.java | 0 .../r2dbc/mysql/codec/StringCodec.java | 0 .../asyncer/r2dbc/mysql/codec/YearCodec.java | 0 .../r2dbc/mysql/codec/ZonedDateTimeCodec.java | 0 .../r2dbc/mysql/codec/lob/LobUtils.java | 0 .../r2dbc/mysql/codec/lob/MultiBlob.java | 0 .../r2dbc/mysql/codec/lob/MultiClob.java | 0 .../r2dbc/mysql/codec/lob/MultiLob.java | 0 .../r2dbc/mysql/codec/lob/SingletonBlob.java | 0 .../r2dbc/mysql/codec/lob/SingletonClob.java | 0 .../r2dbc/mysql/codec/lob/SingletonLob.java | 0 .../r2dbc/mysql/codec/lob/package-info.java | 0 .../r2dbc/mysql/codec/package-info.java | 0 .../collation/AbstractCharCollation.java | 0 .../collation/AbstractCharsetTarget.java | 0 .../r2dbc/mysql/collation/BinaryTarget.java | 0 .../mysql/collation/CachedCharCollation.java | 0 .../mysql/collation/CachedCharsetTarget.java | 0 .../r2dbc/mysql/collation/CharCollation.java | 0 .../r2dbc/mysql/collation/CharCollations.java | 0 .../r2dbc/mysql/collation/CharsetTarget.java | 0 .../r2dbc/mysql/collation/CharsetTargets.java | 0 .../collation/LazyInitCharCollation.java | 0 .../mysql/collation/MixCharsetTarget.java | 0 .../mysql/collation/NamedCharsetTarget.java | 0 .../r2dbc/mysql/collation/package-info.java | 0 .../mysql/constant/CompressionAlgorithm.java | 0 .../r2dbc/mysql/constant/MySqlType.java | 0 .../asyncer/r2dbc/mysql/constant/Packets.java | 0 .../r2dbc/mysql/constant/ServerStatuses.java | 0 .../asyncer/r2dbc/mysql/constant/SslMode.java | 0 .../r2dbc/mysql/constant/TlsVersions.java | 0 .../r2dbc/mysql/constant/ZeroDateOption.java | 0 .../r2dbc/mysql/constant/package-info.java | 0 .../r2dbc/mysql/extension/CodecRegistrar.java | 0 .../r2dbc/mysql/extension/Extension.java | 0 .../r2dbc/mysql/extension/package-info.java | 0 .../mysql/internal/NotNullByDefault.java | 0 .../r2dbc/mysql/internal/package-info.java | 0 .../mysql/internal/util/AddressUtils.java | 0 .../mysql/internal/util/AssertUtils.java | 0 .../util/DiscardOnCancelSubscriber.java | 0 .../internal/util/FluxDiscardOnCancel.java | 0 .../util/FluxDiscardOnCancelFuseable.java | 0 .../mysql/internal/util/FluxEnvelope.java | 0 .../mysql/internal/util/InternalArrays.java | 0 .../mysql/internal/util/NettyBufferUtils.java | 0 .../mysql/internal/util/OperatorUtils.java | 0 .../internal/util/ReadCompletionHandler.java | 0 .../mysql/internal/util/StringUtils.java | 0 .../mysql/internal/util/VarIntUtils.java | 0 .../mysql/internal/util/package-info.java | 0 .../r2dbc/mysql/message/FieldValue.java | 0 .../r2dbc/mysql/message/LargeFieldValue.java | 0 .../r2dbc/mysql/message/NormalFieldValue.java | 0 .../r2dbc/mysql/message/NullFieldValue.java | 0 .../mysql/message/client/AuthResponse.java | 0 .../mysql/message/client/ClientMessage.java | 0 .../mysql/message/client/ExitMessage.java | 0 .../message/client/HandshakeResponse.java | 0 .../message/client/HandshakeResponse320.java | 0 .../message/client/HandshakeResponse41.java | 0 .../mysql/message/client/InitDbMessage.java | 0 .../message/client/LocalInfileResponse.java | 0 .../mysql/message/client/ParamWriter.java | 0 .../mysql/message/client/PingMessage.java | 0 .../message/client/PrepareQueryMessage.java | 0 .../message/client/PreparedCloseMessage.java | 0 .../client/PreparedExecuteMessage.java | 0 .../message/client/PreparedFetchMessage.java | 0 .../client/PreparedLargeDataMessage.java | 0 .../message/client/PreparedResetMessage.java | 0 .../client/PreparedTextQueryMessage.java | 0 .../message/client/ScalarClientMessage.java | 0 .../message/client/SizedClientMessage.java | 0 .../mysql/message/client/SslRequest.java | 0 .../mysql/message/client/SslRequest320.java | 0 .../mysql/message/client/SslRequest41.java | 0 .../client/SubsequenceClientMessage.java | 0 .../message/client/TextQueryMessage.java | 0 .../mysql/message/client/package-info.java | 0 .../r2dbc/mysql/message/package-info.java | 0 .../message/server/AuthMoreDataMessage.java | 0 .../message/server/ChangeAuthMessage.java | 0 .../message/server/ColumnCountMessage.java | 0 .../message/server/CommandDecodeContext.java | 0 .../mysql/message/server/CompleteMessage.java | 0 .../mysql/message/server/DecodeContext.java | 0 .../server/DefinitionMetadataMessage.java | 0 .../mysql/message/server/Eof320Message.java | 0 .../mysql/message/server/Eof41Message.java | 0 .../mysql/message/server/EofMessage.java | 0 .../mysql/message/server/ErrorMessage.java | 0 .../message/server/FetchDecodeContext.java | 0 .../mysql/message/server/FieldReader.java | 0 .../mysql/message/server/HandshakeHeader.java | 0 .../message/server/HandshakeRequest.java | 0 .../message/server/HandshakeV10Request.java | 0 .../message/server/HandshakeV9Request.java | 0 .../message/server/LargeFieldReader.java | 0 .../message/server/LocalInfileRequest.java | 0 .../message/server/LoginDecodeContext.java | 0 .../message/server/MetadataDecodeContext.java | 0 .../message/server/NormalFieldReader.java | 0 .../r2dbc/mysql/message/server/OkMessage.java | 0 .../server/PrepareQueryDecodeContext.java | 0 .../server/PreparedMetadataDecodeContext.java | 0 .../message/server/PreparedOkMessage.java | 0 .../message/server/ResultDecodeContext.java | 0 .../mysql/message/server/RowMessage.java | 0 .../mysql/message/server/ServerMessage.java | 0 .../message/server/ServerMessageDecoder.java | 0 .../message/server/ServerStatusMessage.java | 0 .../server/SyntheticMetadataMessage.java | 0 .../server/SyntheticSslResponseMessage.java | 0 .../mysql/message/server/WarningMessage.java | 0 .../mysql/message/server/package-info.java | 0 .../io/asyncer/r2dbc/mysql/package-info.java | 0 .../io.r2dbc.spi.ConnectionFactoryProvider | 0 .../r2dbc/mysql/ColumnDefinitionTest.java | 0 .../CompressionIntegrationTestSupport.java | 0 .../r2dbc/mysql/ConnectionContextTest.java | 0 .../mysql/ConnectionIntegrationTest.java | 0 .../asyncer/r2dbc/mysql/ExtensionsTest.java | 0 .../r2dbc/mysql/InitDbIntegrationTest.java | 0 .../r2dbc/mysql/IntegrationTestSupport.java | 0 .../mysql/JacksonIntegrationTestSupport.java | 0 .../mysql/JacksonPrepareIntegrationTest.java | 0 .../mysql/JacksonTextIntegrationTest.java | 0 .../mysql/MariaDbIntegrationTestSupport.java | 0 .../mysql/MariaDbPrepareIntegrationTest.java | 0 .../mysql/MariaDbTextIntegrationTest.java | 0 .../r2dbc/mysql/MySqlBatchingBatchTest.java | 0 .../MySqlConnectionConfigurationTest.java | 0 .../MySqlConnectionFactoryProviderTest.java | 0 .../r2dbc/mysql/MySqlConnectionTest.java | 0 .../asyncer/r2dbc/mysql/MySqlNamesTest.java | 0 .../r2dbc/mysql/MySqlPrepareTestKit.java | 0 .../r2dbc/mysql/MySqlSyntheticBatchTest.java | 0 .../r2dbc/mysql/MySqlTestKitSupport.java | 0 .../asyncer/r2dbc/mysql/MySqlTextTestKit.java | 0 .../mysql/MySqlTransactionDefinitionTest.java | 0 .../asyncer/r2dbc/mysql/OptionMapperTest.java | 0 .../PrepareParametrizedStatementTest.java | 0 .../mysql/PrepareQueryIntegrationTest.java | 0 .../mysql/PrepareSimpleStatementTest.java | 0 .../mysql/PrepareTimeZoneIntegrationTest.java | 0 .../mysql/QueryIntegrationTestSupport.java | 0 .../io/asyncer/r2dbc/mysql/QueryTest.java | 0 .../r2dbc/mysql/ServerVersionTest.java | 0 .../mysql/SessionStateIntegrationTest.java | 0 .../r2dbc/mysql/SslTunnelIntegrationTest.java | 0 .../r2dbc/mysql/StatementTestSupport.java | 0 .../mysql/TextParametrizedStatementTest.java | 0 .../r2dbc/mysql/TextQueryIntegrationTest.java | 0 .../r2dbc/mysql/TextSimpleStatementTest.java | 0 .../mysql/TextTimeZoneIntegrationTest.java | 0 .../mysql/TimeZoneIntegrationTestSupport.java | 0 .../mysql/ZlibCompressionIntegrationTest.java | 0 .../mysql/ZstdCompressionIntegrationTest.java | 0 .../r2dbc/mysql/cache/FreqSketchTest.java | 0 .../io/asyncer/r2dbc/mysql/cache/LruTest.java | 0 .../mysql/cache/PrepareBoundedCacheTest.java | 0 .../r2dbc/mysql/client/RequestQueueTest.java | 0 .../mysql/client/ZlibCompressorTest.java | 0 .../mysql/codec/BigDecimalCodecTest.java | 0 .../mysql/codec/BigIntegerCodecTest.java | 0 .../r2dbc/mysql/codec/BitSetCodecTest.java | 0 .../r2dbc/mysql/codec/BlobCodecTest.java | 0 .../r2dbc/mysql/codec/BooleanCodecTest.java | 0 .../r2dbc/mysql/codec/ByteArrayCodecTest.java | 0 .../mysql/codec/ByteBufferCodecTest.java | 0 .../r2dbc/mysql/codec/ByteCodecTest.java | 0 .../r2dbc/mysql/codec/ClobCodecTest.java | 0 .../r2dbc/mysql/codec/CodecTestSupport.java | 0 .../asyncer/r2dbc/mysql/codec/CodecsTest.java | 0 .../mysql/codec/DateTimeCodecTestSupport.java | 0 .../r2dbc/mysql/codec/DateTimesTest.java | 0 .../asyncer/r2dbc/mysql/codec/Decoding.java | 0 .../r2dbc/mysql/codec/DoubleCodecTest.java | 0 .../r2dbc/mysql/codec/DurationCodecTest.java | 0 .../r2dbc/mysql/codec/EnumCodecTest.java | 0 .../r2dbc/mysql/codec/FloatCodecTest.java | 0 .../r2dbc/mysql/codec/InstantCodecTest.java | 0 .../r2dbc/mysql/codec/IntegerCodecTest.java | 0 .../r2dbc/mysql/codec/JacksonCodecTest.java | 0 .../r2dbc/mysql/codec/LocalDateCodecTest.java | 0 .../mysql/codec/LocalDateTimeCodecTest.java | 0 .../r2dbc/mysql/codec/LocalTimeCodecTest.java | 0 .../r2dbc/mysql/codec/LongCodecTest.java | 0 .../mysql/codec/NumericCodecTestSupport.java | 0 .../mysql/codec/OffsetDateTimeCodecTest.java | 0 .../mysql/codec/OffsetTimeCodecTest.java | 0 .../r2dbc/mysql/codec/SetCodecTest.java | 0 .../r2dbc/mysql/codec/ShortCodecTest.java | 0 .../r2dbc/mysql/codec/StringCodecTest.java | 0 .../mysql/codec/TimeCodecTestSupport.java | 0 .../r2dbc/mysql/codec/YearCodecTest.java | 0 .../mysql/codec/ZonedDateTimeCodecTest.java | 0 .../r2dbc/mysql/codec/lob/LobUtilsTest.java | 0 .../mysql/collation/CharCollationTest.java | 0 .../r2dbc/mysql/constant/MySqlTypeTest.java | 0 .../mysql/internal/util/AddressUtilsTest.java | 0 .../util/FluxDiscardOnCancelTest.java | 0 .../mysql/internal/util/FluxEnvelopeTest.java | 0 .../internal/util/InternalArraysTest.java | 0 .../internal/util/NettyBufferUtilsTest.java | 0 .../mysql/internal/util/StringUtilsTest.java | 0 .../mysql/internal/util/VarIntUtilsTest.java | 0 .../r2dbc/mysql/json/JacksonCodec.java | 0 .../mysql/json/JacksonCodecRegistrar.java | 0 .../r2dbc/mysql/json/package-info.java | 0 .../mysql/message/client/MockException.java | 0 .../message/client/MockMySqlParameter.java | 0 .../mysql/message/client/ParamWriterTest.java | 0 .../message/client/ParameterWriterHelper.java | 0 .../mysql/message/server/OkMessageTest.java | 0 .../server/ServerMessageDecoderTest.java | 0 ...io.asyncer.r2dbc.mysql.extension.Extension | 0 .../src}/test/resources/local/stations.csv | 0 .../src}/test/resources/local/stations.json | 0 .../src}/test/resources/local/stations.sql | 0 .../src}/test/resources/local/users.csv | 0 .../src}/test/resources/local/users.json | 0 .../src}/test/resources/local/users.sql | 0 .../src}/test/resources/logback-test.xml | 0 test-native-image/pom.xml | 46 +++++ .../src/main/java/io/asyncer/Main.java | 43 +++++ 361 files changed, 319 insertions(+), 145 deletions(-) create mode 100644 .github/workflows/ci-graalvm-tests.yml create mode 100644 r2dbc-mysql/pom.xml rename {src => r2dbc-mysql/src}/jmh/java/io/asyncer/r2dbc/mysql/BenchmarkSupport.java (100%) rename {src => r2dbc-mysql/src}/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java (100%) rename {src => r2dbc-mysql/src}/jmh/java/io/asyncer/r2dbc/mysql/SelectOneBenchmark.java (100%) rename {src => r2dbc-mysql/src}/jmh/java/io/asyncer/r2dbc/mysql/ServerVersionBenchmark.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/Binding.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/Capability.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/Extensions.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryMetadata.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ParameterIndex.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/PingStatement.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/Query.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/Client.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/San.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/SslState.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/client/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/java/io/asyncer/r2dbc/mysql/package-info.java (100%) rename {src => r2dbc-mysql/src}/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/QueryTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/json/package-info.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java (100%) rename {src => r2dbc-mysql/src}/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java (100%) rename {src => r2dbc-mysql/src}/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension (100%) rename {src => r2dbc-mysql/src}/test/resources/local/stations.csv (100%) rename {src => r2dbc-mysql/src}/test/resources/local/stations.json (100%) rename {src => r2dbc-mysql/src}/test/resources/local/stations.sql (100%) rename {src => r2dbc-mysql/src}/test/resources/local/users.csv (100%) rename {src => r2dbc-mysql/src}/test/resources/local/users.json (100%) rename {src => r2dbc-mysql/src}/test/resources/local/users.sql (100%) rename {src => r2dbc-mysql/src}/test/resources/logback-test.xml (100%) create mode 100644 test-native-image/pom.xml create mode 100644 test-native-image/src/main/java/io/asyncer/Main.java diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 2c3b26efa..70e1ce82a 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -121,11 +121,11 @@ jobs: working-directory: ./prepare-workspace/ run: | cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import - ./mvnw -B -ntp -am clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" + ./mvnw -B -ntp -am -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" - name: Deploy Local Staged Artifacts working-directory: ./prepare-workspace/ - run: ./mvnw -B -ntp --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true + run: ./mvnw -B -ntp -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true - name: Rollback Release diff --git a/.github/workflows/cd-snapshot.yml b/.github/workflows/cd-snapshot.yml index cafd99ded..5bb7e5341 100644 --- a/.github/workflows/cd-snapshot.yml +++ b/.github/workflows/cd-snapshot.yml @@ -52,7 +52,7 @@ jobs: }] - name: Deploy Local Staging - run: ./mvnw -B -ntp clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true + run: ./mvnw -B -ntp -pl r2dbc-mysql clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true - name: Deploy Local Staged Artifacts - run: ./mvnw -B --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DaltStagingDirectory=/home/runner/local-staging + run: ./mvnw -B -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DaltStagingDirectory=/home/runner/local-staging diff --git a/.github/workflows/ci-graalvm-tests.yml b/.github/workflows/ci-graalvm-tests.yml new file mode 100644 index 000000000..8bc67186d --- /dev/null +++ b/.github/workflows/ci-graalvm-tests.yml @@ -0,0 +1,53 @@ +# Copyright 2024 asyncer.io proejcts +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Native Image Build Test + +on: + pull_request: + branches: [ "trunk", "0.9.x" ] + +jobs: + graalvm-build-pr: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v4 + + - name: Set up MySQL + env: + MYSQL_DATABASE: r2dbc + MYSQL_ROOT_PASSWORD: r2dbc-password!@ + MYSQL_VERSION: 8.1 + run: docker-compose -f ${{ github.workspace }}/containers/mysql-compose.yml up -d + + - uses: graalvm/setup-graalvm@v1 + with: + java-version: 21 + distribution: 'graalvm' + native-image-job-reports: true + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Cache & Load Local Maven Repository + uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-prepare-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-prepare- + + - name: Build and run native image + run: | + echo "JAVA_HOME=$JAVA_HOME" + echo "./mvnw -Pgraalvm package" + ./mvnw -Pgraalvm package + ./test-native-image/target/test-native-image -Dtest.mysql.password=r2dbc-password!@ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 286b61581..8c5ae80cf 100644 --- a/pom.xml +++ b/pom.xml @@ -19,12 +19,16 @@ 4.0.0 io.asyncer - r2dbc-mysql + r2dbc-mysql-parent 1.1.2-SNAPSHOT - jar + pom Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql + + r2dbc-mysql + test-native-image + R2DBC MySQL Implementation @@ -63,6 +67,7 @@ UTF-8 1.8 false + true 1.0.0.RELEASE 2022.0.9 @@ -81,6 +86,7 @@ 1.5.5-11 24.1.0 1.77 + 21.2.0 @@ -128,140 +134,6 @@ - - - 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 - - - - 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 - - - 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.bouncycastle - bcpkix-jdk18on - test - - - @@ -293,11 +165,6 @@ - - org.apache.maven.plugins - maven-deploy-plugin - 3.1.1 - org.apache.maven.plugins maven-javadoc-plugin @@ -387,6 +254,17 @@ + + graalvm + + + ${java.home}/bin/gu + + + + false + + jmh diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml new file mode 100644 index 000000000..abce991e7 --- /dev/null +++ b/r2dbc-mysql/pom.xml @@ -0,0 +1,154 @@ + + + 4.0.0 + + io.asyncer + r2dbc-mysql-parent + 1.1.2-SNAPSHOT + + + r2dbc-mysql + + + 8 + 8 + UTF-8 + + + + + 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 + + + + 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 + + + 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.bouncycastle + bcpkix-jdk18on + test + + + + \ No newline at end of file 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/MySqlNamesCompareBenchmark.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java similarity index 100% rename from src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java rename to r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/Binding.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/Capability.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java 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/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnMetadata.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java 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/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlParameter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSslConfiguration.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/Query.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Query.java 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 100% rename from src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryLogger.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/AuthUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FastAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/CachingSha2FullAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlClearAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/MySqlNativeAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/NoAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/OldAuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/Sha256AuthProvider.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/FreqSketch.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Lru.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareDisabledCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/PrepareUnboundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryBoundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryDisabledCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/QueryUnboundedCache.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/RingBuffer.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/Client.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ClientExceptions.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/CompressionDuplexCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Compressor.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/DefaultHostnameVerifier.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/FluxExchangeable.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/PacketEvent.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestQueue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/RequestTask.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/San.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/San.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/San.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/San.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslState.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/WriteSubscriber.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZlibCompressor.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ZstdCompressor.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractMySqlParameter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecRegistry.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecsBuilder.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DateTimes.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/NullMySqlParameter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiBlob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiClob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/MultiLob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonBlob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonClob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/SingletonLob.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/AbstractCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/BinaryTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CachedCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharCollations.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/CharsetTargets.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/LazyInitCharCollation.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/MixCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/NamedCharsetTarget.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/collation/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/CompressionAlgorithm.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/Packets.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/SslMode.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/TlsVersions.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ZeroDateOption.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/CodecRegistrar.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/Extension.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/extension/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/NotNullByDefault.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/AssertUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/DiscardOnCancelSubscriber.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancel.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelFuseable.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelope.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/InternalArrays.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/OperatorUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/ReadCompletionHandler.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtils.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/FieldValue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/LargeFieldValue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NormalFieldValue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/NullFieldValue.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/AuthResponse.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ClientMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ExitMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse320.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/HandshakeResponse41.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/LocalInfileResponse.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PingMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PrepareQueryMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedCloseMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedExecuteMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedFetchMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedLargeDataMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedResetMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest320.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SslRequest41.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SubsequenceClientMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/TextQueryMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/AuthMoreDataMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ChangeAuthMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ColumnCountMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CommandDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/CompleteMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof320Message.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/Eof41Message.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/EofMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FetchDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/FieldReader.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeHeader.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeRequest.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV10Request.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/HandshakeV9Request.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LargeFieldReader.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LocalInfileRequest.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/LoginDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/NormalFieldReader.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/OkMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PrepareQueryDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedMetadataDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/PreparedOkMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ResultDecodeContext.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoder.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ServerStatusMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticMetadataMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/SyntheticSslResponseMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/WarningMessage.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/package-info.java diff --git a/src/main/java/io/asyncer/r2dbc/mysql/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java similarity index 100% rename from src/main/java/io/asyncer/r2dbc/mysql/package-info.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java diff --git a/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider b/r2dbc-mysql/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider similarity index 100% rename from src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider rename to r2dbc-mysql/src/main/resources/META-INF/services/io.r2dbc.spi.ConnectionFactoryProvider diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/CompressionIntegrationTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ExtensionsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonPrepareIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonTextIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbPrepareIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbTextIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlPrepareTestKit.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTextTestKit.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/OptionMapperTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ServerVersionTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZlibCompressionIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/FreqSketchTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/LruTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/cache/PrepareBoundedCacheTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/RequestQueueTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/client/ZlibCompressorTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BitSetCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BlobCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ClobCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimesTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DoubleCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DurationCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/FloatCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/InstantCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/IntegerCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/JacksonCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/LongCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/NumericCodecTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ShortCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/StringCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/TimeCodecTestSupport.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/YearCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/lob/LobUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/collation/CharCollationTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/constant/MySqlTypeTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/AddressUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxDiscardOnCancelTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/FluxEnvelopeTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/InternalArraysTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/NettyBufferUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/StringUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/VarIntUtilsTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodecRegistrar.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/package-info.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockException.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/MockMySqlParameter.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/OkMessageTest.java diff --git a/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java similarity index 100% rename from src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/server/ServerMessageDecoderTest.java diff --git a/src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension b/r2dbc-mysql/src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension similarity index 100% rename from src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension rename to r2dbc-mysql/src/test/resources/META-INF/services/io.asyncer.r2dbc.mysql.extension.Extension diff --git a/src/test/resources/local/stations.csv b/r2dbc-mysql/src/test/resources/local/stations.csv similarity index 100% rename from src/test/resources/local/stations.csv rename to r2dbc-mysql/src/test/resources/local/stations.csv diff --git a/src/test/resources/local/stations.json b/r2dbc-mysql/src/test/resources/local/stations.json similarity index 100% rename from src/test/resources/local/stations.json rename to r2dbc-mysql/src/test/resources/local/stations.json diff --git a/src/test/resources/local/stations.sql b/r2dbc-mysql/src/test/resources/local/stations.sql similarity index 100% rename from src/test/resources/local/stations.sql rename to r2dbc-mysql/src/test/resources/local/stations.sql diff --git a/src/test/resources/local/users.csv b/r2dbc-mysql/src/test/resources/local/users.csv similarity index 100% rename from src/test/resources/local/users.csv rename to r2dbc-mysql/src/test/resources/local/users.csv diff --git a/src/test/resources/local/users.json b/r2dbc-mysql/src/test/resources/local/users.json similarity index 100% rename from src/test/resources/local/users.json rename to r2dbc-mysql/src/test/resources/local/users.json diff --git a/src/test/resources/local/users.sql b/r2dbc-mysql/src/test/resources/local/users.sql similarity index 100% rename from src/test/resources/local/users.sql rename to r2dbc-mysql/src/test/resources/local/users.sql diff --git a/src/test/resources/logback-test.xml b/r2dbc-mysql/src/test/resources/logback-test.xml similarity index 100% rename from src/test/resources/logback-test.xml rename to r2dbc-mysql/src/test/resources/logback-test.xml diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml new file mode 100644 index 000000000..2c49360d3 --- /dev/null +++ b/test-native-image/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + io.asyncer + r2dbc-mysql-parent + 1.1.2-SNAPSHOT + + + test-native-image + + + + ${project.groupId} + r2dbc-mysql + ${project.version} + + + + + + + org.graalvm.nativeimage + native-image-maven-plugin + ${graalvm.version} + + + + native-image + + package + + + + ${skipNativeImage} + ${project.artifactId} + io.asyncer.Main + --report-unsupported-elements-at-runtime --allow-incomplete-classpath --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils + + + + + + \ No newline at end of file diff --git a/test-native-image/src/main/java/io/asyncer/Main.java b/test-native-image/src/main/java/io/asyncer/Main.java new file mode 100644 index 000000000..4a7931843 --- /dev/null +++ b/test-native-image/src/main/java/io/asyncer/Main.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer; + +import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import reactor.core.publisher.Mono; + +public class Main { + public static void main(String[] args) { + ConnectionFactory connectionFactory = ConnectionFactories.get(ConnectionFactoryOptions.builder() + .option(ConnectionFactoryOptions.DRIVER, "mysql") + .option(ConnectionFactoryOptions.HOST, "127.0.0.1") + .option(ConnectionFactoryOptions.PORT, 3306) + .option(ConnectionFactoryOptions.USER, "root") + .option(ConnectionFactoryOptions.PASSWORD, System.getProperty("test.mysql.password")) + .option(ConnectionFactoryOptions.DATABASE, "r2dbc") + .option(MySqlConnectionFactoryProvider.CREATE_DATABASE_IF_NOT_EXIST, true) + .build()); + + Mono.from(connectionFactory.create()) + .flatMapMany(connection -> connection.createStatement("SELECT 1").execute()) + .flatMap((result) -> result.map((row, rowMetadata) -> row.get(0, Integer.class))) + .doOnNext(System.out::println) + .blockLast(); + } +} \ No newline at end of file From 068c0e3ffdb53ba7f0f9b2fad5fcc4a1777c3d31 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 22 Feb 2024 15:46:09 +0900 Subject: [PATCH 19/93] Enable Leak Detection in CI Pipeline (#242) Motivation: Address lack of ByteBuffer leak detection during development to prevent potential production issues. Modifications: Added leak detection feature to CI pipeline for automatic detection during testing. Result: Better CI Resolves #241 --------- Signed-off-by: jchrys --- .github/scripts/ensure_no_leak.sh | 30 +++++++++++++++++++ .github/workflows/ci-integration-tests.yml | 7 ++++- .../ci-mariadb-intergration-tests.yml | 8 ++++- .github/workflows/ci-unit-tests.yml | 8 ++++- .../src/test/resources/logback-test.xml | 1 + 5 files changed, 51 insertions(+), 3 deletions(-) create mode 100755 .github/scripts/ensure_no_leak.sh diff --git a/.github/scripts/ensure_no_leak.sh b/.github/scripts/ensure_no_leak.sh new file mode 100755 index 000000000..6c6ebeead --- /dev/null +++ b/.github/scripts/ensure_no_leak.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# +# Copyright 2024 asyncer.io projects +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -e + +if [ "$#" -ne 1 ] || [ "${1##*.}" != "log" ]; then + echo "Please provide a single log file with a .log extension." + exit 1 +fi + +if grep -q 'LEAK' "$1" ; then + echo "LEAK FOUND: The log file $1 contains a memory leak." + exit 1 +fi + +echo "No Leak: The log file $1 does not contain any memory leaks." +exit 0 diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 881d87903..16b0eadb1 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -33,4 +33,9 @@ jobs: -Dmaven.surefire.skip=true \ -Dtest.mysql.password=r2dbc-password!@ \ -Dtest.mysql.version=${{ matrix.mysql-version }} \ - -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dio.netty.leakDetectionLevel=paranoid \ + -Dio.netty.leakDetection.targetRecords=32 \ + | tee test.log + - name: ensure no leaks + run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/.github/workflows/ci-mariadb-intergration-tests.yml b/.github/workflows/ci-mariadb-intergration-tests.yml index 78d577d97..ee4841ac1 100644 --- a/.github/workflows/ci-mariadb-intergration-tests.yml +++ b/.github/workflows/ci-mariadb-intergration-tests.yml @@ -34,4 +34,10 @@ jobs: -Dtest.mysql.password=r2dbc-password!@ \ -Dtest.mysql.version=${{ matrix.mariadb-version }} \ -Dtest.db.type=mariadb \ - -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dio.netty.leakDetectionLevel=paranoid \ + -Dio.netty.leakDetection.targetRecords=32 \ + | tee test.log + - name: ensure no leaks + run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml index 694f30c1d..c5f87fe71 100644 --- a/.github/workflows/ci-unit-tests.yml +++ b/.github/workflows/ci-unit-tests.yml @@ -20,4 +20,10 @@ jobs: java-version: ${{ matrix.java-version }} cache: maven - name: Unit test with Maven - run: ./mvnw -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ No newline at end of file + run: | + ./mvnw -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dio.netty.leakDetectionLevel=paranoid \ + -Dio.netty.leakDetection.targetRecords=32 \ + | tee test.log + - name: ensure no leaks + run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/r2dbc-mysql/src/test/resources/logback-test.xml b/r2dbc-mysql/src/test/resources/logback-test.xml index c4aa3b8ab..591762720 100644 --- a/r2dbc-mysql/src/test/resources/logback-test.xml +++ b/r2dbc-mysql/src/test/resources/logback-test.xml @@ -26,6 +26,7 @@ + From c0f72df3ccf7c103731adeb80deb61a07855fddf Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 13 Feb 2024 18:33:25 +0900 Subject: [PATCH 20/93] Align behaviors about time zone - Add `preserveInstants`, `connectionTimeZone` and `forceConnectionTimeZoneToSession` - Default `connectionTimeZone` to "LOCAL" - Mark `serverZoneId` as deprecated. It will notice users to use `connectionTimeZone` instead - Add `TimeZoneIntegrationTest` to test JDBC alignment of time zone behavior - Correct `OffsetTimeCodec` to not convert time zone --- r2dbc-mysql/pom.xml | 2 +- .../r2dbc/mysql/ConnectionContext.java | 44 ++- .../asyncer/r2dbc/mysql/MySqlConnection.java | 115 ++---- .../mysql/MySqlConnectionConfiguration.java | 118 +++++- .../r2dbc/mysql/MySqlConnectionFactory.java | 36 +- .../mysql/MySqlConnectionFactoryProvider.java | 43 ++- .../r2dbc/mysql/codec/CodecContext.java | 18 +- .../r2dbc/mysql/codec/InstantCodec.java | 10 +- .../mysql/codec/OffsetDateTimeCodec.java | 6 +- .../r2dbc/mysql/codec/OffsetTimeCodec.java | 14 +- .../r2dbc/mysql/codec/ZonedDateTimeCodec.java | 14 +- .../mysql/internal/util/StringUtils.java | 44 +++ .../r2dbc/mysql/ConnectionContextTest.java | 38 +- ...va => DateTimeIntegrationTestSupport.java} | 29 +- .../mysql/MariaDbIntegrationTestSupport.java | 128 +++++-- .../MySqlConnectionConfigurationTest.java | 4 +- .../MySqlConnectionFactoryProviderTest.java | 8 +- .../r2dbc/mysql/MySqlTestKitSupport.java | 11 +- ...va => PrepareDateTimeIntegrationTest.java} | 4 +- .../mysql/SessionStateIntegrationTest.java | 63 ++- ....java => TextDateTimeIntegrationTest.java} | 4 +- .../r2dbc/mysql/TimeZoneIntegrationTest.java | 362 ++++++++++++++++++ .../mysql/codec/DateTimeCodecTestSupport.java | 12 +- .../mysql/codec/OffsetDateTimeCodecTest.java | 6 +- .../mysql/codec/OffsetTimeCodecTest.java | 7 +- .../mysql/codec/ZonedDateTimeCodecTest.java | 6 +- test-native-image/pom.xml | 2 +- 27 files changed, 919 insertions(+), 229 deletions(-) rename r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/{TimeZoneIntegrationTestSupport.java => DateTimeIntegrationTestSupport.java} (92%) rename r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/{PrepareTimeZoneIntegrationTest.java => PrepareDateTimeIntegrationTest.java} (88%) rename r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/{TextTimeZoneIntegrationTest.java => TextDateTimeIntegrationTest.java} (88%) create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index abce991e7..da87d0c2c 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -151,4 +151,4 @@ - \ No newline at end of file + diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index 0445ff914..47ac71076 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -48,8 +48,10 @@ public final class ConnectionContext implements CodecContext { private final int localInfileBufferSize; + private final boolean preserveInstants; + @Nullable - private ZoneId serverZoneId; + private ZoneId timeZone; /** * Assume that the auto commit is always turned on, it will be set after handshake V10 request message, or @@ -60,12 +62,18 @@ public final class ConnectionContext implements CodecContext { @Nullable private volatile Capability capability = null; - ConnectionContext(ZeroDateOption zeroDateOption, @Nullable Path localInfilePath, - int localInfileBufferSize, @Nullable ZoneId serverZoneId) { + ConnectionContext( + ZeroDateOption zeroDateOption, + @Nullable Path localInfilePath, + int localInfileBufferSize, + boolean preserveInstants, + @Nullable ZoneId timeZone + ) { this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null"); this.localInfilePath = localInfilePath; this.localInfileBufferSize = localInfileBufferSize; - this.serverZoneId = serverZoneId; + this.preserveInstants = preserveInstants; + this.timeZone = timeZone; } /** @@ -101,27 +109,33 @@ public CharCollation getClientCollation() { } @Override - public ZoneId getServerZoneId() { - if (serverZoneId == null) { + public boolean isPreserveInstants() { + return preserveInstants; + } + + @Override + public ZoneId getTimeZone() { + if (timeZone == null) { throw new IllegalStateException("Server timezone have not initialization"); } - return serverZoneId; + return timeZone; } - @Override - public boolean isMariaDb() { - return capability.isMariaDb() || serverVersion.isMariaDb(); + public boolean isTimeZoneInitialized() { + return timeZone != null; } - boolean shouldSetServerZoneId() { - return serverZoneId == null; + @Override + public boolean isMariaDb() { + Capability capability = this.capability; + return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb(); } - void setServerZoneId(ZoneId serverZoneId) { - if (this.serverZoneId != null) { + void setTimeZone(ZoneId timeZone) { + if (isTimeZoneInitialized()) { throw new IllegalStateException("Server timezone have been initialized"); } - this.serverZoneId = serverZoneId; + this.timeZone = timeZone; } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java index 199f92cf1..2bf3a968c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java @@ -34,6 +34,7 @@ import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.Lifecycle; import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.Readable; import io.r2dbc.spi.TransactionDefinition; import io.r2dbc.spi.ValidationDepth; import org.jetbrains.annotations.Nullable; @@ -64,12 +65,6 @@ public final class MySqlConnection implements Connection, Lifecycle, ConnectionS private static final String PING_MARKER = "/* ping */"; - private static final String ZONE_PREFIX_POSIX = "posix/"; - - private static final String ZONE_PREFIX_RIGHT = "right/"; - - private static final int PREFIX_LENGTH = 6; - private static final ServerVersion MARIA_11_1_1 = ServerVersion.create(11, 1, 1, true); private static final ServerVersion MYSQL_8_0_3 = ServerVersion.create(8, 0, 3); @@ -333,7 +328,8 @@ 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()) + return QueryFlow.executeVoid(client, + "SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql()) .doOnSuccess(ignored -> { this.sessionLevel = isolationLevel; if (!this.isInTransaction()) { @@ -436,7 +432,7 @@ public Mono setStatementTimeout(Duration timeout) { final ServerVersion serverVersion = context.getServerVersion(); final long timeoutMs = timeout.toMillis(); final String sql = isMariaDb ? "SET max_statement_time=" + timeoutMs / 1000.0 - : "SET SESSION MAX_EXECUTION_TIME=" + timeoutMs; + : "SET SESSION MAX_EXECUTION_TIME=" + timeoutMs; // mariadb: https://mariadb.com/kb/en/aborting-statements/ // mysql: https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/ @@ -485,10 +481,10 @@ static Mono init( Mono connection = initSessionVariables(client, sessionVariables) .then(loadSessionVariables(client, codecs, context)) .map(data -> { - ZoneId serverZoneId = data.serverZoneId; - if (serverZoneId != null) { - logger.debug("Set server time zone to {} from init query", serverZoneId); - context.setServerZoneId(serverZoneId); + ZoneId timeZone = data.timeZone; + if (timeZone != null) { + logger.debug("Got server time zone {} from loading session variables", timeZone); + context.setTimeZone(timeZone); } return new MySqlConnection(client, context, codecs, data.level, data.lockWaitTimeout, @@ -531,7 +527,7 @@ private static Mono initSessionVariables(Client client, List sessi return QueryFlow.executeVoid(client, query.toString()); } - private static Mono loadSessionVariables( + private static Mono loadSessionVariables( Client client, Codecs codecs, ConnectionContext context ) { StringBuilder query = new StringBuilder(160) @@ -539,13 +535,13 @@ private static Mono loadSessionVariables( .append(transactionIsolationColumn(context)) .append(",@@innodb_lock_wait_timeout AS l,@@version_comment AS v"); - Function> handler; + Function> handler; - if (context.shouldSetServerZoneId()) { - query.append(",@@system_time_zone AS s,@@time_zone AS t"); - handler = MySqlConnection::fullInit; + if (context.isTimeZoneInitialized()) { + handler = r -> convertSessionData(r, false); } else { - handler = MySqlConnection::init; + query.append(",@@system_time_zone AS s,@@time_zone AS t"); + handler = r -> convertSessionData(r, true); } return new TextSimpleStatement(client, codecs, context, query.toString()) @@ -569,70 +565,39 @@ private static Mono initDatabase(Client client, String database) { }); } - private static Flux init(MySqlResult r) { - return r.map((row, meta) -> new InitData(convertIsolationLevel(row.get(0, String.class)), - convertLockWaitTimeout(row.get(1, Long.class)), - row.get(2, String.class), null)); - } - - private static Flux fullInit(MySqlResult r) { - return r.map((row, meta) -> { - IsolationLevel level = convertIsolationLevel(row.get(0, String.class)); - long lockWaitTimeout = convertLockWaitTimeout(row.get(1, Long.class)); - String product = row.get(2, String.class); - String systemTimeZone = row.get(3, String.class); - String timeZone = row.get(4, String.class); - ZoneId zoneId; - - if (timeZone == null || timeZone.isEmpty() || "SYSTEM".equalsIgnoreCase(timeZone)) { - if (systemTimeZone == null || systemTimeZone.isEmpty()) { - logger.warn("MySQL does not return any timezone, trying to use system default timezone"); - zoneId = ZoneId.systemDefault(); - } else { - zoneId = convertZoneId(systemTimeZone); - } - } else { - zoneId = convertZoneId(timeZone); - } + private static Flux convertSessionData(MySqlResult r, boolean timeZone) { + return r.map(readable -> { + IsolationLevel level = convertIsolationLevel(readable.get(0, String.class)); + long lockWaitTimeout = convertLockWaitTimeout(readable.get(1, Long.class)); + String product = readable.get(2, String.class); - return new InitData(level, lockWaitTimeout, product, zoneId); + return new SessionData(level, lockWaitTimeout, product, timeZone ? readZoneId(readable) : null); }); } - /** - * Creates a {@link ZoneId} from MySQL timezone result, or fallback to system default timezone if not - * found. - * - * @param id the ID/name of MySQL timezone. - * @return the {@link ZoneId}. - */ - private static ZoneId convertZoneId(String id) { - String realId; + private static ZoneId readZoneId(Readable readable) { + String systemTimeZone = readable.get(3, String.class); + String timeZone = readable.get(4, String.class); - if (id.startsWith(ZONE_PREFIX_POSIX) || id.startsWith(ZONE_PREFIX_RIGHT)) { - realId = id.substring(PREFIX_LENGTH); + 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 { - realId = id; + return convertZoneId(timeZone); } + } + private static ZoneId convertZoneId(String id) { try { - switch (realId) { - case "Factory": - // It seems like UTC. - return ZoneOffset.UTC; - case "America/Nuuk": - // America/Godthab is the same as America/Nuuk, with DST. - return ZoneId.of("America/Godthab"); - case "ROC": - // It is equal to +08:00. - return ZoneId.of("+8"); - } - - return ZoneId.of(realId, ZoneId.SHORT_IDS); + 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(); + return ZoneId.systemDefault().normalized(); } } @@ -691,7 +656,7 @@ private static String transactionIsolationColumn(ConnectionContext context) { "@@transaction_isolation AS i" : "@@tx_isolation AS i"; } - private static class InitData { + private static class SessionData { private final IsolationLevel level; @@ -701,14 +666,14 @@ private static class InitData { private final String product; @Nullable - private final ZoneId serverZoneId; + private final ZoneId timeZone; - private InitData(IsolationLevel level, long lockWaitTimeout, @Nullable String product, - @Nullable ZoneId serverZoneId) { + private SessionData(IsolationLevel level, long lockWaitTimeout, @Nullable String product, + @Nullable ZoneId timeZone) { this.level = level; this.lockWaitTimeout = lockWaitTimeout; this.product = product; - this.serverZoneId = serverZoneId; + this.timeZone = timeZone; } } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 60ea6ae3b..5953495ce 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -44,6 +44,7 @@ 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; @@ -78,8 +79,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; @@ -120,7 +124,10 @@ public final class MySqlConnectionConfiguration { private MySqlConnectionConfiguration( boolean isHost, String domain, int port, MySqlSslConfiguration ssl, boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout, - ZeroDateOption zeroDateOption, @Nullable ZoneId serverZoneId, + ZeroDateOption zeroDateOption, + boolean preserveInstants, + String connectionTimeZone, + boolean forceConnectionTimeZoneToSession, String user, @Nullable CharSequence password, @Nullable String database, boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement, List sessionVariables, @@ -137,7 +144,9 @@ 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; @@ -198,9 +207,16 @@ ZeroDateOption getZeroDateOption() { return zeroDateOption; } - @Nullable - ZoneId getServerZoneId() { - return serverZoneId; + boolean isPreserveInstants() { + return preserveInstants; + } + + String getConnectionTimeZone() { + return connectionTimeZone; + } + + boolean isForceConnectionTimeZoneToSession() { + return forceConnectionTimeZoneToSession; } String getUser() { @@ -283,7 +299,9 @@ 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) && @@ -305,7 +323,8 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, connectTimeout, - serverZoneId, zeroDateOption, user, password, database, createDatabaseIfNotExist, + preserveInstants, connectionTimeZone, forceConnectionTimeZoneToSession, + zeroDateOption, user, password, database, createDatabaseIfNotExist, preferPrepareStatement, sessionVariables, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, extensions, passwordPublisher); @@ -316,7 +335,10 @@ public String toString() { if (isHost) { return "MySqlConnectionConfiguration{host='" + domain + "', port=" + port + ", ssl=" + ssl + ", tcpNoDelay=" + tcpNoDelay + ", tcpKeepAlive=" + tcpKeepAlive + - ", connectTimeout=" + connectTimeout + ", serverZoneId=" + serverZoneId + + ", connectTimeout=" + connectTimeout + + ", preserveInstants=" + preserveInstants + + ", connectionTimeZone=" + connectionTimeZone + + ", forceConnectionTimeZoneToSession=" + forceConnectionTimeZoneToSession + ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + @@ -331,7 +353,10 @@ public String toString() { } return "MySqlConnectionConfiguration{unixSocket='" + domain + - "', connectTimeout=" + connectTimeout + ", serverZoneId=" + serverZoneId + + "', connectTimeout=" + connectTimeout + + ", preserveInstants=" + preserveInstants + + ", connectionTimeZone=" + connectionTimeZone + + ", forceConnectionTimeZoneToSession=" + forceConnectionTimeZoneToSession + ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + @@ -372,8 +397,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; @@ -453,7 +481,11 @@ 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, + connectTimeout, zeroDateOption, + preserveInstants, + connectionTimeZone, + forceConnectionTimeZoneToSession, + user, password, database, createDatabaseIfNotExist, preferPrepareStatement, sessionVariables, loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, @@ -580,15 +612,63 @@ public Builder username(String user) { } /** - * Configures the time zone of server. Default to query server time zone in initialization. + * 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()); } /** diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index d29bdb968..ec2d57339 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -25,6 +25,7 @@ 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.netty.buffer.ByteBufAllocator; import io.netty.channel.unix.DomainSocketAddress; import io.r2dbc.spi.ConnectionFactory; @@ -35,6 +36,9 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; @@ -94,18 +98,23 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura CharSequence password = configuration.getPassword(); SslMode sslMode = ssl.getSslMode(); int zstdCompressionLevel = configuration.getZstdCompressionLevel(); + ZoneId connectionTimeZone = retrieveZoneId(configuration.getConnectionTimeZone()); ConnectionContext context = new ConnectionContext( configuration.getZeroDateOption(), configuration.getLoadLocalInfilePath(), configuration.getLocalInfileBufferSize(), - configuration.getServerZoneId() + configuration.isPreserveInstants(), + connectionTimeZone ); Set compressionAlgorithms = configuration.getCompressionAlgorithms(); - List sessionVariables = configuration.getSessionVariables(); Extensions extensions = configuration.getExtensions(); Predicate prepare = configuration.getPreferPrepareStatement(); int prepareCacheSize = configuration.getPrepareCacheSize(); Publisher passwordPublisher = configuration.getPasswordPublisher(); + boolean forceTimeZone = configuration.isForceConnectionTimeZoneToSession(); + List sessionVariables = forceTimeZone && connectionTimeZone != null ? + mergeSessionVariables(configuration.getSessionVariables(), connectionTimeZone) : + configuration.getSessionVariables(); if (Objects.nonNull(passwordPublisher)) { return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( @@ -170,6 +179,29 @@ private static Mono getMySqlConnection( }); } + @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 List mergeSessionVariables(List sessionVariables, ZoneId timeZone) { + List res = new ArrayList<>(sessionVariables.size() + 1); + + String offerStr = timeZone instanceof ZoneOffset && "Z".equalsIgnoreCase(timeZone.getId()) ? + "+00:00" : timeZone.getId(); + + res.addAll(sessionVariables); + res.add("time_zone='" + offerStr + "'"); + + return res; + } + private static final class LazyQueryCache { private final int capacity; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index 27b0c6842..652bfd5fe 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -64,12 +64,44 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr */ 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"); /** @@ -309,8 +341,15 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { mapper.optional(UNIX_SOCKET).asString() .to(builder::unixSocket) .otherwise(() -> setupHost(builder, mapper)); - mapper.optional(SERVER_ZONE_ID).as(ZoneId.class, id -> ZoneId.of(id, ZoneId.SHORT_IDS)) - .to(builder::serverZoneId); + mapper.optional(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() diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java index c674f3b16..8eda9c985 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java @@ -28,28 +28,36 @@ public interface CodecContext { /** - * Get the {@link ZoneId} of server-side. + * Checks if the connection is set to preserve instants, i.e. convert instant values to connection time + * zone. + * + * @return if preserve instants. + */ + boolean isPreserveInstants(); + + /** + * Gets the {@link ZoneId} of connection. * * @return the {@link ZoneId}. */ - ZoneId getServerZoneId(); + ZoneId getTimeZone(); /** - * Get the option for zero date handling which is set by connection configuration. + * Gets the option for zero date handling which is set by connection configuration. * * @return the {@link ZeroDateOption}. */ ZeroDateOption getZeroDateOption(); /** - * Get the MySQL server version, which is available after database user logon. + * Gets the MySQL server version, which is available after database user logon. * * @return the {@link ServerVersion}. */ ServerVersion getServerVersion(); /** - * Get the {@link CharCollation} that the client is using. + * Gets the {@link CharCollation} that the client is using. * * @return the {@link CharCollation}. */ diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java index 23147bbbb..17d0793ed 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java @@ -26,6 +26,8 @@ import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; /** * Codec for {@link Instant}. @@ -46,7 +48,10 @@ public Instant decode(ByteBuf value, MySqlColumnMetadata metadata, Class targ return null; } - return origin.toInstant(context.getServerZoneId().getRules().getOffset(origin)); + ZoneId zone = context.isPreserveInstants() ? context.getTimeZone() : ZoneOffset.systemDefault(); + + return origin.toInstant(zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() + .getOffset(origin)); } @Override @@ -108,7 +113,8 @@ public int hashCode() { } private LocalDateTime serverValue() { - return LocalDateTime.ofInstant(value, context.getServerZoneId()); + return LocalDateTime.ofInstant(value, context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault()); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java index d1681ae31..694578b13 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java @@ -48,7 +48,7 @@ public OffsetDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class< return null; } - ZoneId zone = context.getServerZoneId(); + ZoneId zone = context.isPreserveInstants() ? context.getTimeZone() : ZoneId.systemDefault(); return OffsetDateTime.of(origin, zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() .getOffset(origin)); @@ -113,7 +113,9 @@ public int hashCode() { } private LocalDateTime serverValue() { - ZoneId zone = context.getServerZoneId(); + ZoneId zone = context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault().normalized(); + return zone instanceof ZoneOffset ? value.withOffsetSameInstant((ZoneOffset) zone).toLocalDateTime() : value.toZonedDateTime().withZoneSameInstant(zone).toLocalDateTime(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java index db1009c37..1ed3769a6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java @@ -44,8 +44,9 @@ private OffsetTimeCodec() { @Override public OffsetTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, CodecContext context) { + // OffsetTime is not an instant value, so preserveInstants is not used here. LocalTime origin = LocalTimeCodec.decodeOrigin(binary, value); - ZoneId zone = context.getServerZoneId(); + ZoneId zone = ZoneId.systemDefault().normalized(); return OffsetTime.of(origin, zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() .getStandardOffset(Instant.EPOCH)); @@ -112,9 +113,14 @@ public int hashCode() { } private LocalTime serverValue() { - ZoneId zone = context.getServerZoneId(); - ZoneOffset offset = zone instanceof ZoneOffset ? (ZoneOffset) zone : zone.getRules() - .getStandardOffset(Instant.EPOCH); + // OffsetTime is not an instant value, so preserveInstants is not used here. + ZoneId zone = ZoneId.systemDefault().normalized(); + + if (zone instanceof ZoneOffset) { + return value.withOffsetSameInstant((ZoneOffset) zone).toLocalTime(); + } + + ZoneOffset offset = zone.getRules().getStandardOffset(Instant.EPOCH); return value.toLocalTime() .plusSeconds(offset.getTotalSeconds() - value.getOffset().getTotalSeconds()); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java index dcaee0cc7..3bd0072b6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java @@ -28,6 +28,7 @@ import java.lang.reflect.ParameterizedType; import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.chrono.ChronoZonedDateTime; @@ -78,7 +79,13 @@ public boolean canDecode(MySqlColumnMetadata metadata, Class target) { @Nullable private static ZonedDateTime decode0(ByteBuf value, boolean binary, CodecContext context) { LocalDateTime origin = LocalDateTimeCodec.decodeOrigin(value, binary, context); - return origin == null ? null : ZonedDateTime.of(origin, context.getServerZoneId()); + + if (origin == null) { + return null; + } + + return ZonedDateTime.of(origin, context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault()); } private static final class ZonedDateTimeMySqlParameter extends AbstractMySqlParameter { @@ -127,7 +134,10 @@ public int hashCode() { } private LocalDateTime serverValue() { - return value.withZoneSameInstant(context.getServerZoneId()) + ZoneId zoneId = context.isPreserveInstants() ? context.getTimeZone() : + ZoneId.systemDefault().normalized(); + + return value.withZoneSameInstant(zoneId) .toLocalDateTime(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java index 34f6bacd8..e5c3596b6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java @@ -16,6 +16,9 @@ package io.asyncer.r2dbc.mysql.internal.util; +import java.time.ZoneId; +import java.time.ZoneOffset; + import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty; /** @@ -25,6 +28,12 @@ public final class StringUtils { private static final char QUOTE = '`'; + private static final String ZONE_PREFIX_POSIX = "posix/"; + + private static final String ZONE_PREFIX_RIGHT = "right/"; + + private static final int ZONE_PREFIX_LENGTH = 6; + /** * Quotes identifier with backticks, it will escape backticks in the identifier. * @@ -70,6 +79,41 @@ public static String extendReturning(String sql, String returning) { return returning.isEmpty() ? sql : sql + " RETURNING " + returning; } + /** + * Parses a normalized {@link ZoneId} from a time zone string of MySQL. + *

+ * Note: since java 14.0.2, 11.0.8, 8u261 and 7u271, America/Nuuk is already renamed from America/Godthab. + * See also tzdata2020a + * + * @param zoneId the time zone string + * @return the normalized {@link ZoneId} + * @throws IllegalArgumentException if the time zone string is {@code null} or empty + * @throws java.time.DateTimeException if the time zone string has an invalid format + * @throws java.time.zone.ZoneRulesException if the time zone string cannot be found + */ + public static ZoneId parseZoneId(String zoneId) { + requireNonEmpty(zoneId, "zoneId must not be empty"); + + String realId; + + if (zoneId.startsWith(ZONE_PREFIX_POSIX) || zoneId.startsWith(ZONE_PREFIX_RIGHT)) { + realId = zoneId.substring(ZONE_PREFIX_LENGTH); + } else { + realId = zoneId; + } + + switch (realId) { + case "Factory": + // It seems like UTC. + return ZoneOffset.UTC; + case "ROC": + // It is equal to +08:00. + return ZoneOffset.ofHours(8); + } + + return ZoneId.of(realId, ZoneId.SHORT_IDS).normalized(); + } + private StringUtils() { } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java index dce1b0ddb..7e98e5d6c 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java @@ -31,46 +31,30 @@ public class ConnectionContextTest { @Test - void getServerZoneId() { + void getTimeZone() { for (int i = -12; i <= 12; ++i) { String id = i < 0 ? "UTC" + i : "UTC+" + i; ConnectionContext context = new ConnectionContext( ZeroDateOption.USE_NULL, null, - 8192, ZoneId.of(id)); + 8192, true, ZoneId.of(id)); - assertThat(context.getServerZoneId()).isEqualTo(ZoneId.of(id)); + assertThat(context.getTimeZone()).isEqualTo(ZoneId.of(id)); } } @Test - void shouldSetServerZoneId() { + void setTwiceTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, null); - assertThat(context.shouldSetServerZoneId()).isTrue(); - context.setServerZoneId(ZoneId.systemDefault()); - assertThat(context.shouldSetServerZoneId()).isFalse(); + 8192, true, null); + context.setTimeZone(ZoneId.systemDefault()); + assertThatIllegalStateException().isThrownBy(() -> context.setTimeZone(ZoneId.systemDefault())); } @Test - void shouldNotSetServerZoneId() { + void badSetTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, ZoneId.systemDefault()); - assertThat(context.shouldSetServerZoneId()).isFalse(); - } - - @Test - void setTwiceServerZoneId() { - ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, null); - context.setServerZoneId(ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.setServerZoneId(ZoneId.systemDefault())); - } - - @Test - void badSetServerZoneId() { - ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.setServerZoneId(ZoneId.systemDefault())); + 8192, true, ZoneId.systemDefault()); + assertThatIllegalStateException().isThrownBy(() -> context.setTimeZone(ZoneId.systemDefault())); } public static ConnectionContext mock() { @@ -83,7 +67,7 @@ public static ConnectionContext mock(boolean isMariaDB) { public static ConnectionContext mock(boolean isMariaDB, ZoneId zoneId) { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, zoneId); + 8192, true, zoneId); context.init(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"), Capability.of(~(isMariaDB ? 1 : 0))); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/DateTimeIntegrationTestSupport.java similarity index 92% rename from r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/DateTimeIntegrationTestSupport.java index 7e32d07e4..891c4cb41 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/DateTimeIntegrationTestSupport.java @@ -16,7 +16,10 @@ package io.asyncer.r2dbc.mysql; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Isolated; import reactor.core.publisher.Flux; import java.time.Instant; @@ -35,9 +38,10 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Base class considers integration tests for time zone conversion. + * Base class considers integration tests for date times. */ -abstract class TimeZoneIntegrationTestSupport extends IntegrationTestSupport { +@Isolated +abstract class DateTimeIntegrationTestSupport extends IntegrationTestSupport { private static final String TIMESTAMP_TABLE = "CREATE TEMPORARY TABLE test " + "(id INT PRIMARY KEY AUTO_INCREMENT, value TIMESTAMP)"; @@ -56,7 +60,11 @@ abstract class TimeZoneIntegrationTestSupport extends IntegrationTestSupport { private static final ZoneId SERVER_ZONE = ZoneId.of("America/New_York"); - static { + private static TimeZone defaultTimeZone; + + @BeforeAll + static void setUpTimeZone() { + defaultTimeZone = TimeZone.getDefault(); TimeZone.setDefault(TimeZone.getTimeZone("GMT+6")); // Make sure test cases contains daylight. @@ -64,10 +72,15 @@ abstract class TimeZoneIntegrationTestSupport extends IntegrationTestSupport { .isEqualTo(DST.atZone(SERVER_ZONE).plusHours(1)); } - TimeZoneIntegrationTestSupport( + @AfterAll + static void tearDownTimeZone() { + TimeZone.setDefault(defaultTimeZone); + } + + DateTimeIntegrationTestSupport( Function customizer ) { - super(configuration(builder -> customizer.apply(builder.serverZoneId(SERVER_ZONE)))); + super(configuration(builder -> customizer.apply(builder.connectionTimeZone(SERVER_ZONE.getId())))); } @Test @@ -128,8 +141,7 @@ void queryOffsetTime() { .bind(0, 0) .execute()) .flatMap(r -> r.map((row, meta) -> row.get(0, OffsetTime.class))) - .doOnNext(it -> assertThat(it.getOffset()) - .isEqualTo(SERVER_ZONE.getRules().getStandardOffset(Instant.EPOCH))) + .doOnNext(it -> assertThat(it.getOffset()).isEqualTo(ZoneId.systemDefault().normalized())) .map(OffsetTime::toLocalTime) .collectList() .doOnNext(it -> assertThat(it) @@ -208,8 +220,7 @@ void updateOffsetTime() { .bind(0, 0) .execute()) .flatMap(r -> r.map((row, meta) -> row.get(0, OffsetTime.class))) - .doOnNext(it -> assertThat(it.getOffset()) - .isEqualTo(SERVER_ZONE.getRules().getStandardOffset(Instant.EPOCH))) + .doOnNext(it -> assertThat(it.getOffset()).isEqualTo(ZoneId.systemDefault().normalized())) .map(it -> it.withOffsetSameInstant(ZoneId.systemDefault().getRules() .getStandardOffset(Instant.EPOCH)) .toLocalTime()) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java index 8b08b3ead..dcd9fd482 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java @@ -18,12 +18,17 @@ import io.r2dbc.spi.Readable; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import java.time.Instant; +import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.List; import java.util.function.Function; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; /** * Base class considers integration tests for MariaDB. @@ -39,17 +44,17 @@ abstract class MariaDbIntegrationTestSupport extends IntegrationTestSupport { @Test void returningExpression() { complete(conn -> conn.createStatement("CREATE TEMPORARY TABLE test (" + - "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") .execute() .flatMap(IntegrationTestSupport::extractRowsUpdated) .thenMany(conn.createStatement("INSERT INTO test(value) VALUES (?)") .bind(0, 2) - .returnGeneratedValues("CURRENT_TIMESTAMP") + .returnGeneratedValues("POW(value, 4)") .execute()) - .flatMap(result -> result.map(r -> r.get(0, ZonedDateTime.class))) + .flatMap(result -> result.map(r -> r.get(0, Integer.class))) .collectList() - .doOnNext(list -> assertThat(list).hasSize(1) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10))))); + .doOnNext(list -> assertThat(list).hasSize(1)) + .doOnNext(list -> assertThat(list.get(0)).isEqualTo(16))); } @Test @@ -70,13 +75,9 @@ void allReturning() { .execute()) .flatMap(result -> result.map(DataEntity::read)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsExactly(2, 4, 6, 8, 10)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getId).distinct()).hasSize(5)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10)))) - .thenMany(conn.createStatement("REPLACE test(id, value) VALUES (1,?),(2,?),(3,?),(4,?),(5,?)") + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithSelectAll(conn, list)) + .thenMany(conn.createStatement("REPLACE test(id,value) VALUES (1,?),(2,?),(3,?),(4,?),(5,?)") .bind(0, 3) .bind(1, 5) .bind(2, 7) @@ -86,11 +87,8 @@ void allReturning() { .execute()) .flatMap(result -> result.map(DataEntity::read)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsExactly(3, 5, 7, 9, 11)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10))))); + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithSelectAll(conn, list))); } @Test @@ -107,38 +105,30 @@ void partialReturning() { .bind(2, 6) .bind(3, 8) .bind(4, 10) - .returnGeneratedValues("id", "created_at") + .returnGeneratedValues("id", "value") .execute()) - .flatMap(result -> result.map(DataEntity::withoutValue)) + .flatMap(result -> result.map(DataEntity::withoutCreatedAt)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsOnly(0)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getId).distinct()).hasSize(5)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10)))) + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithoutCreatedAt(conn, list)) .thenMany(conn.createStatement("REPLACE test(id, value) VALUES (1,?),(2,?),(3,?),(4,?),(5,?)") .bind(0, 3) .bind(1, 5) .bind(2, 7) .bind(3, 9) .bind(4, 11) - .returnGeneratedValues("id", "created_at") + .returnGeneratedValues("id", "value") .execute()) - .flatMap(result -> result.map(DataEntity::withoutValue)) + .flatMap(result -> result.map(DataEntity::withoutCreatedAt)) .collectList() - .doOnNext(list -> assertThat(list).hasSize(5) - .map(DataEntity::getValue) - .containsOnly(0)) - .doOnNext(list -> assertThat(list.stream().map(DataEntity::getCreatedAt)) - .noneMatch(it -> it.isBefore(ZonedDateTime.now().minusSeconds(10)))) - ); + .doOnNext(list -> assertThat(list).hasSize(5)) + .as(list -> assertWithoutCreatedAt(conn, list))); } @Test void returningGetRowUpdated() { complete(conn -> conn.createStatement("CREATE TEMPORARY TABLE test(" + - "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,value INT NOT NULL)") .execute() .flatMap(IntegrationTestSupport::extractRowsUpdated) .thenMany(conn.createStatement("INSERT INTO test(value) VALUES (?),(?)") @@ -150,6 +140,36 @@ void returningGetRowUpdated() { .doOnNext(it -> assertThat(it).isEqualTo(2))); } + private static Mono assertWithSelectAll(MySqlConnection conn, Mono> returning) { + return returning.zipWhen(list -> conn.createStatement("SELECT * FROM test WHERE id IN (?,?,?,?,?)") + .bind(0, list.get(0).getId()) + .bind(1, list.get(1).getId()) + .bind(2, list.get(2).getId()) + .bind(3, list.get(3).getId()) + .bind(4, list.get(4).getId()) + .execute() + .flatMap(result -> result.map(DataEntity::read)) + .collectList()) + .doOnNext(list -> assertThat(list.getT1()).isEqualTo(list.getT2())) + .then(); + } + + private static Mono assertWithoutCreatedAt(MySqlConnection conn, Mono> returning) { + String sql = "SELECT id,value FROM test WHERE id IN (?,?,?,?,?)"; + + return returning.zipWhen(list -> conn.createStatement(sql) + .bind(0, list.get(0).getId()) + .bind(1, list.get(1).getId()) + .bind(2, list.get(2).getId()) + .bind(3, list.get(3).getId()) + .bind(4, list.get(4).getId()) + .execute() + .flatMap(result -> result.map(DataEntity::withoutCreatedAt)) + .collectList()) + .doOnNext(list -> assertThat(list.getT1()).isEqualTo(list.getT2())) + .then(); + } + private static final class DataEntity { private final int id; @@ -176,6 +196,38 @@ ZonedDateTime getCreatedAt() { return createdAt; } + DataEntity incremented() { + return new DataEntity(id, value + 1, createdAt); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DataEntity)) { + return false; + } + + DataEntity that = (DataEntity) o; + + return id == that.id && value == that.value && createdAt.equals(that.createdAt); + } + + @Override + public int hashCode() { + int result = 31 * id + value; + return 31 * result + createdAt.hashCode(); + } + + @Override + public String toString() { + return "DataEntity{id=" + id + + ", value=" + value + + ", createdAt=" + createdAt + + '}'; + } + static DataEntity read(Readable readable) { Integer id = readable.get("id", Integer.TYPE); Integer value = readable.get("value", Integer.class); @@ -188,14 +240,14 @@ static DataEntity read(Readable readable) { return new DataEntity(id, value, createdAt); } - static DataEntity withoutValue(Readable readable) { + static DataEntity withoutCreatedAt(Readable readable) { Integer id = readable.get("id", Integer.TYPE); - ZonedDateTime createdAt = readable.get("created_at", ZonedDateTime.class); + Integer value = readable.get("value", Integer.TYPE); requireNonNull(id, "id must not be null"); - requireNonNull(createdAt, "createdAt must not be null"); + requireNonNull(value, "value must not be null"); - return new DataEntity(id, 0, createdAt); + return new DataEntity(id, value, ZonedDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC)); } } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java index e0baef7d0..717ecaa0f 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java @@ -243,7 +243,9 @@ private static MySqlConnectionConfiguration filledUp() { .tlsVersion(TlsVersions.TLS1_1, TlsVersions.TLS1_2, TlsVersions.TLS1_3) .compressionAlgorithms(CompressionAlgorithm.ZSTD, CompressionAlgorithm.ZLIB, CompressionAlgorithm.UNCOMPRESSED) - .serverZoneId(ZoneId.systemDefault()) + .preserveInstants(true) + .connectionTimeZone("LOCAL") + .forceConnectionTimeZoneToSession(true) .zeroDateOption(ZeroDateOption.USE_NULL) .sslHostnameVerifier((host, s) -> true) .queryCacheSize(128) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index d34a947bf..41b2ef45a 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -142,7 +142,7 @@ void validProgrammaticHost() { .option(SSL, true) .option(Option.valueOf(CONNECT_TIMEOUT.name()), Duration.ofSeconds(3).toString()) .option(DATABASE, "r2dbc") - .option(Option.valueOf("serverZoneId"), "Asia/Tokyo") + .option(Option.valueOf("connectionTimeZone"), "Asia/Tokyo") .option(Option.valueOf("useServerPrepareStatement"), AllTruePredicate.class.getName()) .option(Option.valueOf("zeroDate"), "use_round") .option(Option.valueOf("sslMode"), "verify_identity") @@ -171,7 +171,7 @@ void validProgrammaticHost() { assertThat(configuration.getZeroDateOption()).isEqualTo(ZeroDateOption.USE_ROUND); assertThat(configuration.isTcpKeepAlive()).isTrue(); assertThat(configuration.isTcpNoDelay()).isTrue(); - assertThat(configuration.getServerZoneId()).isEqualTo(ZoneId.of("Asia/Tokyo")); + assertThat(configuration.getConnectionTimeZone()).isEqualTo("Asia/Tokyo"); assertThat(configuration.getPreferPrepareStatement()).isExactlyInstanceOf(AllTruePredicate.class); assertThat(configuration.getExtensions()).isEqualTo(Extensions.from(Collections.emptyList(), true)); @@ -288,7 +288,7 @@ void validProgrammaticUnixSocket() { .option(Option.valueOf(CONNECT_TIMEOUT.name()), Duration.ofSeconds(3).toString()) .option(DATABASE, "r2dbc") .option(Option.valueOf("createDatabaseIfNotExist"), true) - .option(Option.valueOf("serverZoneId"), "Asia/Tokyo") + .option(Option.valueOf("connectionTimeZone"), "Asia/Tokyo") .option(Option.valueOf("useServerPrepareStatement"), AllTruePredicate.class.getName()) .option(Option.valueOf("zeroDate"), "use_round") .option(Option.valueOf("sslMode"), "verify_identity") @@ -314,7 +314,7 @@ void validProgrammaticUnixSocket() { assertThat(configuration.getZeroDateOption()).isEqualTo(ZeroDateOption.USE_ROUND); assertThat(configuration.isTcpKeepAlive()).isTrue(); assertThat(configuration.isTcpNoDelay()).isTrue(); - assertThat(configuration.getServerZoneId()).isEqualTo(ZoneId.of("Asia/Tokyo")); + assertThat(configuration.getConnectionTimeZone()).isEqualTo("Asia/Tokyo"); assertThat(configuration.getPreferPrepareStatement()).isExactlyInstanceOf(AllTruePredicate.class); assertThat(configuration.getExtensions()).isEqualTo(Extensions.from(Collections.emptyList(), true)); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java index 635d94921..7b85d4150 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java @@ -21,9 +21,7 @@ import org.springframework.jdbc.core.JdbcTemplate; import java.time.Duration; -import java.time.ZoneId; import java.util.Optional; -import java.util.TimeZone; /** * Base class considers integration tests of {@link TestKit}. @@ -88,11 +86,10 @@ private static JdbcTemplate jdbc(MySqlConnectionConfiguration configuration) { source.setConnectionTimeout(Optional.ofNullable(configuration.getConnectTimeout()) .map(Duration::toMillis).orElse(0L)); - ZoneId zoneId = configuration.getServerZoneId(); - - if (zoneId != null) { - source.addDataSourceProperty("serverTimezone", TimeZone.getTimeZone(zoneId).getID()); - } + source.addDataSourceProperty("preserveInstants", configuration.isPreserveInstants()); + source.addDataSourceProperty("connectionTimeZone", configuration.getConnectionTimeZone()); + source.addDataSourceProperty("forceConnectionTimeZoneToSession", + configuration.isForceConnectionTimeZoneToSession()); return new JdbcTemplate(source); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareDateTimeIntegrationTest.java similarity index 88% rename from r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareDateTimeIntegrationTest.java index 6b53312e5..11c26a547 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareTimeZoneIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareDateTimeIntegrationTest.java @@ -19,9 +19,9 @@ /** * Integration tests for time zone conversion in the binary protocol. */ -class PrepareTimeZoneIntegrationTest extends TimeZoneIntegrationTestSupport { +class PrepareDateTimeIntegrationTest extends DateTimeIntegrationTestSupport { - PrepareTimeZoneIntegrationTest() { + PrepareDateTimeIntegrationTest() { super(builder -> builder.useServerPrepareStatement(sql -> true)); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java index 49ed0b672..a121374e9 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java @@ -16,12 +16,16 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.internal.util.StringUtils; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.time.ZoneId; import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; @@ -29,10 +33,63 @@ import java.util.stream.Stream; /** - * Integration tests for session state. + * Integration tests for session states. */ class SessionStateIntegrationTest { + @Test + void forcedLocalTimeZone() { + ZoneId zoneId = ZoneId.systemDefault().normalized(); + + connectionFactory(builder -> builder.connectionTimeZone("local") + .forceConnectionTimeZoneToSession(true)) + .create() + .flatMapMany( + connection -> connection.createStatement("SELECT @@time_zone").execute() + .flatMap(result -> result.map(r -> r.get(0, String.class))) + .map(StringUtils::parseZoneId) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(zoneId) + .verifyComplete(); + } + + @ParameterizedTest + @ValueSource(strings = { + "America/New_York", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Tokyo", + "Europe/London", + "Factory", + "GMT", + "JST", + "ROC", + "UTC", + "+00:00", + "+09:00", + "-09:00", + }) + void forcedConnectionTimeZone(String timeZone) { + ZoneId zoneId = StringUtils.parseZoneId(timeZone); + + connectionFactory(builder -> builder.connectionTimeZone(timeZone) + .forceConnectionTimeZoneToSession(true)) + .create() + .flatMapMany( + connection -> connection.createStatement("SELECT @@time_zone").execute() + .flatMap(result -> result.map(r -> r.get(0, String.class))) + .map(StringUtils::parseZoneId) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(zoneId) + .verifyComplete(); + } + @ParameterizedTest @MethodSource void sessionVariables(Map variables) { @@ -50,10 +107,10 @@ void sessionVariables(Map variables) { connectionFactory(builder -> builder.sessionVariables(pairs)) .create() .flatMapMany(connection -> connection.createStatement(selection).execute() - .flatMap(result -> result.map((row, metadata) -> { + .flatMap(result -> result.map(r -> { Map map = new LinkedHashMap<>(); for (String key : keys) { - map.put(key, row.get(key, String.class)); + map.put(key, r.get(key, String.class)); } return map; })) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextDateTimeIntegrationTest.java similarity index 88% rename from r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextDateTimeIntegrationTest.java index 336a0d5c1..4d1e153c0 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextTimeZoneIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextDateTimeIntegrationTest.java @@ -19,9 +19,9 @@ /** * Integration tests for time zone conversion in the text protocol. */ -class TextTimeZoneIntegrationTest extends TimeZoneIntegrationTestSupport { +class TextDateTimeIntegrationTest extends DateTimeIntegrationTestSupport { - TextTimeZoneIntegrationTest() { + TextDateTimeIntegrationTest() { super(builder -> builder); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java new file mode 100644 index 000000000..0d302494c --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java @@ -0,0 +1,362 @@ +package io.asyncer.r2dbc.mysql; + +import com.zaxxer.hikari.HikariDataSource; +import org.assertj.core.data.TemporalUnitOffset; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.jdbc.core.JdbcTemplate; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.function.Tuple3; +import reactor.util.function.Tuple4; +import reactor.util.function.Tuple6; +import reactor.util.function.Tuple8; +import reactor.util.function.Tuples; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.Optional; +import java.util.TimeZone; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +/** + * Integration tests for aligning time zone configuration options with jdbc. + */ +@Isolated +class TimeZoneIntegrationTest { + + // Earlier versions did not support microseconds, so it is almost always within 1 second, extending to + // 2 seconds due to network reasons + private static final TemporalUnitOffset TINY_WITHIN = within(2, ChronoUnit.SECONDS); + + private static TimeZone defaultTimeZone; + + @BeforeAll + static void setUpTimeZone() { + defaultTimeZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("GMT+9:30")); + } + + @AfterAll + static void tearDownTimeZone() { + TimeZone.setDefault(defaultTimeZone); + } + + @BeforeEach + void setUp() { + String tdl = "CREATE TABLE IF NOT EXISTS test_time_zone (" + + "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," + + "data1 DATETIME" + dateTimeSuffix(false) + " NOT NULL," + + "data2 TIMESTAMP" + dateTimeSuffix(false) + " NOT NULL)"; + + MySqlConnectionFactory.from(configuration(Function.identity())).create() + .flatMapMany(connection -> connection.createStatement(tdl) + .execute() + .flatMap(MySqlResult::getRowsUpdated) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .then(connection.close())) + .as(StepVerifier::create) + .verifyComplete(); + } + + @AfterEach + void tearDown() { + MySqlConnectionFactory.from(configuration(Function.identity())).create() + .flatMapMany(connection -> connection.createStatement("DROP TABLE IF EXISTS test_time_zone") + .execute() + .flatMap(MySqlResult::getRowsUpdated) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .then(connection.close())) + .as(StepVerifier::create) + .verifyComplete(); + } + + @ParameterizedTest + @MethodSource + void alignDateTimeFunction(boolean instants, String timeZone, boolean force) { + String selectQuery = "SELECT CURRENT_TIMESTAMP" + dateTimeSuffix(true) + + ", NOW" + dateTimeSuffix(true) + + ", CURRENT_TIME" + dateTimeSuffix(true) + + ", CURRENT_DATE()"; + MySqlConnectionConfiguration config = configuration(builder -> builder + .preserveInstants(instants) + .connectionTimeZone(timeZone) + .forceConnectionTimeZoneToSession(force)); + JdbcTemplate jdbc = jdbc(config); + + Tuple4< + Tuple3, + Tuple3, + Tuple3, + LocalDate + > expectedTuples = jdbc.query(selectQuery, (rs, ignored) -> Tuples.of( + Tuples.of( + requireNonNull(rs.getObject(1, LocalDateTime.class)), + requireNonNull(rs.getObject(1, ZonedDateTime.class)), + requireNonNull(rs.getObject(1, OffsetDateTime.class)) + ), + Tuples.of( + requireNonNull(rs.getObject(2, LocalDateTime.class)), + requireNonNull(rs.getObject(2, ZonedDateTime.class)), + requireNonNull(rs.getObject(2, OffsetDateTime.class)) + ), + Tuples.of( + rs.getObject(3, LocalTime.class), + rs.getObject(3, OffsetTime.class), + rs.getObject(3, Duration.class) + ), + rs.getObject(4, LocalDate.class) + )).get(0); + + MySqlConnectionFactory.from(config).create() + .flatMapMany(connection -> connection.createStatement(selectQuery) + .execute() + .flatMap(result -> result.map((row, metadata) -> Tuples.of( + Tuples.of( + requireNonNull(row.get(0, LocalDateTime.class)), + requireNonNull(row.get(0, ZonedDateTime.class)), + requireNonNull(row.get(0, OffsetDateTime.class)), + requireNonNull(row.get(0, Instant.class)) + ), + Tuples.of( + requireNonNull(row.get(1, LocalDateTime.class)), + requireNonNull(row.get(1, ZonedDateTime.class)), + requireNonNull(row.get(1, OffsetDateTime.class)), + requireNonNull(row.get(1, Instant.class)) + ), + Tuples.of( + requireNonNull(row.get(2, LocalTime.class)), + requireNonNull(row.get(2, OffsetTime.class)), + requireNonNull(row.get(2, Duration.class)) + ), + requireNonNull(row.get(3, LocalDate.class)) + ))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty()))) + .as(StepVerifier::create) + .assertNext(data -> { + assertDateTimeTuples(data.getT1(), expectedTuples.getT1()); + assertDateTimeTuples(data.getT2(), expectedTuples.getT2()); + + assertThat(data.getT3().getT1()).isCloseTo(expectedTuples.getT3().getT1(), TINY_WITHIN); + assertThat(data.getT3().getT2().getOffset()) + .isEqualTo(expectedTuples.getT3().getT2().getOffset()); + assertThat(data.getT3().getT2()).isCloseTo(expectedTuples.getT3().getT2(), TINY_WITHIN); + assertThat(data.getT3().getT3()).isCloseTo(expectedTuples.getT3().getT3(), Duration.ofSeconds(2)); + + // If the test case is run close to UTC midnight, it may fail, just run it again + assertThat(data.getT4()).isEqualTo(expectedTuples.getT4()); + }) + .verifyComplete(); + + requireNonNull((HikariDataSource) jdbc.getDataSource()).close(); + } + + @ParameterizedTest + @MethodSource + void alignSendAndReceiveTimeZoneOption(boolean instants, String timeZone, boolean force, Temporal now) { + String insertQuery = "INSERT INTO test_time_zone VALUES (DEFAULT, ?, ?)"; + String selectQuery = "SELECT data1, data2 FROM test_time_zone"; + MySqlConnectionConfiguration config = configuration(builder -> builder + .preserveInstants(instants) + .connectionTimeZone(timeZone) + .forceConnectionTimeZoneToSession(force)); + JdbcTemplate jdbc = jdbc(config); + + assertThat(jdbc.update(insertQuery, now, now)).isOne(); + Tuple6 expectedTuples = jdbc.query(selectQuery, (rs, ignored) -> Tuples.of( + requireNonNull(rs.getObject(1, LocalDateTime.class)), + requireNonNull(rs.getObject(2, LocalDateTime.class)), + requireNonNull(rs.getObject(1, ZonedDateTime.class)), + requireNonNull(rs.getObject(2, ZonedDateTime.class)), + requireNonNull(rs.getObject(1, OffsetDateTime.class)), + requireNonNull(rs.getObject(2, OffsetDateTime.class)) + )).get(0); + Consumer> assertion = actual -> { + assertThat(actual.getT1()).isCloseTo(expectedTuples.getT1(), TINY_WITHIN); + assertThat(actual.getT2()).isCloseTo(expectedTuples.getT2(), TINY_WITHIN); + assertThat(actual.getT3().getZone().normalized()) + .isEqualTo(expectedTuples.getT3().getZone().normalized()); + assertThat(actual.getT3()).isCloseTo(expectedTuples.getT3(), TINY_WITHIN); + assertThat(actual.getT4().getZone().normalized()) + .isEqualTo(expectedTuples.getT4().getZone().normalized()); + assertThat(actual.getT4()).isCloseTo(expectedTuples.getT4(), TINY_WITHIN); + assertThat(actual.getT5().getOffset()).isEqualTo(expectedTuples.getT5().getOffset()); + assertThat(actual.getT5()).isCloseTo(expectedTuples.getT5(), TINY_WITHIN); + assertThat(actual.getT6().getOffset()).isEqualTo(expectedTuples.getT6().getOffset()); + assertThat(actual.getT6()).isCloseTo(expectedTuples.getT6(), TINY_WITHIN); + assertThat(actual.getT7()).isCloseTo(expectedTuples.getT3().toInstant(), TINY_WITHIN); + assertThat(actual.getT8()).isCloseTo(expectedTuples.getT4().toInstant(), TINY_WITHIN); + assertThat(actual.getT7()).isCloseTo(expectedTuples.getT5().toInstant(), TINY_WITHIN); + assertThat(actual.getT8()).isCloseTo(expectedTuples.getT6().toInstant(), TINY_WITHIN); + }; + + MySqlConnectionFactory.from(config).create() + .flatMapMany(connection -> connection.createStatement(insertQuery) + .bind(0, now) + .bind(1, now) + .execute() + .flatMap(MySqlResult::getRowsUpdated) + .thenMany(connection.createStatement(selectQuery).execute()) + .flatMap(result -> result.map(r -> Tuples.of( + requireNonNull(r.get(0, LocalDateTime.class)), + requireNonNull(r.get(1, LocalDateTime.class)), + requireNonNull(r.get(0, ZonedDateTime.class)), + requireNonNull(r.get(1, ZonedDateTime.class)), + requireNonNull(r.get(0, OffsetDateTime.class)), + requireNonNull(r.get(1, OffsetDateTime.class)), + requireNonNull(r.get(0, Instant.class)), + requireNonNull(r.get(1, Instant.class)) + ))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty()))) + .as(StepVerifier::create) + .assertNext(assertion) + .assertNext(assertion) + .verifyComplete(); + + requireNonNull((HikariDataSource) jdbc.getDataSource()).close(); + } + + static Stream alignDateTimeFunction() { + return Stream.of( + Arguments.of(false, "LOCAL", false), + Arguments.of(false, "LOCAL", true), + Arguments.of(true, "LOCAL", false), + Arguments.of(true, "LOCAL", true), + Arguments.of(false, "SERVER", false), + Arguments.of(false, "SERVER", true), + Arguments.of(true, "SERVER", false), + Arguments.of(true, "SERVER", true), + Arguments.of(false, "GMT+2", false), + Arguments.of(false, "GMT+3", true), + Arguments.of(true, "GMT+4", false), + Arguments.of(true, "GMT+5", true) + ); + } + + static Stream alignSendAndReceiveTimeZoneOption() { + ZonedDateTime dateTime = ZonedDateTime.now(); + + return Stream.of(dateTime).flatMap(now -> Stream.of( + now.toLocalDateTime(), + now, + now.toOffsetDateTime(), + now.withZoneSameInstant(ZoneOffset.ofHours(2)).toLocalDateTime(), + now.withZoneSameInstant(ZoneOffset.ofHours(3)), + now.withZoneSameInstant(ZoneOffset.ofHours(4)).toOffsetDateTime(), + now.withZoneSameLocal(ZoneOffset.ofHours(5)).toLocalDateTime(), + now.withZoneSameLocal(ZoneOffset.ofHours(6)), + now.withZoneSameLocal(ZoneOffset.ofHours(7)).toOffsetDateTime() + )).flatMap(temporal -> Stream.of( + Arguments.of(false, "LOCAL", false, temporal), + Arguments.of(false, "LOCAL", true, temporal), + Arguments.of(true, "LOCAL", false, temporal), + Arguments.of(true, "LOCAL", true, temporal), + Arguments.of(false, "SERVER", false, temporal), + Arguments.of(false, "SERVER", true, temporal), + Arguments.of(true, "SERVER", false, temporal), + Arguments.of(true, "SERVER", true, temporal), + Arguments.of(false, "GMT+1", false, temporal), + Arguments.of(false, "GMT+1", true, temporal), + Arguments.of(true, "GMT+1", false, temporal), + Arguments.of(true, "GMT+1", true, temporal) + )); + } + + private static MySqlConnectionConfiguration configuration( + Function customizer + ) { + String password = System.getProperty("test.mysql.password"); + + if (password == null || password.isEmpty()) { + throw new IllegalStateException("Property test.mysql.password must exists and not be empty"); + } + + MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() + .host("localhost") + .port(3306) + .user("root") + .password(password) + .database("r2dbc"); + + return customizer.apply(builder).build(); + } + + private static JdbcTemplate jdbc(MySqlConnectionConfiguration config) { + HikariDataSource source = new HikariDataSource(); + + source.setJdbcUrl(String.format("jdbc:mysql://%s:%d/%s", config.getDomain(), + config.getPort(), config.getDatabase())); + source.setUsername(config.getUser()); + source.setPassword(Optional.ofNullable(config.getPassword()) + .map(Object::toString).orElse(null)); + source.setMaximumPoolSize(1); + source.setConnectionTimeout(Optional.ofNullable(config.getConnectTimeout()) + .map(Duration::toMillis).orElse(0L)); + + source.addDataSourceProperty("preserveInstants", config.isPreserveInstants()); + source.addDataSourceProperty("connectionTimeZone", config.getConnectionTimeZone()); + source.addDataSourceProperty("forceConnectionTimeZoneToSession", + config.isForceConnectionTimeZoneToSession()); + + return new JdbcTemplate(source); + } + + private static String dateTimeSuffix(boolean function) { + String version = System.getProperty("test.mysql.version"); + return version != null && isMicrosecondSupported(version) ? "(6)" : function ? "()" : ""; + } + + private static boolean isMicrosecondSupported(String version) { + if (version.isEmpty()) { + return false; + } + + ServerVersion ver = ServerVersion.parse(version); + String type = System.getProperty("test.db.type"); + + return "mariadb".equalsIgnoreCase(type) || + ver.isGreaterThanOrEqualTo(ServerVersion.create(5, 6, 0)); + } + + private static void assertDateTimeTuples( + Tuple4 actual, + Tuple3 expected + ) { + assertThat(actual.getT1()).isCloseTo(expected.getT1(), TINY_WITHIN); + assertThat(actual.getT2().getZone().normalized()) + .isEqualTo(expected.getT2().getZone().normalized()); + assertThat(actual.getT2()).isCloseTo(expected.getT2(), TINY_WITHIN); + assertThat(actual.getT3().getOffset()) + .isEqualTo(expected.getT3().getOffset()); + assertThat(actual.getT3()).isCloseTo(expected.getT3(), TINY_WITHIN); + assertThat(actual.getT4()) + .isCloseTo(expected.getT2().toInstant(), TINY_WITHIN); + assertThat(actual.getT4()) + .isCloseTo(expected.getT3().toInstant(), TINY_WITHIN); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java index 100ea09cf..8d4adf4b1 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/DateTimeCodecTestSupport.java @@ -21,7 +21,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; -import java.time.ZoneId; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.SignStyle; @@ -29,14 +29,20 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; -import static java.time.temporal.ChronoField.*; +import static java.time.temporal.ChronoField.DAY_OF_MONTH; +import static java.time.temporal.ChronoField.HOUR_OF_DAY; +import static java.time.temporal.ChronoField.MICRO_OF_SECOND; +import static java.time.temporal.ChronoField.MINUTE_OF_HOUR; +import static java.time.temporal.ChronoField.MONTH_OF_YEAR; +import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.YEAR; /** * Base class considers codecs unit tests of date/time. */ abstract class DateTimeCodecTestSupport implements CodecTestSupport { - protected static final ZoneId ENCODE_SERVER_ZONE = ZoneId.of("+6"); + protected static final ZoneOffset ENCODE_SERVER_ZONE = ZoneOffset.ofHours(6); private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() .appendLiteral('\'') diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java index 97774b2a4..8485b1059 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodecTest.java @@ -22,6 +22,7 @@ import java.time.Instant; import java.time.LocalDateTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Arrays; @@ -58,6 +59,9 @@ public ByteBuf[] binaryParameters(Charset charset) { } private LocalDateTime convert(OffsetDateTime value) { - return value.withOffsetSameInstant((ZoneOffset) ENCODE_SERVER_ZONE).toLocalDateTime(); + ZoneOffset offset = context().isPreserveInstants() ? ENCODE_SERVER_ZONE : + ZoneId.systemDefault().getRules().getOffset(value.toLocalDateTime()); + + return value.withOffsetSameInstant(offset).toLocalDateTime(); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java index 4aa3ecdee..b3d849745 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodecTest.java @@ -19,8 +19,10 @@ import io.netty.buffer.ByteBuf; import java.nio.charset.Charset; +import java.time.Instant; import java.time.LocalTime; import java.time.OffsetTime; +import java.time.ZoneId; import java.time.ZoneOffset; import java.util.Arrays; @@ -67,6 +69,9 @@ public ByteBuf[] binaryParameters(Charset charset) { } private LocalTime convert(OffsetTime value) { - return value.withOffsetSameInstant((ZoneOffset) ENCODE_SERVER_ZONE).toLocalTime(); + ZoneId zone = ZoneId.systemDefault().normalized(); + + return value.withOffsetSameInstant(zone instanceof ZoneOffset ? (ZoneOffset) zone : + zone.getRules().getStandardOffset(Instant.EPOCH)).toLocalTime(); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java index 66697a75a..10f729193 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodecTest.java @@ -75,6 +75,10 @@ public ByteBuf[] binaryParameters(Charset charset) { } private LocalDateTime convert(ZonedDateTime value) { - return value.withZoneSameInstant(ENCODE_SERVER_ZONE).toLocalDateTime(); + if (context().isPreserveInstants()) { + return value.withZoneSameInstant(ENCODE_SERVER_ZONE).toLocalDateTime(); + } + + return value.toLocalDateTime(); } } diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 2c49360d3..ac3368454 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -43,4 +43,4 @@ - \ No newline at end of file + From 436ddade28953b06c3621b9be2c62d9a5a7b2671 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Wed, 21 Feb 2024 11:35:15 +0900 Subject: [PATCH 21/93] Default to use LocalDateTime for DATETIME --- .../main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java index 196b70cf0..d425cd9cd 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java @@ -23,6 +23,7 @@ import java.math.BigInteger; import java.nio.ByteBuffer; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.LocalTime; import java.time.ZonedDateTime; @@ -271,7 +272,7 @@ public int getBinarySize() { /** * A date time type. It does not contain timezone. It uses string format to transfer the value. */ - DATETIME(MySqlType.ID_DATETIME, ZonedDateTime.class), + DATETIME(MySqlType.ID_DATETIME, LocalDateTime.class), /** * A year type. It contains neither leap year information nor timezone. From a5eb9a85ffa82b51d0d6a325a631c38f1daab238 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Mon, 26 Feb 2024 20:12:30 +0900 Subject: [PATCH 22/93] Correct workflows and pom files --- .github/workflows/cd-release.yml | 2 +- .github/workflows/ci-graalvm-tests.yml | 2 +- pom.xml | 345 ------------------------- r2dbc-mysql/pom.xml | 344 +++++++++++++++++++++++- test-native-image/pom.xml | 55 +++- 5 files changed, 389 insertions(+), 359 deletions(-) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 70e1ce82a..c11ba2af7 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -49,7 +49,7 @@ jobs: - name: Run release prepare command run: | - ./mvnw -B -ntp --file pom.xml release:prepare -DpreparationGoals=clean -DskipTests=true + ./mvnw -B -ntp -pl r2dbc-mysql release:prepare -DpreparationGoals=clean -DskipTests=true ./mvnw -B -ntp clean - name: Ensure Prepared diff --git a/.github/workflows/ci-graalvm-tests.yml b/.github/workflows/ci-graalvm-tests.yml index 8bc67186d..c963bed88 100644 --- a/.github/workflows/ci-graalvm-tests.yml +++ b/.github/workflows/ci-graalvm-tests.yml @@ -50,4 +50,4 @@ jobs: echo "JAVA_HOME=$JAVA_HOME" echo "./mvnw -Pgraalvm package" ./mvnw -Pgraalvm package - ./test-native-image/target/test-native-image -Dtest.mysql.password=r2dbc-password!@ \ No newline at end of file + ./test-native-image/target/test-native-image -Dtest.mysql.password=r2dbc-password!@ diff --git a/pom.xml b/pom.xml index 8c5ae80cf..93b6dd630 100644 --- a/pom.xml +++ b/pom.xml @@ -17,358 +17,13 @@ 4.0.0 - io.asyncer r2dbc-mysql-parent 1.1.2-SNAPSHOT pom - Reactive Relational Database Connectivity - MySQL - https://github.com/asyncer-io/r2dbc-mysql r2dbc-mysql test-native-image - 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 - HEAD - - - - UTF-8 - UTF-8 - 1.8 - false - true - - 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 - 1.5.5-11 - 24.1.0 - 1.77 - 21.2.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 - - - org.bouncycastle - bcpkix-jdk18on - ${bouncy-castle.version} - 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-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 - - - - - - - graalvm - - - ${java.home}/bin/gu - - - - false - - - - 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/ - - diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index da87d0c2c..69ee18212 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -3,20 +3,118 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - io.asyncer - r2dbc-mysql-parent - 1.1.2-SNAPSHOT - + io.asyncer r2dbc-mysql + 1.1.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 + + 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 + HEAD + + UTF-8 + UTF-8 + 1.8 8 8 - UTF-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 + 1.5.5-11 + 24.1.0 + 1.77 + + + + 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 + + + org.bouncycastle + bcpkix-jdk18on + ${bouncy-castle.version} + test + + + + io.projectreactor @@ -151,4 +249,238 @@ + + + + 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-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.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/ + + diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index ac3368454..adb8dcd94 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -3,13 +3,22 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - io.asyncer - r2dbc-mysql-parent - 1.1.2-SNAPSHOT - - + io.asyncer test-native-image + 1.1.2-SNAPSHOT + + + UTF-8 + UTF-8 + 1.8 + 8 + 8 + true + + 2022.0.9 + 1.0.0.RELEASE + 21.2.0 + @@ -17,8 +26,29 @@ r2dbc-mysql ${project.version} + + io.r2dbc + r2dbc-spi + ${r2dbc-spi.version} + + + io.projectreactor + reactor-core + + + + + io.projectreactor + reactor-bom + ${reactor.version} + pom + import + + + + @@ -43,4 +73,17 @@ + + + graalvm + + + ${java.home}/bin/gu + + + + false + + + From 59548656b3ebf1d017dba5c69a3c65df1a43086d Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 27 Feb 2024 11:35:08 +0900 Subject: [PATCH 23/93] Resolve native image warnings --- .github/workflows/ci-graalvm-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-graalvm-tests.yml b/.github/workflows/ci-graalvm-tests.yml index c963bed88..46135627d 100644 --- a/.github/workflows/ci-graalvm-tests.yml +++ b/.github/workflows/ci-graalvm-tests.yml @@ -48,6 +48,6 @@ jobs: - name: Build and run native image run: | echo "JAVA_HOME=$JAVA_HOME" - echo "./mvnw -Pgraalvm package" - ./mvnw -Pgraalvm package + echo "./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true" + ./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true ./test-native-image/target/test-native-image -Dtest.mysql.password=r2dbc-password!@ From 04f6221867dd9e59ce9d8de2fd05c759590238b5 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 27 Feb 2024 13:59:32 +0900 Subject: [PATCH 24/93] Correct check scripts --- .github/scripts/ensure_prepared.sh | 4 ++-- .github/scripts/release_rollback.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/ensure_prepared.sh b/.github/scripts/ensure_prepared.sh index 1e4df3995..73e5f2586 100755 --- a/.github/scripts/ensure_prepared.sh +++ b/.github/scripts/ensure_prepared.sh @@ -16,7 +16,7 @@ # set -e -TAG=$(grep scm.tag= release.properties | cut -d'=' -f2) +TAG=$(grep scm.tag= r2dbc-mysql/release.properties | cut -d'=' -f2) echo "checkout tag $TAG" git checkout "$TAG" -exit 0 \ No newline at end of file +exit 0 diff --git a/.github/scripts/release_rollback.sh b/.github/scripts/release_rollback.sh index dd505e92c..14582ed9a 100755 --- a/.github/scripts/release_rollback.sh +++ b/.github/scripts/release_rollback.sh @@ -21,4 +21,4 @@ git remote set-url origin git@github.com:asyncer-io/r2dbc-mysql.git git fetch git checkout "$1" ./mvnw -B --file pom.xml release:rollback -git push origin :"$TAG" \ No newline at end of file +git push origin :"$TAG" From c41c5ef3ceb353c24c38b056b84c4e936fe6c881 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Tue, 27 Feb 2024 05:07:41 +0000 Subject: [PATCH 25/93] [maven-release-plugin] prepare release r2dbc-mysql-1.1.2 --- r2dbc-mysql/pom.xml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 69ee18212..0955caff9 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -1,12 +1,10 @@ - + 4.0.0 io.asyncer r2dbc-mysql - 1.1.2-SNAPSHOT + 1.1.2 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -40,7 +38,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.1.2 From 24f188fd3d0fdb5e3b38e3fe281d1d28024ec853 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Tue, 27 Feb 2024 05:07:44 +0000 Subject: [PATCH 26/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 0955caff9..7fe2c5bcb 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -4,7 +4,7 @@ io.asyncer r2dbc-mysql - 1.1.2 + 1.1.3-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -38,7 +38,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.1.2 + HEAD From 545b8408dc5601422e3bd1d24b9635bbf6c76296 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 27 Feb 2024 14:58:39 +0900 Subject: [PATCH 27/93] Correct version auto-update --- .../scripts/upgrade_native_image_version.sh | 23 +++++++++++++++++++ .github/workflows/cd-release.yml | 9 +++++++- test-native-image/pom.xml | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/upgrade_native_image_version.sh diff --git a/.github/scripts/upgrade_native_image_version.sh b/.github/scripts/upgrade_native_image_version.sh new file mode 100644 index 000000000..7c978d0b0 --- /dev/null +++ b/.github/scripts/upgrade_native_image_version.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# +# Copyright 2024 asyncer.io projects +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +set -e + +VERSION=$(grep 'project.dev.io.asyncer\\:r2dbc-mysql=' r2dbc-mysql/release.properties | cut -d'=' -f2) + +echo 'Set test-native-image version to' $VERSION +./mvnw -pl test-native-image versions:set -DnewVersion=$VERSION +git add test-native-image/pom.xml diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index c11ba2af7..0bef91e4a 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -47,9 +47,16 @@ jobs: key: ${{ runner.os }}-prepare-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-prepare- + - name: DryRun Release Prepare + run: | + ./mvnw -B -ntp -pl r2dbc-mysql release:prepare -DpreparationGoals=clean -DdryRun=true -DskipTests=true + + - name: Upgrade Native Image Version + run: ./.github/scripts/upgrade_native_image_version.sh + - name: Run release prepare command run: | - ./mvnw -B -ntp -pl r2dbc-mysql release:prepare -DpreparationGoals=clean -DskipTests=true + ./mvnw -B -ntp -pl r2dbc-mysql release:prepare -DpreparationGoals=clean -Dresume=false -DskipTests=true ./mvnw -B -ntp clean - name: Ensure Prepared diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index adb8dcd94..4d055475d 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.1.2-SNAPSHOT + 1.1.3-SNAPSHOT UTF-8 From 2192c2425216bf5334f75a7d697e7db9844b70d9 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 15 Feb 2024 19:34:12 +0900 Subject: [PATCH 28/93] Update README and POM File Motivation: To streamline and improve the readability of the extensive README document. Modification: Simplified and updated the README and POM files for better clarity and maintenance. Result: The README and POM files are now concise, cleaner, and up-to-date. --- README.md | 8 ++++---- r2dbc-mysql/pom.xml | 10 +++++++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0badecc56..4ddab2f62 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,6 @@ This driver provides the following features: - [x] Extensible, e.g. extend built-in `Codec`(s). - [x] MariaDB `RETURNING` clause. -## Maintainer - -This project is currently being maintained by [@jchrys](https://github.com/jchrys) - ## Version compatibility / Integration tests states ![MySQL 5.5 status](https://img.shields.io/badge/MySQL%205.5-pass-blue) ![MySQL 5.6 status](https://img.shields.io/badge/MySQL%205.6-pass-blue) @@ -145,6 +141,10 @@ This project is released under version 2.0 of the [Apache License](https://www.a ## Contributors + + + + Thanks a lot for your support! ## Supports diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 7fe2c5bcb..9029206d1 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -28,7 +28,15 @@ jchrys jchrys@me.com - Project Lead + Maintainer + + + + mirromutth + mirromutth + mirromutth@gmail.com + + Maintainer From 140c634ddf9229a7aa311802ccde1485b830b567 Mon Sep 17 00:00:00 2001 From: jchrys Date: Wed, 28 Feb 2024 11:33:00 +0900 Subject: [PATCH 29/93] Implementing Checkstyle (#208) Motivation: To ensure code consistency and quality through the adoption of Checkstyle Modification: Added Checkstyle plugin and configuration, updated code to comply with new coding standards. Set version as INTERNAL on internal modules Result: Enhanced code quality and consistency across the project, facilitating easier contributions and maintenance. --- .github/workflows/ci-integration-tests.yml | 2 + .../ci-mariadb-intergration-tests.yml | 2 + .github/workflows/ci-unit-tests.yml | 2 + build-tools/pom.xml | 25 +++ .../io/asyncer/checkstyle-suppressions.xml | 10 + .../main/resources/io/asyncer/checkstyle.xml | 203 ++++++++++++++++++ pom.xml | 3 +- r2dbc-mysql/pom.xml | 47 +++- .../asyncer/r2dbc/mysql/ColumnDefinition.java | 2 +- .../io/asyncer/r2dbc/mysql/MySqlBatch.java | 4 +- .../asyncer/r2dbc/mysql/MySqlConnection.java | 3 +- .../io/asyncer/r2dbc/mysql/MySqlResult.java | 7 +- .../io/asyncer/r2dbc/mysql/OptionMapper.java | 4 +- .../mysql/ParametrizedStatementSupport.java | 2 +- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 4 +- .../io/asyncer/r2dbc/mysql/ServerVersion.java | 2 +- .../mysql/authentication/package-info.java | 2 +- .../io/asyncer/r2dbc/mysql/cache/Caches.java | 3 + .../r2dbc/mysql/cache/package-info.java | 2 +- .../r2dbc/mysql/client/SslBridgeHandler.java | 4 +- .../r2dbc/mysql/client/package-info.java | 2 +- .../codec/AbstractLobMySqlParameter.java | 2 +- .../r2dbc/mysql/codec/BigIntegerCodec.java | 2 +- .../r2dbc/mysql/codec/BooleanCodec.java | 2 +- .../asyncer/r2dbc/mysql/codec/ClobCodec.java | 2 +- .../r2dbc/mysql/codec/DefaultCodecs.java | 1 - .../r2dbc/mysql/codec/StringCodec.java | 2 +- .../r2dbc/mysql/codec/lob/package-info.java | 2 +- .../r2dbc/mysql/internal/package-info.java | 2 +- .../mysql/message/client/InitDbMessage.java | 20 +- .../message/client/ScalarClientMessage.java | 2 +- .../message/client/SizedClientMessage.java | 4 +- .../r2dbc/mysql/message/package-info.java | 2 +- .../message/server/MetadataDecodeContext.java | 8 +- .../io/asyncer/r2dbc/mysql/package-info.java | 2 +- .../src/main/java/io/asyncer/Main.java | 9 +- .../main/java/io/asyncer/package-info.java | 23 ++ 37 files changed, 382 insertions(+), 38 deletions(-) create mode 100644 build-tools/pom.xml create mode 100644 build-tools/src/main/resources/io/asyncer/checkstyle-suppressions.xml create mode 100644 build-tools/src/main/resources/io/asyncer/checkstyle.xml create mode 100644 test-native-image/src/main/java/io/asyncer/package-info.java diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 16b0eadb1..f1ed4ab57 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -29,6 +29,7 @@ jobs: run: docker-compose -f ${{ github.workspace }}/containers/mysql-compose.yml up -d - name: Integration test with MySQL ${{ matrix.mysql-version }} run: | + set -o pipefail ./mvnw -B verify -Dmaven.javadoc.skip=true \ -Dmaven.surefire.skip=true \ -Dtest.mysql.password=r2dbc-password!@ \ @@ -37,5 +38,6 @@ jobs: -Dio.netty.leakDetectionLevel=paranoid \ -Dio.netty.leakDetection.targetRecords=32 \ | tee test.log + set +o pipefail - name: ensure no leaks run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/.github/workflows/ci-mariadb-intergration-tests.yml b/.github/workflows/ci-mariadb-intergration-tests.yml index ee4841ac1..959e8a3cc 100644 --- a/.github/workflows/ci-mariadb-intergration-tests.yml +++ b/.github/workflows/ci-mariadb-intergration-tests.yml @@ -29,6 +29,7 @@ jobs: run: docker-compose -f ${{ github.workspace }}/containers/mariadb-compose.yml up -d - name: Integration test with MySQL ${{ matrix.mysql-version }} run: | + set -o pipefail ./mvnw -B verify -Dmaven.javadoc.skip=true \ -Dmaven.surefire.skip=true \ -Dtest.mysql.password=r2dbc-password!@ \ @@ -39,5 +40,6 @@ jobs: -Dio.netty.leakDetectionLevel=paranoid \ -Dio.netty.leakDetection.targetRecords=32 \ | tee test.log + set +o pipefail - name: ensure no leaks run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml index c5f87fe71..fdf48ce0d 100644 --- a/.github/workflows/ci-unit-tests.yml +++ b/.github/workflows/ci-unit-tests.yml @@ -21,9 +21,11 @@ jobs: cache: maven - name: Unit test with Maven run: | + set -o pipefail ./mvnw -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ -Dio.netty.leakDetectionLevel=paranoid \ -Dio.netty.leakDetection.targetRecords=32 \ | tee test.log + set +o pipefail - name: ensure no leaks run: ./.github/scripts/ensure_no_leak.sh test.log diff --git a/build-tools/pom.xml b/build-tools/pom.xml new file mode 100644 index 000000000..96fc8ec27 --- /dev/null +++ b/build-tools/pom.xml @@ -0,0 +1,25 @@ + + + + + 4.0.0 + io.asyncer + build-tools + INTERNAL + diff --git a/build-tools/src/main/resources/io/asyncer/checkstyle-suppressions.xml b/build-tools/src/main/resources/io/asyncer/checkstyle-suppressions.xml new file mode 100644 index 000000000..ad162be87 --- /dev/null +++ b/build-tools/src/main/resources/io/asyncer/checkstyle-suppressions.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/build-tools/src/main/resources/io/asyncer/checkstyle.xml b/build-tools/src/main/resources/io/asyncer/checkstyle.xml new file mode 100644 index 000000000..641d43fec --- /dev/null +++ b/build-tools/src/main/resources/io/asyncer/checkstyle.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 93b6dd630..305230922 100644 --- a/pom.xml +++ b/pom.xml @@ -19,11 +19,12 @@ 4.0.0 io.asyncer r2dbc-mysql-parent - 1.1.2-SNAPSHOT + INTERNAL pom r2dbc-mysql test-native-image + build-tools diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 9029206d1..225353812 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -1,5 +1,22 @@ - + + 4.0.0 io.asyncer @@ -257,6 +274,34 @@ + + 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 diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java index 62a565159..66422f293 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java @@ -140,7 +140,7 @@ public int hashCode() { @Override public String toString() { - return "ColumnDefinition<0x" + Integer.toHexString(bitmap) + ", 0x" + Integer.toHexString(collationId)+ '>'; + return "ColumnDefinition<0x" + Integer.toHexString(bitmap) + ", 0x" + Integer.toHexString(collationId) + '>'; } /** diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java index c980207cf..2fb0e431e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java @@ -25,8 +25,8 @@ public abstract class MySqlBatch implements Batch { @Override - abstract public MySqlBatch add(String sql); + public abstract MySqlBatch add(String sql); @Override - abstract public Flux execute(); + public abstract Flux execute(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java index 2bf3a968c..893c519a2 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java @@ -45,7 +45,6 @@ import java.time.DateTimeException; import java.time.Duration; import java.time.ZoneId; -import java.time.ZoneOffset; import java.util.List; import java.util.function.BiConsumer; import java.util.function.Function; @@ -656,7 +655,7 @@ private static String transactionIsolationColumn(ConnectionContext context) { "@@transaction_isolation AS i" : "@@tx_isolation AS i"; } - private static class SessionData { + private static final class SessionData { private final IsolationLevel level; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java index 749086572..4fd4b5196 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java @@ -226,11 +226,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() { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java index 62ecede60..f75a913f1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/OptionMapper.java @@ -56,7 +56,9 @@ final class Source { @Nullable private final T value; - private Source(@Nullable T value) { this.value = value; } + private Source(@Nullable T value) { + this.value = value; + } Otherwise to(Consumer consumer) { if (value == null) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java index 98ce359e4..fc67087a0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java @@ -121,7 +121,7 @@ public final Flux execute() { }); } - abstract protected Flux execute(List bindings); + protected abstract Flux execute(List bindings); /** * Get parameter index(es) by parameter name. diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 5efe0be34..cec6a9876 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -349,9 +349,9 @@ public final void accept(ServerMessage message, SynchronousSink s } } - abstract protected void tryNextOrComplete(@Nullable SynchronousSink sink); + protected abstract void tryNextOrComplete(@Nullable SynchronousSink sink); - abstract protected String offendingSql(); + protected abstract String offendingSql(); } final class SimpleQueryExchangeable extends BaseFluxExchangeable { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java index 519a56ebe..6ffa079e0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ServerVersion.java @@ -41,7 +41,7 @@ public final class ServerVersion implements Comparable { * Unresolved/origin version pattern, do NOT use it on {@link #hashCode()}, {@link #equals(Object)} or * {@link #compareTo(ServerVersion)}. */ - private transient final String origin; + private final transient String origin; private final int major; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java index cd5b412a3..92fa6bb83 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/authentication/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.authentication; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java index 8fa9d38e8..27d5806fc 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/Caches.java @@ -21,6 +21,9 @@ */ public final class Caches { + private Caches() { + } + /** * Create a new {@link QueryCache} by cache configuration. * diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java index 0a103f872..f695c24cd 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/cache/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.cache; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java index 6ba3f2844..9d5166132 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java @@ -196,7 +196,9 @@ private static final class MySqlSslContextSpec implements SslProvider.ProtocolSs private final SslContextBuilder builder; - private MySqlSslContextSpec(SslContextBuilder builder) { this.builder = builder; } + private MySqlSslContextSpec(SslContextBuilder builder) { + this.builder = builder; + } @Override public MySqlSslContextSpec configure(Consumer customizer) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java index e6016d09e..8ecf67881 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.client; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java index 8adc58f02..ac74b983b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractLobMySqlParameter.java @@ -52,5 +52,5 @@ public final void dispose() { } @Nullable - abstract protected Publisher getDiscard(); + protected abstract Publisher getDiscard(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java index de31cdfac..0b1be61f4 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java @@ -140,7 +140,7 @@ private static BigInteger decimalBigInteger(ByteBuf buf) { return new BigDecimal(buf.toString(StandardCharsets.US_ASCII)).toBigInteger(); } - private static class BigIntegerMySqlParameter extends AbstractMySqlParameter { + private static final class BigIntegerMySqlParameter extends AbstractMySqlParameter { private final BigInteger value; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index f2c479e52..adec25731 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java @@ -48,7 +48,7 @@ public boolean canEncode(Object value) { @Override public MySqlParameter encode(Object value, CodecContext context) { - return (Boolean) value? BooleanMySqlParameter.TRUE : BooleanMySqlParameter.FALSE; + return (Boolean) value ? BooleanMySqlParameter.TRUE : BooleanMySqlParameter.FALSE; } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java index ba204c348..9d69a0c57 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java @@ -81,7 +81,7 @@ public MySqlParameter encode(Object value, CodecContext context) { return new ClobMySqlParameter((Clob) value, context); } - private static class ClobMySqlParameter extends AbstractLobMySqlParameter { + private static final class ClobMySqlParameter extends AbstractLobMySqlParameter { private final AtomicReference clob; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index 9bdbe5787..9c95d7161 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -22,7 +22,6 @@ import io.asyncer.r2dbc.mysql.message.FieldValue; import io.asyncer.r2dbc.mysql.message.LargeFieldValue; import io.asyncer.r2dbc.mysql.message.NormalFieldValue; -import io.netty.buffer.ByteBufAllocator; import io.r2dbc.spi.Parameter; import org.jetbrains.annotations.Nullable; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java index 134dc9159..cb5a661bb 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java @@ -84,7 +84,7 @@ static ByteBuf encodeCharSequence(ByteBufAllocator allocator, CharSequence value } } - private static class StringMySqlParameter extends AbstractMySqlParameter { + private static final class StringMySqlParameter extends AbstractMySqlParameter { private final CharSequence value; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java index b89762436..4e233c883 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/lob/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.codec.lob; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java index 144e8c1fc..6c403ec53 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/package-info.java @@ -25,4 +25,4 @@ * discouraged, as it may lead to unpredictable behavior and compatibility issues. */ @NotNullByDefault -package io.asyncer.r2dbc.mysql.internal; \ No newline at end of file +package io.asyncer.r2dbc.mysql.internal; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java index eb926fe41..20e71ae95 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/InitDbMessage.java @@ -1,3 +1,19 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package io.asyncer.r2dbc.mysql.message.client; import io.asyncer.r2dbc.mysql.ConnectionContext; @@ -9,7 +25,9 @@ public final class InitDbMessage extends ScalarClientMessage { private final String database; - public InitDbMessage(String database) { this.database = database; } + public InitDbMessage(String database) { + this.database = database; + } @Override protected void writeTo(ByteBuf buf, ConnectionContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java index 7f4598013..e62906101 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ScalarClientMessage.java @@ -28,7 +28,7 @@ */ abstract class ScalarClientMessage implements ClientMessage { - abstract protected void writeTo(ByteBuf buf, ConnectionContext context); + protected abstract void writeTo(ByteBuf buf, ConnectionContext context); @Override public Mono encode(ByteBufAllocator allocator, ConnectionContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java index 4b53ddbed..7e6062924 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/SizedClientMessage.java @@ -29,9 +29,9 @@ */ abstract class SizedClientMessage implements ClientMessage { - abstract protected int size(); + protected abstract int size(); - abstract protected void writeTo(ByteBuf buf); + protected abstract void writeTo(ByteBuf buf); @Override public Mono encode(ByteBufAllocator allocator, ConnectionContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java index d2611e3b5..4248ee533 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/package-info.java @@ -22,4 +22,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql.message; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java index 978ee8411..435748f87 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/MetadataDecodeContext.java @@ -81,7 +81,7 @@ final SyntheticMetadataMessage putPart(ServerMessage message) { } @Nullable - abstract protected SyntheticMetadataMessage checkComplete(int index, @Nullable EofMessage eof); + protected abstract SyntheticMetadataMessage checkComplete(int index, @Nullable EofMessage eof); /** * Put a column metadata message into this context. @@ -89,19 +89,19 @@ final SyntheticMetadataMessage putPart(ServerMessage message) { * @param metadata the column metadata message. * @return current index after putting the metadata. */ - abstract protected int putMetadata(DefinitionMetadataMessage metadata); + protected abstract int putMetadata(DefinitionMetadataMessage metadata); /** * Get the current index, for {@link #checkComplete(int, EofMessage)} when receive a EOF message. * * @return the current index. */ - abstract protected int currentIndex(); + protected abstract int currentIndex(); /** * Get checkpoints for logging. * * @return serializable object, like {@link String} or {@link Integer}. */ - abstract protected Object loggingPoints(); + protected abstract Object loggingPoints(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java index b0dc5f8d9..4803f5428 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/package-info.java @@ -21,4 +21,4 @@ @NotNullByDefault package io.asyncer.r2dbc.mysql; -import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; \ No newline at end of file +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/test-native-image/src/main/java/io/asyncer/Main.java b/test-native-image/src/main/java/io/asyncer/Main.java index 4a7931843..1ad7788b3 100644 --- a/test-native-image/src/main/java/io/asyncer/Main.java +++ b/test-native-image/src/main/java/io/asyncer/Main.java @@ -22,7 +22,12 @@ import io.r2dbc.spi.ConnectionFactoryOptions; import reactor.core.publisher.Mono; -public class Main { +public final class Main { + + private Main() { + // Native Image Entry Point + } + public static void main(String[] args) { ConnectionFactory connectionFactory = ConnectionFactories.get(ConnectionFactoryOptions.builder() .option(ConnectionFactoryOptions.DRIVER, "mysql") @@ -40,4 +45,4 @@ public static void main(String[] args) { .doOnNext(System.out::println) .blockLast(); } -} \ No newline at end of file +} diff --git a/test-native-image/src/main/java/io/asyncer/package-info.java b/test-native-image/src/main/java/io/asyncer/package-info.java new file mode 100644 index 000000000..fb9d7aed7 --- /dev/null +++ b/test-native-image/src/main/java/io/asyncer/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Native image build test classes. + */ +@NotNullByDefault +package io.asyncer; + +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; From f8832dcfd2e93b44fa4602b6c2ae5496ab7a2a9b Mon Sep 17 00:00:00 2001 From: jchrys Date: Wed, 28 Feb 2024 12:08:33 +0900 Subject: [PATCH 30/93] Fix Deploy Motivation: Deploy Failure Modification: Prepare internal dependencies before deploy Result: Fix Resolves #249 --- .github/workflows/cd-release.yml | 5 ++++- .github/workflows/cd-snapshot.yml | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 0bef91e4a..1e9034bc0 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -130,9 +130,12 @@ jobs: cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import ./mvnw -B -ntp -am -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" + - name: Prepare Internal Dependencies + run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip + - name: Deploy Local Staged Artifacts working-directory: ./prepare-workspace/ - run: ./mvnw -B -ntp -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true + run: ./mvnw -B -ntp -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true -Dcheckstyle.skip - name: Rollback Release diff --git a/.github/workflows/cd-snapshot.yml b/.github/workflows/cd-snapshot.yml index 5bb7e5341..a04976b28 100644 --- a/.github/workflows/cd-snapshot.yml +++ b/.github/workflows/cd-snapshot.yml @@ -51,8 +51,11 @@ jobs: "password": "${{ secrets.OSSRH_PASSWORD }}" }] + - name: Prepare Internal Dependencies + run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip + - name: Deploy Local Staging - run: ./mvnw -B -ntp -pl r2dbc-mysql clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true + run: ./mvnw -B -ntp -pl r2dbc-mysql clean package org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip=true - name: Deploy Local Staged Artifacts run: ./mvnw -B -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DaltStagingDirectory=/home/runner/local-staging From 3f119eb7a298be582324aec2bad4c1496b0d5016 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 29 Feb 2024 12:43:56 +0900 Subject: [PATCH 31/93] Correct transaction statement building --- .../mysql/MySqlTransactionDefinition.java | 27 ++++--- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 18 ++++- .../mysql/ConnectionIntegrationTest.java | 26 +++++++ .../mysql/StartTransactionStateTest.java | 71 +++++++++++++++++++ 4 files changed, 125 insertions(+), 17 deletions(-) create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java index fab2bcc15..4abc58e36 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java @@ -37,35 +37,34 @@ 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"); /** - * 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"); /** - * 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"); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index cec6a9876..afb23fb55 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -1281,20 +1281,28 @@ protected boolean process(int task, SynchronousSink sink) { return false; } - private static String buildStartTransaction(TransactionDefinition definition) { + /** + * Visible for testing. + * + * @param definition the transaction definition + * @return the {@code START TRANSACTION} statement + */ + static String buildStartTransaction(TransactionDefinition definition) { Boolean readOnly = definition.getAttribute(TransactionDefinition.READ_ONLY); Boolean snapshot = definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT); - if (readOnly == null && (snapshot == null || !snapshot)) { + if (readOnly == null && !Boolean.TRUE.equals(snapshot)) { return "BEGIN"; } StringBuilder builder = new StringBuilder(90).append("START TRANSACTION"); + boolean first = true; - if (snapshot != null && snapshot) { + if (Boolean.TRUE.equals(snapshot)) { ConsistentSnapshotEngine engine = definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE); + first = false; builder.append(" WITH CONSISTENT "); if (engine == null) { @@ -1312,6 +1320,10 @@ private static String buildStartTransaction(TransactionDefinition definition) { } if (readOnly != null) { + if (!first) { + builder.append(','); + } + if (readOnly) { builder.append(" READ ONLY"); } else { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index 48d63c0ed..50c4d5122 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -18,7 +18,9 @@ import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.TransactionDefinition; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException; @@ -71,6 +73,30 @@ void isInTransaction() { .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse())); } + @DisabledIf("envIsLessThanMySql56") + @Test + void startTransaction() { + TransactionDefinition readOnlyConsistent = MySqlTransactionDefinition.builder() + .withConsistentSnapshot(true) + .readOnly(true) + .build(); + TransactionDefinition readWriteConsistent = MySqlTransactionDefinition.builder() + .withConsistentSnapshot(true) + .readOnly(false) + .build(); + + complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) + .isFalse()) + .then(connection.beginTransaction(readOnlyConsistent)) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .then(connection.rollbackTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .then(connection.beginTransaction(readWriteConsistent)) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .then(connection.rollbackTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse())); + } + @Test void autoRollbackPreRelease() { // Mock pool allocate/release. diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java new file mode 100644 index 000000000..14a878d91 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java @@ -0,0 +1,71 @@ +/* + * 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.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.TransactionDefinition; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link StartTransactionState}. + */ +class StartTransactionStateTest { + + @ParameterizedTest + @MethodSource + void buildStartTransaction(TransactionDefinition definition, String excepted) { + assertThat(StartTransactionState.buildStartTransaction(definition)).isEqualTo(excepted); + } + + static Stream buildStartTransaction() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), "BEGIN"), + Arguments.of(MySqlTransactionDefinition.builder() + .isolationLevel(IsolationLevel.READ_UNCOMMITTED) + .build(), "BEGIN"), + Arguments.of(MySqlTransactionDefinition.builder() + .readOnly(true) + .build(), "START TRANSACTION READ ONLY"), + Arguments.of(MySqlTransactionDefinition.builder() + .readOnly(false) + .build(), "START TRANSACTION READ WRITE"), + Arguments.of(MySqlTransactionDefinition.builder() + .withConsistentSnapshot(true) + .build(), "START TRANSACTION WITH CONSISTENT SNAPSHOT"), + Arguments.of(MySqlTransactionDefinition.builder() + .withConsistentSnapshot(true) + .readOnly(true) + .build(), "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY"), + Arguments.of(MySqlTransactionDefinition.builder() + .withConsistentSnapshot(true) + .readOnly(false) + .build(), "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE"), + Arguments.of(MySqlTransactionDefinition.builder() + .withConsistentSnapshot(true) + .consistentSnapshotEngine(ConsistentSnapshotEngine.ROCKSDB) + .consistentSnapshotFromSession(3L) + .readOnly(true) + .build(), "START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT FROM SESSION 3, READ ONLY") + ); + } +} From 09a2246a625e4494b291d590c0eed1ac2330d663 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 29 Feb 2024 12:16:26 +0900 Subject: [PATCH 32/93] Extract public API to api package - Use strongly typed interfaces - Standardize R2DBC driver MySQL-specific API --- .../asyncer/r2dbc/mysql/ColumnDefinition.java | 168 ---------- .../asyncer/r2dbc/mysql/ConnectionState.java | 2 +- .../r2dbc/mysql/ConsistentSnapshotEngine.java | 7 +- .../r2dbc/mysql/InsertSyntheticRow.java | 34 +- .../r2dbc/mysql/MySqlBatchingBatch.java | 6 +- .../r2dbc/mysql/MySqlColumnDescriptor.java | 29 +- .../r2dbc/mysql/MySqlConnectionFactory.java | 9 +- .../{MySqlRow.java => MySqlDataRow.java} | 34 +- ...wMetadata.java => MySqlRowDescriptor.java} | 14 +- ...SqlResult.java => MySqlSegmentResult.java} | 34 +- ...ection.java => MySqlSimpleConnection.java} | 27 +- ...ava => MySqlSimpleConnectionMetadata.java} | 14 +- .../asyncer/r2dbc/mysql/MySqlStatement.java | 83 ----- .../r2dbc/mysql/MySqlStatementSupport.java | 1 + .../r2dbc/mysql/MySqlSyntheticBatch.java | 6 +- .../mysql/MySqlTransactionDefinition.java | 15 +- .../r2dbc/mysql/MySqlTypeMetadata.java | 106 ++++-- .../mysql/ParametrizedStatementSupport.java | 6 +- .../io/asyncer/r2dbc/mysql/PingStatement.java | 16 +- .../mysql/PrepareParametrizedStatement.java | 4 +- .../r2dbc/mysql/PrepareSimpleStatement.java | 4 +- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 9 +- .../r2dbc/mysql/SimpleStatementSupport.java | 1 + .../mysql/TextParametrizedStatement.java | 3 +- .../r2dbc/mysql/TextSimpleStatement.java | 3 +- .../asyncer/r2dbc/mysql/api/MySqlBatch.java | 46 +++ .../MySqlColumnMetadata.java} | 19 +- .../r2dbc/mysql/api/MySqlConnection.java | 203 +++++++++++ .../mysql/api/MySqlConnectionMetadata.java | 52 +++ .../mysql/api/MySqlNativeTypeMetadata.java | 74 ++++ .../mysql/api/MySqlOutParameterMetadata.java | 27 ++ .../r2dbc/mysql/api/MySqlOutParameters.java | 35 ++ .../mysql/api/MySqlOutParametersMetadata.java | 59 ++++ .../r2dbc/mysql/api/MySqlReadable.java | 30 ++ .../MySqlReadableMetadata.java} | 42 ++- .../asyncer/r2dbc/mysql/api/MySqlResult.java | 173 ++++++++++ .../io/asyncer/r2dbc/mysql/api/MySqlRow.java | 69 ++++ .../r2dbc/mysql/api/MySqlRowMetadata.java | 70 ++++ .../r2dbc/mysql/api/MySqlStatement.java | 129 +++++++ .../mysql/api/MySqlTransactionDefinition.java | 194 +++++++++++ .../api/SimpleTransactionDefinition.java | 218 ++++++++++++ .../asyncer/r2dbc/mysql/api/package-info.java | 24 ++ .../mysql/codec/AbstractClassedCodec.java | 6 +- .../mysql/codec/AbstractPrimitiveCodec.java | 4 +- .../r2dbc/mysql/codec/BigDecimalCodec.java | 6 +- .../r2dbc/mysql/codec/BigIntegerCodec.java | 6 +- .../r2dbc/mysql/codec/BitSetCodec.java | 6 +- .../asyncer/r2dbc/mysql/codec/BlobCodec.java | 8 +- .../r2dbc/mysql/codec/BooleanCodec.java | 9 +- .../r2dbc/mysql/codec/ByteArrayCodec.java | 6 +- .../r2dbc/mysql/codec/ByteBufferCodec.java | 6 +- .../asyncer/r2dbc/mysql/codec/ByteCodec.java | 6 +- .../asyncer/r2dbc/mysql/codec/ClobCodec.java | 8 +- .../io/asyncer/r2dbc/mysql/codec/Codec.java | 10 +- .../io/asyncer/r2dbc/mysql/codec/Codecs.java | 10 +- .../r2dbc/mysql/codec/DefaultCodecs.java | 18 +- .../r2dbc/mysql/codec/DoubleCodec.java | 6 +- .../r2dbc/mysql/codec/DurationCodec.java | 6 +- .../asyncer/r2dbc/mysql/codec/EnumCodec.java | 6 +- .../asyncer/r2dbc/mysql/codec/FloatCodec.java | 6 +- .../r2dbc/mysql/codec/InstantCodec.java | 6 +- .../r2dbc/mysql/codec/IntegerCodec.java | 6 +- .../r2dbc/mysql/codec/LocalDateCodec.java | 6 +- .../r2dbc/mysql/codec/LocalDateTimeCodec.java | 10 +- .../r2dbc/mysql/codec/LocalTimeCodec.java | 6 +- .../asyncer/r2dbc/mysql/codec/LongCodec.java | 6 +- .../r2dbc/mysql/codec/MassiveCodec.java | 6 +- .../mysql/codec/MassiveParametrizedCodec.java | 9 +- .../mysql/codec/OffsetDateTimeCodec.java | 6 +- .../r2dbc/mysql/codec/OffsetTimeCodec.java | 6 +- .../r2dbc/mysql/codec/ParametrizedCodec.java | 10 +- .../r2dbc/mysql/codec/PrimitiveCodec.java | 10 +- .../asyncer/r2dbc/mysql/codec/SetCodec.java | 10 +- .../asyncer/r2dbc/mysql/codec/ShortCodec.java | 6 +- .../r2dbc/mysql/codec/StringCodec.java | 6 +- .../asyncer/r2dbc/mysql/codec/YearCodec.java | 6 +- .../r2dbc/mysql/codec/ZonedDateTimeCodec.java | 10 +- .../r2dbc/mysql/constant/MySqlType.java | 30 +- .../server/DefinitionMetadataMessage.java | 28 +- .../mysql/message/server/RowMessage.java | 6 +- .../r2dbc/mysql/ColumnDefinitionTest.java | 61 ---- .../mysql/ConnectionIntegrationTest.java | 315 +++++++++--------- .../r2dbc/mysql/InitDbIntegrationTest.java | 1 + .../r2dbc/mysql/IntegrationTestSupport.java | 3 +- .../mysql/JacksonIntegrationTestSupport.java | 7 +- .../mysql/MariaDbIntegrationTestSupport.java | 1 + ...st.java => MySqlSimpleConnectionTest.java} | 12 +- .../mysql/MySqlTransactionDefinitionTest.java | 80 ----- .../r2dbc/mysql/MySqlTypeMetadataTest.java | 60 ++++ .../mysql/QueryIntegrationTestSupport.java | 12 +- .../r2dbc/mysql/SslTunnelIntegrationTest.java | 1 + .../mysql/StartTransactionStateTest.java | 49 ++- .../r2dbc/mysql/StatementTestSupport.java | 1 + .../r2dbc/mysql/TimeZoneIntegrationTest.java | 1 + .../api/MySqlTransactionDefinitionTest.java | 172 ++++++++++ .../asyncer/r2dbc/mysql/codec/CodecsTest.java | 13 +- .../asyncer/r2dbc/mysql/codec/Decoding.java | 12 +- .../r2dbc/mysql/json/JacksonCodec.java | 13 +- 98 files changed, 2295 insertions(+), 940 deletions(-) delete mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlRow.java => MySqlDataRow.java} (68%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlRowMetadata.java => MySqlRowDescriptor.java} (88%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlResult.java => MySqlSegmentResult.java} (89%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlConnection.java => MySqlSimpleConnection.java} (95%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlConnectionMetadata.java => MySqlSimpleConnectionMetadata.java} (76%) delete mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlBatch.java rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlBatch.java => api/MySqlColumnMetadata.java} (59%) create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnection.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnectionMetadata.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlNativeTypeMetadata.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameterMetadata.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParameters.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlOutParametersMetadata.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlReadable.java rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlColumnMetadata.java => api/MySqlReadableMetadata.java} (61%) create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlResult.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRow.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlRowMetadata.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinition.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/SimpleTransactionDefinition.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/package-info.java delete mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java rename r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/{MySqlConnectionTest.java => MySqlSimpleConnectionTest.java} (91%) delete mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadataTest.java create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinitionTest.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java deleted file mode 100644 index 66422f293..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnDefinition.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.collation.CharCollation; - -/** - * A flag bitmap considers column definitions. - */ -public final class ColumnDefinition { - - private static final short NOT_NULL = 1; - -// public static final short PRIMARY_PART = 1 << 1; // This field is a part of the primary key -// public static final short UNIQUE_PART = 1 << 2; // This field is a part of a unique key -// public static final short KEY_PART = 1 << 3; // This field is a part of a normal key -// public static final short BLOB = 1 << 4; - - private static final short UNSIGNED = 1 << 5; - -// public static final short ZEROFILL = 1 << 6; - - public static final short BINARY = 1 << 7; - - private static final short ENUM = 1 << 8; - -// public static final short AUTO_INCREMENT = 1 << 9; -// public static final short TIMESTAMP = 1 << 10; - - private static final short SET = 1 << 11; // type is set - -// public static final short NO_DEFAULT = 1 << 12; // column has no default value -// public static final short ON_UPDATE_NOW = 1 << 13; // field will be set to NOW() in UPDATE statement - - private static final short ALL_USED = NOT_NULL | UNSIGNED | BINARY | ENUM | SET; - - /** - * The original bitmap of {@link ColumnDefinition this}. - *

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

- * collationId > 0 when protocol version == 4.1, 0 otherwise. - */ - private final int collationId; - - private ColumnDefinition(short bitmap, int collationId) { - this.bitmap = bitmap; - this.collationId = collationId; - } - - /** - * Checks if value is not null. - * - * @return if value is not null. - */ - public boolean isNotNull() { - return (bitmap & 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 support it in MySQL 8.0+. When creating a - * column as an unsigned floating type, the server may report a warning. - * - * @return if value is an unsigned number. - */ - public boolean isUnsigned() { - return (bitmap & UNSIGNED) != 0; - } - - /** - * Checks if value is binary data. - * - * @return if value is binary data. - */ - 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; - } - - /** - * Checks if value type is enum. - * - * @return if value is an enum. - */ - public boolean isEnum() { - return (bitmap & ENUM) != 0; - } - - /** - * Checks if value type is set. - * - * @return if value is a set. - */ - public boolean isSet() { - return (bitmap & SET) != 0; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (!(o instanceof ColumnDefinition)) { - return false; - } - - ColumnDefinition that = (ColumnDefinition) o; - - return bitmap == that.bitmap & collationId == that.collationId; - } - - @Override - public int hashCode() { - return bitmap; - } - - @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); - } -} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java index 33f8cf551..73a9caf09 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java @@ -31,7 +31,7 @@ interface ConnectionState { void setIsolationLevel(IsolationLevel level); /** - * Reutrns session lock wait timeout. + * Returns session lock wait timeout. * * @return Session lock wait timeout. */ diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConsistentSnapshotEngine.java index 716b65a37..a501d1887 100644 --- a/r2dbc-mysql/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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java index eab13e88c..a9865419c 100644 --- a/r2dbc-mysql/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; @@ -96,19 +103,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 +123,7 @@ public ColumnMetadata getColumnMetadata(String name) { } @Override - public List getColumnMetadatas() { + public List getColumnMetadatas() { return Collections.singletonList(this); } @@ -125,6 +132,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,6 +152,18 @@ public Nullability getNullability() { return Nullability.NON_NULL; } + @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)); + } + + @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 void assertValidName(String name) { if (!contains0(name)) { throw new NoSuchElementException("Column name '" + name + "' does not exist in " + this.nameSet); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java index 6d74cf4d0..d85ebfd9e 100644 --- a/r2dbc-mysql/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,7 +30,7 @@ * 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; @@ -63,7 +65,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, codecs, context, null, messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java index 4bc9aaca4..5f3720c08 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java @@ -16,6 +16,8 @@ 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; @@ -48,28 +50,28 @@ final class MySqlColumnDescriptor implements MySqlColumnMetadata { private final int collationId; - private MySqlColumnDescriptor(int index, short typeId, String name, ColumnDefinition definition, + private 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 +90,7 @@ public String getName() { } @Override - public MySqlTypeMetadata getNativeTypeMetadata() { + public MySqlNativeTypeMetadata getNativeTypeMetadata() { return typeMetadata; } @@ -99,14 +101,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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index ec2d57339..9e269eda5 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -16,6 +16,7 @@ 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.PrepareCache; import io.asyncer.r2dbc.mysql.cache.QueryCache; @@ -52,14 +53,14 @@ */ public final class MySqlConnectionFactory implements ConnectionFactory { - private final Mono client; + private final Mono client; - private MySqlConnectionFactory(Mono client) { + private MySqlConnectionFactory(Mono client) { this.client = client; } @Override - public Mono create() { + public Mono create() { return client; } @@ -174,7 +175,7 @@ private static Mono getMySqlConnection( extensions.forEach(CodecRegistrar.class, registrar -> registrar.register(allocator, builder)); - return MySqlConnection.init(client, builder.build(), context, db, queryCache.get(), + return MySqlSimpleConnection.init(client, builder.build(), context, db, queryCache.get(), prepareCache, sessionVariables, prepare); }); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java similarity index 68% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java index 04cc12eff..a9e56b747 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java @@ -16,11 +16,11 @@ 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.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 +29,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; @@ -44,7 +44,7 @@ public final class MySqlRow implements Row { private final ConnectionContext context; - MySqlRow(FieldValue[] fields, MySqlRowMetadata rowMetadata, Codecs codecs, boolean binary, + MySqlDataRow(FieldValue[] fields, MySqlRowDescriptor rowMetadata, Codecs codecs, boolean binary, ConnectionContext context) { this.fields = requireNonNull(fields, "fields must not be null"); this.rowMetadata = requireNonNull(rowMetadata, "rowMetadata must not be null"); @@ -69,16 +69,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 +77,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 +89,7 @@ public T get(String name, ParameterizedType type) { * {@inheritDoc} */ @Override - public RowMetadata getMetadata() { + public MySqlRowMetadata getMetadata() { return rowMetadata; } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java similarity index 88% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java index 37f26ac7b..1b5311da6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowMetadata.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java @@ -16,9 +16,9 @@ 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 java.util.Arrays; import java.util.List; @@ -27,11 +27,11 @@ import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** - * An implementation of {@link RowMetadata} for MySQL database text/binary results. + * An implementation of {@link MySqlRowMetadata} for MySQL database text/binary results. * * @see MySqlNames column name searching rules. */ -final class MySqlRowMetadata implements RowMetadata { +final class MySqlRowDescriptor implements MySqlRowMetadata { private final MySqlColumnDescriptor[] originMetadata; @@ -39,7 +39,7 @@ final class MySqlRowMetadata implements RowMetadata { private final ColumnNameSet nameSet; - private MySqlRowMetadata(MySqlColumnDescriptor[] metadata) { + private MySqlRowDescriptor(MySqlColumnDescriptor[] metadata) { int size = metadata.length; switch (size) { @@ -105,7 +105,7 @@ public List getColumnMetadatas() { @Override public String toString() { - return "MySqlRowMetadata{metadata=" + Arrays.toString(originMetadata) + ", sortedNames=" + + return "MySqlRowDescriptor{metadata=" + Arrays.toString(originMetadata) + ", sortedNames=" + Arrays.toString(nameSet.getSortedNames()) + '}'; } @@ -113,7 +113,7 @@ MySqlColumnDescriptor[] unwrap() { return originMetadata; } - static MySqlRowMetadata create(DefinitionMetadataMessage[] columns) { + static MySqlRowDescriptor create(DefinitionMetadataMessage[] columns) { int size = columns.length; MySqlColumnDescriptor[] metadata = new MySqlColumnDescriptor[size]; @@ -121,7 +121,7 @@ static MySqlRowMetadata create(DefinitionMetadataMessage[] columns) { metadata[i] = MySqlColumnDescriptor.create(i, columns[i]); } - return new MySqlRowMetadata(metadata); + return new MySqlRowDescriptor(metadata); } private static String[] getNames(MySqlColumnDescriptor[] metadata) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java similarity index 89% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java index 4fd4b5196..d38f6cd91 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlResult.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java @@ -16,6 +16,8 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlRow; 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 +51,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}. */ -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 +83,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 +118,10 @@ public Flux map(Function f) { } @Override - public MySqlResult filter(Predicate filter) { + public MySqlResult filter(Predicate filter) { requireNonNull(filter, "filter must not be null"); - return new MySqlResult(segments.filter(segment -> { + return new MySqlSegmentResult(segments.filter(segment -> { if (filter.test(segment)) { return true; } @@ -133,7 +135,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 -> { @@ -160,7 +162,7 @@ static MySqlResult toResult(boolean binary, Codecs codecs, ConnectionContext con 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))); } @@ -200,14 +202,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; } @@ -258,7 +260,7 @@ private MySqlOkSegment(long rows, long lastInsertId, Codecs codecs, String keyNa } @Override - public Row row() { + public MySqlRow row() { return new InsertSyntheticRow(codecs, keyName, lastInsertId); } } @@ -276,7 +278,7 @@ 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; } @@ -316,7 +318,7 @@ 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; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java similarity index 95% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java index 893c519a2..f4a2b3746 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java @@ -16,6 +16,12 @@ 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.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; import io.asyncer.r2dbc.mysql.cache.PrepareCache; import io.asyncer.r2dbc.mysql.cache.QueryCache; import io.asyncer.r2dbc.mysql.client.Client; @@ -30,9 +36,7 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import io.r2dbc.spi.Connection; import io.r2dbc.spi.IsolationLevel; -import io.r2dbc.spi.Lifecycle; import io.r2dbc.spi.R2dbcNonTransientResourceException; import io.r2dbc.spi.Readable; import io.r2dbc.spi.TransactionDefinition; @@ -54,11 +58,11 @@ import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; /** - * An implementation of {@link Connection} for connecting to the MySQL database. + * An implementation of {@link MySqlConnection} for connecting to the MySQL database. */ -public final class MySqlConnection implements Connection, Lifecycle, ConnectionState { +final class MySqlSimpleConnection implements MySqlConnection, ConnectionState { - private static final InternalLogger logger = InternalLoggerFactory.getInstance(MySqlConnection.class); + private static final InternalLogger logger = InternalLoggerFactory.getInstance(MySqlSimpleConnection.class); private static final int DEFAULT_LOCK_WAIT_TIMEOUT = 50; @@ -170,7 +174,7 @@ public final class MySqlConnection implements Connection, Lifecycle, ConnectionS */ private volatile long currentLockWaitTimeout; - MySqlConnection(Client client, ConnectionContext context, Codecs codecs, IsolationLevel level, + MySqlSimpleConnection(Client client, ConnectionContext context, Codecs codecs, IsolationLevel level, long lockWaitTimeout, QueryCache queryCache, PrepareCache prepareCache, @Nullable String product, @Nullable Predicate prepare) { this.client = client; @@ -182,7 +186,8 @@ public final class MySqlConnection implements Connection, Lifecycle, ConnectionS this.currentLockWaitTimeout = lockWaitTimeout; this.queryCache = queryCache; this.prepareCache = prepareCache; - this.metadata = new MySqlConnectionMetadata(context.getServerVersion().toString(), product); + this.metadata = new MySqlSimpleConnectionMetadata(context.getServerVersion().toString(), product, + context.isMariaDb()); this.batchSupported = context.getCapability().isMultiStatementsAllowed(); this.prepare = prepare; @@ -239,7 +244,7 @@ public MySqlStatement createStatement(String sql) { requireNonNull(sql, "sql must not be null"); if (sql.startsWith(PING_MARKER)) { - return new PingStatement(this, codecs, context); + return new PingStatement(codecs, context, Flux.defer(this::doPingInternal)); } Query query = queryCache.get(sql); @@ -451,11 +456,11 @@ public Mono setStatementTimeout(Duration timeout) { ); } - Flux doPingInternal() { + private Flux doPingInternal() { return client.exchange(PingMessage.INSTANCE, PING); } - boolean isSessionAutoCommit() { + private boolean isSessionAutoCommit() { return (context.getServerStatuses() & ServerStatuses.AUTO_COMMIT) != 0; } @@ -486,7 +491,7 @@ static Mono init( context.setTimeZone(timeZone); } - return new MySqlConnection(client, context, codecs, data.level, data.lockWaitTimeout, + return new MySqlSimpleConnection(client, context, codecs, data.level, data.lockWaitTimeout, queryCache, prepareCache, data.product, prepare); }); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionMetadata.java similarity index 76% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionMetadata.java index af40495f5..ee7faf42d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionMetadata.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionMetadata.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql; -import io.r2dbc.spi.ConnectionMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlConnectionMetadata; import org.jetbrains.annotations.Nullable; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; @@ -24,15 +24,18 @@ /** * Connection metadata for a connection connected to MySQL database. */ -public final class MySqlConnectionMetadata implements ConnectionMetadata { +final class MySqlSimpleConnectionMetadata implements MySqlConnectionMetadata { private final String version; private final String product; - MySqlConnectionMetadata(String version, @Nullable String product) { + private final boolean isMariaDb; + + MySqlSimpleConnectionMetadata(String version, @Nullable String product, boolean isMariaDb) { this.version = requireNonNull(version, "version must not be null"); this.product = product == null ? "Unknown" : product; + this.isMariaDb = isMariaDb; } @Override @@ -40,6 +43,11 @@ public String getDatabaseVersion() { return version; } + @Override + public boolean isMariaDb() { + return isMariaDb; + } + @Override public String getDatabaseProductName() { return product; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java deleted file mode 100644 index a228681f1..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatement.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.r2dbc.spi.Statement; -import reactor.core.publisher.Flux; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * A strongly typed implementation of {@link Statement} for the MySQL database. - */ -public interface MySqlStatement extends Statement { - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement add(); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bind(int index, Object value); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bind(String name, Object value); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bindNull(int index, Class type); - - /** - * {@inheritDoc} - */ - @Override - MySqlStatement bindNull(String name, Class type); - - /** - * {@inheritDoc} - */ - @Override - Flux execute(); - - /** - * {@inheritDoc} - */ - @Override - default MySqlStatement returnGeneratedValues(String... columns) { - requireNonNull(columns, "columns must not be null"); - return this; - } - - /** - * {@inheritDoc} - */ - @Override - default MySqlStatement fetchSize(int rows) { - require(rows >= 0, "Fetch size must be greater or equal to zero"); - return this; - } -} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java index 696626ba0..d976b6155 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import org.jetbrains.annotations.Nullable; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java index 87325591e..efc677beb 100644 --- a/r2dbc-mysql/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,7 +31,7 @@ * 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; @@ -54,7 +56,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, codecs, context, null, messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java index 4abc58e36..f5c9af4ed 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinition.java @@ -33,7 +33,9 @@ * 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 { /** @@ -43,7 +45,8 @@ public final class MySqlTransactionDefinition implements TransactionDefinition { * same as if a {@code START TRANSACTION} followed by a {@code SELECT ...} from any InnoDB table was * issued. */ - 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 WITH CONSISTENT [engine] SNAPSHOT} for Facebook/MySQL or similar property. Only available @@ -52,8 +55,8 @@ public final class MySqlTransactionDefinition implements TransactionDefinition { * 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 WITH CONSISTENT SNAPSHOT FROM SESSION [session_id]} for Percona/MySQL or similar property. @@ -67,7 +70,7 @@ public final class MySqlTransactionDefinition implements TransactionDefinition { * 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()); @@ -186,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()); } /** @@ -199,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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java index 4ef11772c..9217367fa 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadata.java @@ -16,36 +16,94 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlNativeTypeMetadata; +import io.asyncer.r2dbc.mysql.collation.CharCollation; + /** - * A metadata descriptor considers MySQL types. + * An implementation of {@link MySqlNativeTypeMetadata}. */ -public final class MySqlTypeMetadata { +final class MySqlTypeMetadata implements MySqlNativeTypeMetadata { - private final int id; + private static final short NOT_NULL = 1; - private final ColumnDefinition definition; +// public static final short PRIMARY_PART = 1 << 1; // This field is a part of the primary key +// public static final short UNIQUE_PART = 1 << 2; // This field is a part of a unique key +// public static final short KEY_PART = 1 << 3; // This field is a part of a normal key +// public static final short BLOB = 1 << 4; - MySqlTypeMetadata(int id, ColumnDefinition definition) { - this.id = id; - this.definition = definition; - } + private static final short UNSIGNED = 1 << 5; + +// public static final short ZEROFILL = 1 << 6; + + public static final short BINARY = 1 << 7; + + private static final short ENUM = 1 << 8; + +// public static final short AUTO_INCREMENT = 1 << 9; +// public static final short TIMESTAMP = 1 << 10; + + private static final short SET = 1 << 11; // type is set + +// public static final short NO_DEFAULT = 1 << 12; // column has no default value +// public static final short ON_UPDATE_NOW = 1 << 13; // field will be set to NOW() in UPDATE statement + + private static final short ALL_USED = NOT_NULL | UNSIGNED | BINARY | ENUM | SET; + + private final int typeId; /** - * Get the native type identifier. - * - * @return the native type identifier. + * The original bitmap of definitions. + *

+ * MySQL uses 32-bits definition flags, but only returns the lower 16-bits. */ - public int getId() { - return id; - } + private final short definitions; /** - * Get the {@link ColumnDefinition} that potentially exposes more type differences. - * - * @return the column definitions. + * The character collation id of the column. + *

+ * collationId > 0 when protocol version == 4.1, 0 otherwise. */ - public ColumnDefinition getDefinition() { - return definition; + private final int collationId; + + MySqlTypeMetadata(int typeId, int definitions, int collationId) { + this.typeId = typeId; + this.definitions = (short) (definitions & ALL_USED); + this.collationId = collationId; + } + + @Override + public int getTypeId() { + return typeId; + } + + @Override + public boolean isNotNull() { + return (definitions & NOT_NULL) != 0; + } + + @Override + public boolean isUnsigned() { + return (definitions & UNSIGNED) != 0; + } + + @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 + // 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; + } + + @Override + public boolean isEnum() { + return (definitions & ENUM) != 0; + } + + @Override + public boolean isSet() { + return (definitions & SET) != 0; } @Override @@ -59,16 +117,20 @@ public boolean equals(Object o) { MySqlTypeMetadata that = (MySqlTypeMetadata) o; - return id == that.id && definition.equals(that.definition); + return typeId == that.typeId && definitions == that.definitions && collationId == that.collationId; } @Override public int hashCode() { - return 31 * id + definition.hashCode(); + int result = 31 * typeId + (int) definitions; + return 31 * result + collationId; } @Override public String toString() { - return "MySqlTypeMetadata(" + id + ", " + definition + ')'; + return "MySqlTypeMetadata{typeId=" + typeId + + ", definitions=0x" + Integer.toHexString(definitions) + + ", collationId=" + collationId + + '}'; } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java index fc67087a0..41ea8e465 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.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; @@ -106,7 +108,7 @@ public final MySqlStatement bindNull(String name, Class type) { } @Override - public final Flux execute() { + public final Flux execute() { if (bindings.bindings.isEmpty()) { throw new IllegalStateException("No parameters bound for current statement"); } @@ -121,7 +123,7 @@ public final Flux execute() { }); } - protected abstract Flux execute(List bindings); + protected abstract Flux execute(List bindings); /** * Get parameter index(es) by parameter name. diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java index d11717a34..f8b1936e2 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java @@ -16,7 +16,10 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.codec.Codecs; +import io.asyncer.r2dbc.mysql.message.server.ServerMessage; import reactor.core.publisher.Flux; /** @@ -24,16 +27,16 @@ */ final class PingStatement implements MySqlStatement { - private final MySqlConnection connection; - private final Codecs codecs; private final ConnectionContext context; - PingStatement(MySqlConnection connection, Codecs codecs, ConnectionContext context) { - this.connection = connection; + private final Flux deferred; + + PingStatement(Codecs codecs, ConnectionContext context, Flux deferred) { this.codecs = codecs; this.context = context; + this.deferred = deferred; } @Override @@ -63,7 +66,8 @@ public MySqlStatement bindNull(String name, Class type) { @Override public Flux execute() { - return Flux.just(MySqlResult.toResult(false, codecs, context, null, - connection.doPingInternal())); + return Flux.just( + MySqlSegmentResult.toResult(false, codecs, context, null, deferred) + ); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java index 3a946f3ea..9395a1309 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.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.cache.PrepareCache; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; @@ -47,7 +49,7 @@ public Flux execute(List bindings) { StringUtils.extendReturning(query.getFormattedSql(), returningIdentifiers()), bindings, fetchSize, prepareCache )) - .map(messages -> MySqlResult.toResult(true, codecs, context, syntheticKeyName(), messages)); + .map(messages -> MySqlSegmentResult.toResult(true, codecs, context, syntheticKeyName(), messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java index 2284b991e..d037eda39 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.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.cache.PrepareCache; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; @@ -48,7 +50,7 @@ final class PrepareSimpleStatement extends SimpleStatementSupport { 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)); + .map(messages -> MySqlSegmentResult.toResult(true, codecs, context, syntheticKeyName(), messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index afb23fb55..7b100cd24 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -16,6 +16,8 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlBatch; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; import io.asyncer.r2dbc.mysql.authentication.MySqlAuthProvider; import io.asyncer.r2dbc.mysql.cache.PrepareCache; import io.asyncer.r2dbc.mysql.client.Client; @@ -1299,8 +1301,9 @@ static String buildStartTransaction(TransactionDefinition definition) { boolean first = true; if (Boolean.TRUE.equals(snapshot)) { - ConsistentSnapshotEngine engine = - definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE); + // Compatible for enum ConsistentSnapshotEngine. + Object eng = definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE); + String engine = eng == null ? null : eng.toString(); first = false; builder.append(" WITH CONSISTENT "); @@ -1308,7 +1311,7 @@ static String buildStartTransaction(TransactionDefinition definition) { if (engine == null) { builder.append("SNAPSHOT"); } else { - builder.append(engine.asSql()).append(" SNAPSHOT"); + builder.append(engine).append(" SNAPSHOT"); } Long sessionId = diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java index 56b34a926..42ba279e3 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java index e0fd475c6..88a10d1a1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import reactor.core.publisher.Flux; @@ -35,6 +36,6 @@ final class TextParametrizedStatement extends ParametrizedStatementSupport { protected Flux execute(List bindings) { return Flux.defer(() -> QueryFlow.execute(client, query, returningIdentifiers(), bindings)) - .map(messages -> MySqlResult.toResult(false, codecs, context, syntheticKeyName(), messages)); + .map(messages -> MySqlSegmentResult.toResult(false, codecs, context, syntheticKeyName(), messages)); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java index 04fd90001..a265f7af2 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; @@ -35,6 +36,6 @@ public Flux execute() { return Flux.defer(() -> QueryFlow.execute( client, StringUtils.extendReturning(sql, returningIdentifiers()) - ).map(messages -> MySqlResult.toResult(false, codecs, context, syntheticKeyName(), messages))); + ).map(messages -> MySqlSegmentResult.toResult(false, codecs, context, syntheticKeyName(), messages))); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlBatch.java new file mode 100644 index 000000000..d49ca9b1e --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlBatch.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.Batch; +import reactor.core.publisher.Flux; + +/** + * {@link Batch} for executing a collection of statements in a batch against a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlBatch extends Batch { + + /** + * {@inheritDoc} + * + * @param sql the statement to add + * @return {@link MySqlBatch this} + * @throws IllegalArgumentException if {@code sql} is {@code null} + */ + @Override + MySqlBatch add(String sql); + + /** + * {@inheritDoc} + * + * @return the {@link MySqlResult}s of executing the batch + */ + @Override + Flux execute(); +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlColumnMetadata.java similarity index 59% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlColumnMetadata.java index 2fb0e431e..9bd71a36a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatch.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlColumnMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 asyncer.io projects + * Copyright 2024 asyncer.io projects * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,19 +14,14 @@ * limitations under the License. */ -package io.asyncer.r2dbc.mysql; +package io.asyncer.r2dbc.mysql.api; -import io.r2dbc.spi.Batch; -import reactor.core.publisher.Flux; +import io.r2dbc.spi.ColumnMetadata; /** - * Base class considers methods definition for implementations of {@link Batch}. + * {@link ColumnMetadata} for column metadata returned from a MySQL database. + * + * @since 1.1.3 */ -public abstract class MySqlBatch implements Batch { - - @Override - public abstract MySqlBatch add(String sql); - - @Override - public abstract Flux execute(); +public interface MySqlColumnMetadata extends MySqlReadableMetadata, ColumnMetadata { } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnection.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnection.java new file mode 100644 index 000000000..e2b5b2244 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlConnection.java @@ -0,0 +1,203 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Lifecycle; +import io.r2dbc.spi.TransactionDefinition; +import io.r2dbc.spi.ValidationDepth; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +/** + * A {@link Connection} for connecting to a MySQL database. + * + * @since 1.1.3 + */ +public interface MySqlConnection extends Connection, Lifecycle { + + /** + * {@inheritDoc} + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * Note: for now, it is only available in InnoDB or InnoDB-compatible engines. + * + * @param timeout the lock wait timeout. + * @return a new {@link MySqlTransactionDefinition} with the {@code timeout}. + * @throws IllegalArgumentException if {@code timeout} is {@code null}. + */ + MySqlTransactionDefinition lockWaitTimeout(Duration timeout); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to use the + * default lock wait timeout. Removes transaction lock wait timeout if configured already. + * + * @return a new {@link MySqlTransactionDefinition} without specified lock wait timeout. + */ + MySqlTransactionDefinition withoutLockWaitTimeout(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent snapshot. Overrides transaction consistency if configured already. + * + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + */ + MySqlTransactionDefinition consistent(); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent engine snapshot. Overrides transaction consistency if configured already. + * + * @param engine the consistent snapshot engine, e.g. {@code ROCKSDB}. + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + * @throws IllegalArgumentException if {@code engine} is {@code null}. + */ + MySqlTransactionDefinition consistent(String engine); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent engine snapshot from session. Overrides transaction consistency if configured already. + * + * @param engine the consistent snapshot engine, e.g. {@code ROCKSDB}. + * @param sessionId the session id. + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + * @throws IllegalArgumentException if {@code engine} is {@code null}. + */ + MySqlTransactionDefinition consistent(String engine, long sessionId); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to with + * consistent snapshot from session. Overrides transaction consistency if configured already. + * + * @param sessionId the session id. + * @return a new {@link MySqlTransactionDefinition} with consistent snapshot semantics. + */ + MySqlTransactionDefinition consistentFromSession(long sessionId); + + /** + * Creates a {@link MySqlTransactionDefinition} retaining all configured options and applying to without + * consistent snapshot. Removes transaction consistency if configured already. + * + * @return a new {@link MySqlTransactionDefinition} without consistent snapshot semantics. + */ + MySqlTransactionDefinition withoutConsistent(); + + /** + * Gets an empty {@link MySqlTransactionDefinition}. + * + * @return an empty {@link MySqlTransactionDefinition}. + */ + static MySqlTransactionDefinition empty() { + return SimpleTransactionDefinition.EMPTY; + } + + /** + * Creates a {@link MySqlTransactionDefinition} specifying transaction mutability. + * + * @param readWrite {@code true} for read-write, {@code false} to use a read-only transaction. + * @return a new {@link MySqlTransactionDefinition} using the specified transaction mutability. + */ + static MySqlTransactionDefinition mutability(boolean readWrite) { + return readWrite ? SimpleTransactionDefinition.EMPTY.readWrite() : + SimpleTransactionDefinition.EMPTY.readOnly(); + } + + static MySqlTransactionDefinition from(IsolationLevel isolationLevel) { + return SimpleTransactionDefinition.EMPTY.isolationLevel(isolationLevel); + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/SimpleTransactionDefinition.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/SimpleTransactionDefinition.java new file mode 100644 index 000000000..3c2d0f40d --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/SimpleTransactionDefinition.java @@ -0,0 +1,218 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Option; + +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; + +/** + * An implementation of {@link MySqlTransactionDefinition} for immutable transaction definition. + * + * @since 1.1.3 + */ +final class SimpleTransactionDefinition implements MySqlTransactionDefinition { + + static final SimpleTransactionDefinition EMPTY = new SimpleTransactionDefinition(Collections.emptyMap()); + + private final Map, Object> options; + + private SimpleTransactionDefinition(Map, Object> options) { + this.options = options; + } + + @SuppressWarnings("unchecked") + @Override + public T getAttribute(Option option) { + return (T) this.options.get(option); + } + + @Override + public MySqlTransactionDefinition isolationLevel(IsolationLevel isolationLevel) { + requireNonNull(isolationLevel, "isolationLevel must not be null"); + + return with(ISOLATION_LEVEL, isolationLevel); + } + + @Override + public MySqlTransactionDefinition withoutIsolationLevel() { + return without(ISOLATION_LEVEL); + } + + @Override + public MySqlTransactionDefinition readOnly() { + return with(READ_ONLY, true); + } + + @Override + public MySqlTransactionDefinition readWrite() { + return with(READ_ONLY, false); + } + + @Override + public MySqlTransactionDefinition withoutMutability() { + return without(READ_ONLY); + } + + @Override + public MySqlTransactionDefinition lockWaitTimeout(Duration timeout) { + requireNonNull(timeout, "timeout must not be null"); + + return with(LOCK_WAIT_TIMEOUT, timeout); + } + + @Override + public MySqlTransactionDefinition withoutLockWaitTimeout() { + return without(LOCK_WAIT_TIMEOUT); + } + + @Override + public MySqlTransactionDefinition consistent() { + return with(WITH_CONSISTENT_SNAPSHOT, true); + } + + @Override + public MySqlTransactionDefinition consistent(String engine) { + requireNonNull(engine, "engine must not be null"); + + return consistent0(CONSISTENT_SNAPSHOT_ENGINE, engine); + } + + @Override + public MySqlTransactionDefinition consistent(String engine, long sessionId) { + requireNonNull(engine, "engine must not be null"); + + Map, Object> options = new HashMap<>(this.options); + + options.put(WITH_CONSISTENT_SNAPSHOT, true); + options.put(CONSISTENT_SNAPSHOT_ENGINE, engine); + options.put(CONSISTENT_SNAPSHOT_FROM_SESSION, sessionId); + + return of(options); + } + + @Override + public MySqlTransactionDefinition consistentFromSession(long sessionId) { + return consistent0(CONSISTENT_SNAPSHOT_FROM_SESSION, sessionId); + } + + @Override + public MySqlTransactionDefinition withoutConsistent() { + if (this.options.isEmpty()) { + return this; + } + + Map, Object> options = new HashMap<>(this.options); + + options.remove(WITH_CONSISTENT_SNAPSHOT); + options.remove(CONSISTENT_SNAPSHOT_ENGINE); + options.remove(CONSISTENT_SNAPSHOT_FROM_SESSION); + + return of(options); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SimpleTransactionDefinition)) { + return false; + } + + SimpleTransactionDefinition that = (SimpleTransactionDefinition) o; + + return options.equals(that.options); + } + + @Override + public int hashCode() { + return options.hashCode(); + } + + @Override + public String toString() { + return "SimpleTransactionDefinition" + options; + } + + private MySqlTransactionDefinition with(Option option, T value) { + if (this.options.isEmpty()) { + return new SimpleTransactionDefinition(Collections.singletonMap(option, value)); + } + + if (value.equals(this.options.get(option))) { + return this; + } + + Map, Object> options = new HashMap<>(this.options); + + options.put(option, value); + + return of(options); + } + + private SimpleTransactionDefinition without(Option option) { + requireNonNull(option, "option must not be null"); + + if (!this.options.containsKey(option)) { + return this; + } + + if (this.options.size() == 1) { + return EMPTY; + } + + Map, Object> options = new HashMap<>(this.options); + + options.remove(option); + + return of(options); + } + + private MySqlTransactionDefinition consistent0(Option option, T value) { + if (Boolean.TRUE.equals(this.options.get(WITH_CONSISTENT_SNAPSHOT))) { + return with(option, value); + } + + Map, Object> options = new HashMap<>(this.options); + + options.put(WITH_CONSISTENT_SNAPSHOT, true); + options.put(option, value); + + return of(options); + } + + private static SimpleTransactionDefinition of(Map, Object> options) { + switch (options.size()) { + case 0: + return EMPTY; + case 1: { + Map.Entry, Object> e = options.entrySet().iterator().next(); + + return new SimpleTransactionDefinition(Collections.singletonMap(e.getKey(), e.getValue())); + } + default: + return new SimpleTransactionDefinition(options); + } + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/package-info.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/package-info.java new file mode 100644 index 000000000..65e67fa90 --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * R2DBC driver API for MySQL. + */ + +@NotNullByDefault +package io.asyncer.r2dbc.mysql.api; + +import io.asyncer.r2dbc.mysql.internal.NotNullByDefault; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java index 9e26692fb..2462c46dc 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; /** * Codec for classed type when field bytes less or equals than {@link Integer#MAX_VALUE}. @@ -32,9 +32,9 @@ abstract class AbstractClassedCodec implements Codec { } @Override - public final boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public final boolean canDecode(MySqlReadableMetadata metadata, Class target) { return target.isAssignableFrom(this.type) && doCanDecode(metadata); } - protected abstract boolean doCanDecode(MySqlColumnMetadata metadata); + protected abstract boolean doCanDecode(MySqlReadableMetadata metadata); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java index 3370d79d0..295b0d18d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; @@ -40,7 +40,7 @@ abstract class AbstractPrimitiveCodec implements PrimitiveCodec { } @Override - public final boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public final boolean canDecode(MySqlReadableMetadata metadata, Class target) { return target.isAssignableFrom(boxedClass) && canPrimitiveDecode(metadata); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java index 7d7e7008c..ced23b056 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigDecimalCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -39,7 +39,7 @@ private BigDecimalCodec() { } @Override - public BigDecimal decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public BigDecimal decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -82,7 +82,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java index 0b1be61f4..622c9edf0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BigIntegerCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -40,7 +40,7 @@ private BigIntegerCodec() { } @Override - public BigInteger decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public BigInteger decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -93,7 +93,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java index 839c81a6e..7ad97b1fb 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BitSetCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -41,7 +41,7 @@ private BitSetCodec() { } @Override - public BitSet decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public BitSet decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return BitSet.valueOf(EMPTY_BYTES); @@ -91,7 +91,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.BIT; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java index 0363986e3..dc3a75b5e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.lob.LobUtils; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -50,19 +50,19 @@ private BlobCodec() { } @Override - public Blob decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Blob decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createBlob(value); } @Override - public Blob decodeMassive(List value, MySqlColumnMetadata metadata, Class target, + public Blob decodeMassive(List value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createBlob(value); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { MySqlType type = metadata.getType(); return (type.isLob() || type == MySqlType.GEOMETRY) && target.isAssignableFrom(Blob.class); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index adec25731..3b8035c5f 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -36,7 +36,7 @@ private BooleanCodec() { } @Override - public Boolean decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Boolean decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return binary || metadata.getType() == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0'; } @@ -52,9 +52,10 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); - return (type == MySqlType.BIT || type == MySqlType.TINYINT) && metadata.getNativePrecision() == 1; + return (type == MySqlType.BIT || type == MySqlType.TINYINT) && + Integer.valueOf(1).equals(metadata.getPrecision()); } private static final class BooleanMySqlParameter extends AbstractMySqlParameter { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java index aa4beb2fb..4dd9b989c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -42,7 +42,7 @@ private ByteArrayCodec() { } @Override - public byte[] decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public byte[] decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return EMPTY_BYTES; @@ -62,7 +62,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isBinary(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java index 0720e6c81..598551d29 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteBufferCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -41,7 +41,7 @@ private ByteBufferCodec() { } @Override - public ByteBuffer decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public ByteBuffer decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return ByteBuffer.wrap(EMPTY_BYTES); @@ -66,7 +66,7 @@ public boolean canEncode(Object value) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isBinary(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java index c8257cf3d..649a6266d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -36,7 +36,7 @@ private ByteCodec() { } @Override - public Byte decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Byte decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return (byte) IntegerCodec.decodeInt(value, binary, metadata.getType()); } @@ -52,7 +52,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java index 9d69a0c57..b3bae3689 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.lob.LobUtils; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -53,19 +53,19 @@ private ClobCodec() { } @Override - public Clob decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Clob decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createClob(value, metadata.getCharCollation(context)); } @Override - public Clob decodeMassive(List value, MySqlColumnMetadata metadata, Class target, + public Clob decodeMassive(List value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return LobUtils.createClob(value, metadata.getCharCollation(context)); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { MySqlType type = metadata.getType(); return (type.isLob() || type == MySqlType.JSON) && target.isAssignableFrom(Clob.class); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java index eb184fc20..c970c212f 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java @@ -16,8 +16,8 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; @@ -34,24 +34,24 @@ public interface Codec { * Decodes a {@link ByteBuf} as specified {@link Class}. * * @param value the {@link ByteBuf}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link Class}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - T decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + T decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context); /** * Checks if the field value can be decoded as specified {@link Class}. * - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link Class}. * @return if it can decode. */ - boolean canDecode(MySqlColumnMetadata metadata, Class target); + boolean canDecode(MySqlReadableMetadata metadata, Class target); /** * Checks if it can encode the specified value. diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java index 45129a26f..d9ac71e99 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codecs.java @@ -16,8 +16,8 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.netty.buffer.ByteBufAllocator; import org.jetbrains.annotations.Nullable; @@ -33,7 +33,7 @@ public interface Codecs { * Decode a {@link FieldValue} as specified {@link Class type}. * * @param value the {@link FieldValue}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param type the specified {@link Class}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. @@ -42,14 +42,14 @@ public interface Codecs { * @throws IllegalArgumentException if any parameter is {@code null}, or {@code value} cannot be decoded. */ @Nullable - T decode(FieldValue value, MySqlColumnMetadata metadata, Class type, boolean binary, + T decode(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context); /** * Decode a {@link FieldValue} as a specified {@link ParameterizedType type}. * * @param value the {@link FieldValue}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param type the specified {@link ParameterizedType}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. @@ -58,7 +58,7 @@ T decode(FieldValue value, MySqlColumnMetadata metadata, Class type, bool * @throws IllegalArgumentException if any parameter is {@code null}, or {@code value} cannot be decoded. */ @Nullable - T decode(FieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, boolean binary, + T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context); /** diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index 9c95d7161..882637f10 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -16,8 +16,8 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.asyncer.r2dbc.mysql.message.LargeFieldValue; @@ -89,7 +89,7 @@ private DefaultCodecs(Codec[] codecs) { * release this buffer. */ @Override - public T decode(FieldValue value, MySqlColumnMetadata metadata, Class type, boolean binary, + public T decode(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { requireNonNull(value, "value must not be null"); requireNonNull(metadata, "info must not be null"); @@ -117,7 +117,7 @@ public T decode(FieldValue value, MySqlColumnMetadata metadata, Class typ } @Override - public T decode(FieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, + public T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { requireNonNull(value, "value must not be null"); requireNonNull(metadata, "info must not be null"); @@ -199,7 +199,7 @@ public MySqlParameter encodeNull() { } @Nullable - private T decodePrimitive(FieldValue value, MySqlColumnMetadata metadata, Class type, + private T decodePrimitive(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { @SuppressWarnings("unchecked") PrimitiveCodec codec = (PrimitiveCodec) this.primitiveCodecs.get(type); @@ -214,7 +214,7 @@ private T decodePrimitive(FieldValue value, MySqlColumnMetadata metadata, Cl } @Nullable - private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, Class type, + private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { for (Codec codec : codecs) { if (codec.canDecode(metadata, type)) { @@ -228,7 +228,7 @@ private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, } @Nullable - private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, + private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { for (ParametrizedCodec codec : parametrizedCodecs) { if (codec.canDecode(metadata, type)) { @@ -242,7 +242,7 @@ private T decodeNormal(NormalFieldValue value, MySqlColumnMetadata metadata, } @Nullable - private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, Class type, + private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { for (MassiveCodec codec : massiveCodecs) { if (codec.canDecode(metadata, type)) { @@ -256,7 +256,7 @@ private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, } @Nullable - private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, ParameterizedType type, + private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { for (MassiveParametrizedCodec codec : massiveParametrizedCodecs) { if (codec.canDecode(metadata, type)) { @@ -269,7 +269,7 @@ private T decodeMassive(LargeFieldValue value, MySqlColumnMetadata metadata, throw new IllegalArgumentException("Cannot decode massive " + type + " for " + metadata.getType()); } - private static Class chooseClass(MySqlColumnMetadata metadata, Class type) { + private static Class chooseClass(MySqlReadableMetadata metadata, Class type) { Class javaType = metadata.getType().getJavaType(); return type.isAssignableFrom(javaType) ? javaType : type; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java index 728838787..fa544965b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -38,7 +38,7 @@ private DoubleCodec() { } @Override - public Double decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Double decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -68,7 +68,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java index e4ae98b87..1c83d06b6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DurationCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -48,7 +48,7 @@ private DurationCodec() { } @Override - public Duration decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Duration decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return binary ? decodeBinary(value) : decodeText(value); } @@ -64,7 +64,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.TIME; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java index 9a2a0ce78..a658b3598 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -38,7 +38,7 @@ private EnumCodec() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Override - public Enum decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Enum decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { Charset charset = metadata.getCharCollation(context).getCharset(); @@ -46,7 +46,7 @@ public Enum decode(ByteBuf value, MySqlColumnMetadata metadata, Class targ } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return metadata.getType() == MySqlType.ENUM && target.isEnum(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java index 581b289f8..91dd20b46 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -38,7 +38,7 @@ private FloatCodec() { } @Override - public Float decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Float decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -68,7 +68,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java index 17d0793ed..0a14c6ee3 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -40,7 +40,7 @@ private InstantCodec() { } @Override - public Instant decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Instant decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { LocalDateTime origin = LocalDateTimeCodec.decodeOrigin(value, binary, context); @@ -65,7 +65,7 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, Instant.class); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java index 7e67e2a6f..73b9d702c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.codec.ShortCodec.ShortMySqlParameter; import io.asyncer.r2dbc.mysql.constant.MySqlType; @@ -41,7 +41,7 @@ private IntegerCodec() { } @Override - public Integer decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Integer decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decodeInt(value, binary, metadata.getType()); } @@ -65,7 +65,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java index 8f72fcb92..a3f9fbbd3 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -41,7 +41,7 @@ final class LocalDateCodec extends AbstractClassedCodec { } @Override - public LocalDate decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public LocalDate decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { int bytes = value.readableBytes(); LocalDate date = binary ? readDateBinary(value, bytes) : readDateText(value); @@ -64,7 +64,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.DATE; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java index a0d553d67..17b09b276 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -46,13 +46,13 @@ private LocalDateTimeCodec() { } @Override - public LocalDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public LocalDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decodeOrigin(value, binary, context); } @Override - public ChronoLocalDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, + public ChronoLocalDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { return decodeOrigin(value, binary, context); } @@ -68,12 +68,12 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { return DateTimes.canDecodeChronology(metadata.getType(), target, ChronoLocalDateTime.class); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, LocalDateTime.class); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java index 101f88af6..c0102709b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -55,7 +55,7 @@ private LocalTimeCodec() { } @Override - public LocalTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public LocalTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decodeOrigin(binary, value); } @@ -71,7 +71,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.TIME; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java index 862a8f2ce..30c495db6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.codec.IntegerCodec.IntMySqlParameter; import io.asyncer.r2dbc.mysql.codec.ShortCodec.ShortMySqlParameter; @@ -42,7 +42,7 @@ private LongCodec() { } @Override - public Long decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Long decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { MySqlType type = metadata.getType(); @@ -73,7 +73,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java index 18bdb80a2..65cbd2222 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; @@ -33,13 +33,13 @@ public interface MassiveCodec extends Codec { * Decode a massive value as specified {@link Class}. * * @param value {@link ByteBuf}s list. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link Class}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - T decodeMassive(List value, MySqlColumnMetadata metadata, Class target, boolean binary, + T decodeMassive(List value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java index 290a4b815..e43af83d8 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; @@ -34,14 +34,13 @@ public interface MassiveParametrizedCodec extends ParametrizedCodec, Massi * Decode a massive value as specified {@link ParameterizedType}. * * @param value {@link ByteBuf}s list. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link ParameterizedType}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - Object decodeMassive(List value, MySqlColumnMetadata metadata, ParameterizedType target, - boolean binary, - CodecContext context); + Object decodeMassive(List value, MySqlReadableMetadata metadata, ParameterizedType target, + boolean binary, CodecContext context); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java index 694578b13..b99714638 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -40,7 +40,7 @@ private OffsetDateTimeCodec() { } @Override - public OffsetDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public OffsetDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { LocalDateTime origin = LocalDateTimeCodec.decodeOrigin(value, binary, context); @@ -65,7 +65,7 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, OffsetDateTime.class); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java index 1ed3769a6..57fb77b17 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -42,7 +42,7 @@ private OffsetTimeCodec() { } @Override - public OffsetTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public OffsetTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { // OffsetTime is not an instant value, so preserveInstants is not used here. LocalTime origin = LocalTimeCodec.decodeOrigin(binary, value); @@ -63,7 +63,7 @@ public boolean canEncode(Object value) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.TIME; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java index 9c43bedb4..2950b08bb 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; import org.jetbrains.annotations.Nullable; @@ -35,22 +35,22 @@ public interface ParametrizedCodec extends Codec { * Decodes a {@link ByteBuf} as specified {@link ParameterizedType}. * * @param value the {@link ByteBuf}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link ParameterizedType}. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded result. */ @Nullable - Object decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedType target, boolean binary, + Object decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context); /** * Checks if the field value can be decoded as specified {@link ParameterizedType}. * - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link ParameterizedType}. * @return if it can decode. */ - boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target); + boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java index 3e2c90691..b8c420777 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.netty.buffer.ByteBuf; /** @@ -33,23 +33,23 @@ interface PrimitiveCodec extends Codec { * Decodes a {@link ByteBuf} as specified {@link Class}. * * @param value the {@link ByteBuf}. - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @param target the specified {@link Class}, which can be a primitive type. * @param binary if the value should be decoded by binary protocol. * @param context the codec context. * @return the decoded data that is boxed. */ @Override - T decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + T decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context); /** * Checks if the field value can be decoded as a primitive data. * - * @param metadata the metadata of the column. + * @param metadata the metadata of the column or the {@code OUT} parameter. * @return if it can decode. */ - boolean canPrimitiveDecode(MySqlColumnMetadata metadata); + boolean canPrimitiveDecode(MySqlReadableMetadata metadata); /** * Gets the primitive {@link Class}, such as {@link Integer#TYPE}, etc. diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java index 67e8db371..a4ce64e06 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -49,7 +49,7 @@ private SetCodec() { } @Override - public String[] decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public String[] decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return EMPTY_STRINGS; @@ -67,7 +67,7 @@ public String[] decode(ByteBuf value, MySqlColumnMetadata metadata, Class tar @SuppressWarnings({ "unchecked", "rawtypes" }) @Override - public Set decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedType target, boolean binary, + public Set decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { if (!value.isReadable()) { return Collections.emptySet(); @@ -105,12 +105,12 @@ public Set decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedT } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return metadata.getType() == MySqlType.SET && target.isAssignableFrom(String[].class); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { if (metadata.getType() != MySqlType.SET) { return false; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java index 952ce2de7..c3c42948c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; @@ -37,7 +37,7 @@ private ShortCodec() { } @Override - public Short decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Short decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return (short) IntegerCodec.decodeInt(value, binary, metadata.getType()); } @@ -59,7 +59,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlColumnMetadata metadata) { + public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java index cb5a661bb..2a4bfe5a7 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/StringCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -39,7 +39,7 @@ private StringCodec() { } @Override - public String decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public String decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { if (!value.isReadable()) { return ""; @@ -59,7 +59,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - protected boolean doCanDecode(MySqlColumnMetadata metadata) { + protected boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isString(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java index 90a54c1bb..fa7f8b9c0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/YearCodec.java @@ -16,8 +16,8 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.ByteCodec.ByteMySqlParameter; import io.asyncer.r2dbc.mysql.codec.IntegerCodec.IntMySqlParameter; import io.asyncer.r2dbc.mysql.codec.ShortCodec.ShortMySqlParameter; @@ -40,7 +40,7 @@ private YearCodec() { } @Override - public Year decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Year decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return binary ? Year.of(value.readShortLE()) : Year.of(CodecUtils.parseInt(value)); } @@ -66,7 +66,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean doCanDecode(MySqlColumnMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType() == MySqlType.YEAR; } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java index 3bd0072b6..9af4aaa61 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java @@ -16,9 +16,9 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -45,13 +45,13 @@ private ZonedDateTimeCodec() { } @Override - public ZonedDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public ZonedDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { return decode0(value, binary, context); } @Override - public ChronoZonedDateTime decode(ByteBuf value, MySqlColumnMetadata metadata, + public ChronoZonedDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { return decode0(value, binary, context); } @@ -67,12 +67,12 @@ public boolean canEncode(Object value) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { return DateTimes.canDecodeChronology(metadata.getType(), target, ChronoZonedDateTime.class); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return DateTimes.canDecodeDateTime(metadata.getType(), target, ZonedDateTime.class); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java index d425cd9cd..2485d897b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/MySqlType.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.constant; -import io.asyncer.r2dbc.mysql.ColumnDefinition; +import io.asyncer.r2dbc.mysql.api.MySqlNativeTypeMetadata; import io.r2dbc.spi.Type; import java.math.BigDecimal; @@ -671,24 +671,24 @@ public int getBinarySize() { return 0; } - public static MySqlType of(int id, ColumnDefinition definition) { + public static MySqlType of(MySqlNativeTypeMetadata metadata) { // Maybe need to check if it is a string-like type? - if (definition.isSet()) { + if (metadata.isSet()) { return SET; - } else if (definition.isEnum()) { + } else if (metadata.isEnum()) { return ENUM; } - switch (id) { + switch (metadata.getTypeId()) { case ID_DECIMAL: case ID_NEW_DECIMAL: return DECIMAL; case ID_TINYINT: - return definition.isUnsigned() ? TINYINT_UNSIGNED : TINYINT; + return metadata.isUnsigned() ? TINYINT_UNSIGNED : TINYINT; case ID_SMALLINT: - return definition.isUnsigned() ? SMALLINT_UNSIGNED : SMALLINT; + return metadata.isUnsigned() ? SMALLINT_UNSIGNED : SMALLINT; case ID_INT: - return definition.isUnsigned() ? INT_UNSIGNED : INT; + return metadata.isUnsigned() ? INT_UNSIGNED : INT; case ID_FLOAT: return FLOAT; case ID_DOUBLE: @@ -698,9 +698,9 @@ public static MySqlType of(int id, ColumnDefinition definition) { case ID_TIMESTAMP: return TIMESTAMP; case ID_BIGINT: - return definition.isUnsigned() ? BIGINT_UNSIGNED : BIGINT; + return metadata.isUnsigned() ? BIGINT_UNSIGNED : BIGINT; case ID_MEDIUMINT: - return definition.isUnsigned() ? MEDIUMINT_UNSIGNED : MEDIUMINT; + return metadata.isUnsigned() ? MEDIUMINT_UNSIGNED : MEDIUMINT; case ID_DATE: return DATE; case ID_TIME: @@ -712,7 +712,7 @@ public static MySqlType of(int id, ColumnDefinition definition) { case ID_VARCHAR: case ID_VAR_STRING: case ID_STRING: - return definition.isBinary() ? VARBINARY : VARCHAR; + return metadata.isBinary() ? VARBINARY : VARCHAR; case ID_BIT: return BIT; case ID_JSON: @@ -722,13 +722,13 @@ public static MySqlType of(int id, ColumnDefinition definition) { case ID_SET: return SET; case ID_TINYBLOB: - return definition.isBinary() ? TINYBLOB : TINYTEXT; + return metadata.isBinary() ? TINYBLOB : TINYTEXT; case ID_MEDIUMBLOB: - return definition.isBinary() ? MEDIUMBLOB : MEDIUMTEXT; + return metadata.isBinary() ? MEDIUMBLOB : MEDIUMTEXT; case ID_LONGBLOB: - return definition.isBinary() ? LONGBLOB : LONGTEXT; + return metadata.isBinary() ? LONGBLOB : LONGTEXT; case ID_BLOB: - return definition.isBinary() ? BLOB : TEXT; + return metadata.isBinary() ? BLOB : TEXT; case ID_GEOMETRY: // Most Geometry libraries were using byte[] to encode/decode which based on WKT // (includes Extended-WKT) or WKB diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java index a2aac7a8b..b4a9d9fdd 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/DefinitionMetadataMessage.java @@ -16,7 +16,6 @@ package io.asyncer.r2dbc.mysql.message.server; -import io.asyncer.r2dbc.mysql.ColumnDefinition; import io.asyncer.r2dbc.mysql.ConnectionContext; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; @@ -53,15 +52,14 @@ public final class DefinitionMetadataMessage implements ServerMessage { private final short typeId; - private final ColumnDefinition definition; + private final int definitions; private final short decimals; private DefinitionMetadataMessage(@Nullable String database, String table, @Nullable String originTable, String column, @Nullable String originColumn, int collationId, long size, short typeId, - ColumnDefinition definition, short decimals) { + int definitions, short decimals) { require(size >= 0, "size must not be a negative integer"); - require(collationId > 0, "collationId must be a positive integer"); this.database = database; this.table = requireNonNull(table, "table must not be null"); @@ -71,7 +69,7 @@ private DefinitionMetadataMessage(@Nullable String database, String table, @Null this.collationId = collationId; this.size = size; this.typeId = typeId; - this.definition = requireNonNull(definition, "definition must not be null"); + this.definitions = definitions; this.decimals = decimals; } @@ -91,8 +89,8 @@ public short getTypeId() { return typeId; } - public ColumnDefinition getDefinition() { - return definition; + public int getDefinitions() { + return definitions; } public short getDecimals() { @@ -111,7 +109,7 @@ public boolean equals(Object o) { return collationId == that.collationId && size == that.size && typeId == that.typeId && - definition.equals(that.definition) && + definitions == that.definitions && decimals == that.decimals && Objects.equals(database, that.database) && table.equals(that.table) && @@ -123,14 +121,14 @@ public boolean equals(Object o) { @Override public int hashCode() { return Objects.hash(database, table, originTable, column, originColumn, collationId, size, typeId, - definition, decimals); + definitions, decimals); } @Override public String toString() { return "DefinitionMetadataMessage{database='" + database + "', table='" + table + "' (origin:'" + originTable + "'), column='" + column + "' (origin:'" + originColumn + "'), collationId=" + - collationId + ", size=" + size + ", type=" + typeId + ", definition=" + definition + + collationId + ", size=" + size + ", type=" + typeId + ", definitions=" + definitions + ", decimals=" + decimals + '}'; } @@ -155,11 +153,11 @@ private static DefinitionMetadataMessage decode320(ByteBuf buf, ConnectionContex short typeId = buf.readUnsignedByte(); buf.skipBytes(1); // Constant 0x3 - ColumnDefinition definition = ColumnDefinition.of(buf.readShortLE()); + int definitions = buf.readUnsignedShortLE(); short decimals = buf.readUnsignedByte(); - return new DefinitionMetadataMessage(null, table, null, column, null, collation.getId(), size, typeId, - definition, decimals); + return new DefinitionMetadataMessage(null, table, null, column, null, 0, size, typeId, + definitions, decimals); } private static DefinitionMetadataMessage decode41(ByteBuf buf, ConnectionContext context) { @@ -179,10 +177,10 @@ private static DefinitionMetadataMessage decode41(ByteBuf buf, ConnectionContext int collationId = buf.readUnsignedShortLE(); long size = buf.readUnsignedIntLE(); short typeId = buf.readUnsignedByte(); - ColumnDefinition definition = ColumnDefinition.of(buf.readShortLE(), collationId); + int definitions = buf.readUnsignedShortLE(); return new DefinitionMetadataMessage(database, table, originTable, column, originColumn, collationId, - size, typeId, definition, buf.readUnsignedByte()); + size, typeId, definitions, buf.readUnsignedByte()); } private static String readVarIntSizedString(ByteBuf buf, Charset charset) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java index 9e4a3ac74..e914b440b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/RowMessage.java @@ -16,7 +16,7 @@ package io.asyncer.r2dbc.mysql.message.server; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.internal.util.NettyBufferUtils; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.netty.util.ReferenceCounted; @@ -45,7 +45,7 @@ public final class RowMessage implements ReferenceCounted, ServerMessage { * @param context information context array. * @return the {@link FieldValue} array. */ - public FieldValue[] decode(boolean isBinary, MySqlColumnMetadata[] context) { + public FieldValue[] decode(boolean isBinary, MySqlReadableMetadata[] context) { return isBinary ? binary(context) : text(context.length); } @@ -69,7 +69,7 @@ private FieldValue[] text(int size) { } } - private FieldValue[] binary(MySqlColumnMetadata[] context) { + private FieldValue[] binary(MySqlReadableMetadata[] context) { reader.skipOneByte(); // constant 0x00 int size = context.length; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java deleted file mode 100644 index 4b84d09ba..000000000 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ColumnDefinitionTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.collation.CharCollation; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link ColumnDefinition}. - */ -class ColumnDefinitionTest { - - @Test - void allSet() { - ColumnDefinition definition = ColumnDefinition.of(-1); - - assertThat(definition.isBinary()).isTrue(); - assertThat(definition.isSet()).isTrue(); - assertThat(definition.isUnsigned()).isTrue(); - assertThat(definition.isEnum()).isTrue(); - assertThat(definition.isNotNull()).isTrue(); - } - - @Test - void noSet() { - ColumnDefinition definition = ColumnDefinition.of(0); - - assertThat(definition.isBinary()).isFalse(); - assertThat(definition.isSet()).isFalse(); - assertThat(definition.isUnsigned()).isFalse(); - assertThat(definition.isEnum()).isFalse(); - assertThat(definition.isNotNull()).isFalse(); - - } - - @Test - void isBinaryUsesCollationId() { - ColumnDefinition definition = ColumnDefinition.of(-1, CharCollation.BINARY_ID); - - assertThat(definition.isBinary()).isTrue(); - - definition = ColumnDefinition.of(-1, ~CharCollation.BINARY_ID); - assertThat(definition.isBinary()).isFalse(); - } -} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index 50c4d5122..b45d7f91c 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -16,6 +16,11 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlBatch; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.R2dbcPermissionDeniedException; import io.r2dbc.spi.TransactionDefinition; @@ -23,6 +28,7 @@ import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.reactivestreams.Publisher; import org.testcontainers.shaded.com.fasterxml.jackson.core.JsonProcessingException; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import org.testcontainers.shaded.com.fasterxml.jackson.databind.node.ArrayNode; @@ -43,6 +49,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.Objects; +import java.util.function.Function; import static io.r2dbc.spi.IsolationLevel.READ_COMMITTED; import static io.r2dbc.spi.IsolationLevel.READ_UNCOMMITTED; @@ -51,7 +58,7 @@ import static org.assertj.core.api.Assertions.assertThat; /** - * Integration tests for {@link MySqlConnection}. + * Integration tests for {@link MySqlSimpleConnection}. */ class ConnectionIntegrationTest extends IntegrationTestSupport { @@ -61,7 +68,7 @@ class ConnectionIntegrationTest extends IntegrationTestSupport { @Test void isInTransaction() { - complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) .isFalse()) .then(connection.beginTransaction()) .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) @@ -76,16 +83,12 @@ void isInTransaction() { @DisabledIf("envIsLessThanMySql56") @Test void startTransaction() { - TransactionDefinition readOnlyConsistent = MySqlTransactionDefinition.builder() - .withConsistentSnapshot(true) - .readOnly(true) - .build(); - TransactionDefinition readWriteConsistent = MySqlTransactionDefinition.builder() - .withConsistentSnapshot(true) - .readOnly(false) - .build(); - - complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) + TransactionDefinition readOnlyConsistent = MySqlTransactionDefinition.mutability(false) + .consistent(); + TransactionDefinition readWriteConsistent = MySqlTransactionDefinition.mutability(true) + .consistent(); + + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) .isFalse()) .then(connection.beginTransaction(readOnlyConsistent)) .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) @@ -100,56 +103,60 @@ void startTransaction() { @Test void autoRollbackPreRelease() { // Mock pool allocate/release. - complete(conn -> conn.postAllocate() - .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") - .execute()) - .flatMap(MySqlResult::getRowsUpdated) - .then(conn.beginTransaction()) - .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") - .execute()) - .flatMap(MySqlResult::getRowsUpdated) - .single() - .doOnNext(it -> assertThat(it).isEqualTo(1)) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isTrue()) - .then(conn.preRelease()) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) - .then(conn.postAllocate()) - .thenMany(conn.createStatement("SELECT * FROM test") - .execute()) - .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) - .count() - .doOnNext(it -> assertThat(it).isZero())); + complete(connection -> Mono.just(connection) + .cast(MySqlSimpleConnection.class) + .flatMap(conn -> connection.postAllocate() + .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .then(conn.beginTransaction()) + .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .single() + .doOnNext(it -> assertThat(it).isEqualTo(1)) + .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isTrue()) + .then(conn.preRelease()) + .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) + .then(conn.postAllocate()) + .thenMany(conn.createStatement("SELECT * FROM test") + .execute()) + .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) + .count() + .doOnNext(it -> assertThat(it).isZero()))); } @Test void shouldNotRollbackCommittedPreRelease() { // Mock pool allocate/release. - complete(conn -> conn.postAllocate() - .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") - .execute()) - .flatMap(MySqlResult::getRowsUpdated) - .then(conn.beginTransaction()) - .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") - .execute()) - .flatMap(MySqlResult::getRowsUpdated) - .single() - .doOnNext(it -> assertThat(it).isEqualTo(1)) - .then(conn.commitTransaction()) - .then(conn.preRelease()) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) - .then(conn.postAllocate()) - .thenMany(conn.createStatement("SELECT * FROM test") - .execute()) - .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) - .collectList() - .doOnNext(it -> assertThat(it).isEqualTo(Collections.singletonList(1)))); + complete(connection -> Mono.just(connection) + .cast(MySqlSimpleConnection.class) + .flatMap(conn -> conn.postAllocate() + .thenMany(conn.createStatement("CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .then(conn.beginTransaction()) + .thenMany(conn.createStatement("INSERT INTO test VALUES (1)") + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .single() + .doOnNext(it -> assertThat(it).isEqualTo(1)) + .then(conn.commitTransaction()) + .then(conn.preRelease()) + .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) + .then(conn.postAllocate()) + .thenMany(conn.createStatement("SELECT * FROM test") + .execute()) + .flatMap(it -> it.map((row, metadata) -> row.get(0, Integer.class))) + .collectList() + .doOnNext(it -> assertThat(it).isEqualTo(Collections.singletonList(1))))); } @Test void transactionDefinitionLockWaitTimeout() { - complete(connection -> connection.beginTransaction(MySqlTransactionDefinition.builder() - .lockWaitTimeout(Duration.ofSeconds(345)) - .build()) + castedComplete(connection -> connection + .beginTransaction(MySqlTransactionDefinition.empty() + .lockWaitTimeout(Duration.ofSeconds(345))) .doOnSuccess(ignored -> { assertThat(connection.isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); @@ -165,9 +172,8 @@ void transactionDefinitionLockWaitTimeout() { @Test void transactionDefinitionIsolationLevel() { - complete(connection -> connection.beginTransaction(MySqlTransactionDefinition.builder() - .isolationLevel(READ_COMMITTED) - .build()) + castedComplete(connection -> connection + .beginTransaction(MySqlTransactionDefinition.from(READ_COMMITTED)) .doOnSuccess(ignored -> { assertThat(connection.isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(READ_COMMITTED); @@ -183,7 +189,7 @@ void transactionDefinitionIsolationLevel() { @Test void setTransactionLevelNotInTransaction() { - complete(connection -> + castedComplete(connection -> // check initial session isolation level Mono.fromSupplier(connection::getTransactionIsolationLevel) .doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ)) @@ -206,7 +212,7 @@ void setTransactionLevelNotInTransaction() { @Test void setTransactionLevelInTransaction() { - complete(connection -> + castedComplete(connection -> // check initial session transaction isolation level Mono.fromSupplier(connection::getTransactionIsolationLevel) .doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ)) @@ -229,11 +235,10 @@ void setTransactionLevelInTransaction() { @Test void transactionDefinition() { // The WITH CONSISTENT SNAPSHOT phrase can only be used with the REPEATABLE READ isolation level. - complete(connection -> connection.beginTransaction(MySqlTransactionDefinition.builder() + castedComplete(connection -> connection + .beginTransaction(MySqlTransactionDefinition.from(REPEATABLE_READ) .lockWaitTimeout(Duration.ofSeconds(112)) - .isolationLevel(REPEATABLE_READ) - .withConsistentSnapshot(true) - .build()) + .consistent()) .doOnSuccess(ignored -> { assertThat(connection.isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); @@ -259,29 +264,29 @@ void setAutoCommit() { @Test void autoCommitAutomaticallyTurnedOffInTransaction() { complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isAutoCommit()).isTrue()) - .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.commitTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); + .then(connection.beginTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.commitTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); } @Test void autoCommitStatusIsRestoredAfterTransaction() { complete(connection -> Mono.fromRunnable(() -> assertThat(connection.isAutoCommit()).isTrue()) - .then(connection.setAutoCommit(false)) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.commitTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) - .then(connection.setAutoCommit(true)) - .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); + .then(connection.setAutoCommit(false)) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.beginTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.commitTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isFalse()) + .then(connection.setAutoCommit(true)) + .doOnSuccess(ignored -> assertThat(connection.isAutoCommit()).isTrue())); } @ParameterizedTest @ValueSource(strings = { "test", "save`point" }) void createSavepointAndRollbackToSavepoint(String savepoint) { - complete(connection -> Mono.from(connection.createStatement( + castedComplete(connection -> Mono.from(connection.createStatement( "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) .flatMap(IntegrationTestSupport::extractRowsUpdated) .then(connection.beginTransaction()) @@ -322,36 +327,36 @@ void createSavepointAndRollbackToSavepoint(String savepoint) { @ParameterizedTest @ValueSource(strings = { "test", "save`point" }) void createSavepointAndRollbackEntireTransaction(String savepoint) { - complete(connection -> Mono.from(connection.createStatement( - "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (1, 'test1')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (2, 'test2')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) - .doOnSuccess(count -> assertThat(count).isEqualTo(2)) - .then(connection.createSavepoint(savepoint)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (3, 'test3')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (4, 'test4')") - .execute())) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) - .doOnSuccess(count -> assertThat(count).isEqualTo(4)) - .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) - .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) - .doOnSuccess(count -> assertThat(count).isEqualTo(0)) + castedComplete(connection -> Mono.from(connection.createStatement( + "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(connection.beginTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (1, 'test1')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (2, 'test2')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) + .doOnSuccess(count -> assertThat(count).isEqualTo(2)) + .then(connection.createSavepoint(savepoint)) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (3, 'test3')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (4, 'test4')") + .execute())) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) + .doOnSuccess(count -> assertThat(count).isEqualTo(4)) + .then(connection.rollbackTransaction()) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) + .doOnSuccess(count -> assertThat(count).isEqualTo(0)) ); } @@ -376,72 +381,72 @@ void setTransactionIsolationLevel() { @Test void errorPropagteRequestQueue() { illegalArgument(connection -> Flux.merge( - connection.createStatement("SELECT 'Result 1', SLEEP(1)").execute(), - connection.createStatement("SELECT 'Result 2'").execute(), - connection.createStatement("SELECT 'Result 3'").execute() - ).flatMap(result -> result.map((row, meta) -> row.get(0, Integer.class))) + connection.createStatement("SELECT 'Result 1', SLEEP(1)").execute(), + connection.createStatement("SELECT 'Result 2'").execute(), + connection.createStatement("SELECT 'Result 3'").execute() + ).flatMap(result -> result.map((row, meta) -> row.get(0, Integer.class))) ); } @Test void commitTransactionShouldRespectQueuedMessages() { final String tdl = "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))"; - complete(connection -> - Mono.from(connection.createStatement(tdl).execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .thenMany(Flux.merge( - connection.beginTransaction(), - connection.createStatement("INSERT INTO test VALUES (1, 'test1')") - .execute(), - connection.commitTransaction() - )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) - .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) - .flatMap(result -> - Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) - ) - .doOnNext(text -> assertThat(text).isEqualTo(1L)) + castedComplete(connection -> + Mono.from(connection.createStatement(tdl).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(Flux.merge( + connection.beginTransaction(), + connection.createStatement("INSERT INTO test VALUES (1, 'test1')") + .execute(), + connection.commitTransaction() + )) + .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) + .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) + .flatMap(result -> + Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) + ) + .doOnNext(text -> assertThat(text).isEqualTo(1L)) ); } @Test void rollbackTransactionShouldRespectQueuedMessages() { final String tdl = "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))"; - complete(connection -> - Mono.from(connection.createStatement(tdl).execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .thenMany(Flux.merge( - connection.beginTransaction(), - connection.createStatement("INSERT INTO test VALUES (1, 'test1')") - .execute(), - connection.rollbackTransaction() - )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) - .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) - .doOnNext(count -> assertThat(count).isEqualTo(0L))) + castedComplete(connection -> + Mono.from(connection.createStatement(tdl).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(Flux.merge( + connection.beginTransaction(), + connection.createStatement("INSERT INTO test VALUES (1, 'test1')") + .execute(), + connection.rollbackTransaction() + )) + .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) + .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) + .doOnNext(count -> assertThat(count).isEqualTo(0L))) ); } @Test void beginTransactionShouldRespectQueuedMessages() { final String tdl = "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))"; - complete(connection -> - Mono.from(connection.createStatement(tdl).execute()) - .flatMap(IntegrationTestSupport::extractRowsUpdated) - .then(Mono.from(connection.beginTransaction())) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) - .thenMany(Flux.merge( - connection.createStatement("INSERT INTO test VALUES (1, 'test1')").execute(), - connection.commitTransaction(), - connection.beginTransaction() - )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isTrue()) - .then(Mono.from(connection.rollbackTransaction())) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) - .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) - .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) - .doOnNext(count -> assertThat(count).isEqualTo(1L))) + castedComplete(connection -> + Mono.from(connection.createStatement(tdl).execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .then(Mono.from(connection.beginTransaction())) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .thenMany(Flux.merge( + connection.createStatement("INSERT INTO test VALUES (1, 'test1')").execute(), + connection.commitTransaction(), + connection.beginTransaction() + )) + .doOnComplete(() -> assertThat(connection.isInTransaction()).isTrue()) + .then(Mono.from(connection.rollbackTransaction())) + .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) + .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) + .doOnNext(count -> assertThat(count).isEqualTo(1L))) ); } @@ -593,6 +598,10 @@ void batchCrud() { }); } + private void castedComplete(Function> runner) { + complete(conn -> runner.apply((MySqlSimpleConnection) conn)); + } + private static String formattedSelect(String condition) { if (condition.isEmpty()) { return "SELECT id,value FROM test ORDER BY id"; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java index 66fb46e5a..efdeac68d 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/InitDbIntegrationTest.java @@ -1,5 +1,6 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import org.junit.jupiter.api.Test; import java.util.concurrent.ThreadLocalRandom; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java index fb83de493..5e5d046c0 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; import io.r2dbc.spi.R2dbcBadGrammarException; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Result; @@ -61,7 +62,7 @@ void illegalArgument(Function> runner) { process(runner).expectError(IllegalArgumentException.class).verify(Duration.ofSeconds(3)); } - Mono create() { + Mono create() { return connectionFactory.create(); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java index 79a851693..10e138607 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java @@ -18,12 +18,15 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.json.JacksonCodecRegistrar; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; -import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -81,7 +84,7 @@ void json() { .verifyComplete(); } - private static Publisher insert(MySqlConnection connection) { + private static Flux insert(MySqlConnection connection) { MySqlStatement statement = connection.createStatement("INSERT INTO test VALUES (DEFAULT, ?)"); for (int i = 0; i < BARS.length; ++i) { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java index dcd9fd482..00d192de3 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MariaDbIntegrationTestSupport.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; import io.r2dbc.spi.Readable; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java similarity index 91% rename from r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java index 2cec69c8f..9fa2395b8 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java @@ -35,9 +35,9 @@ import static org.mockito.Mockito.when; /** - * Unit tests for {@link MySqlConnection}. + * Unit tests for {@link MySqlSimpleConnection}. */ -class MySqlConnectionTest { +class MySqlSimpleConnectionTest { private final Client client = mock(Client.class); @@ -47,20 +47,20 @@ class MySqlConnectionTest { private final String product = "MockConnection"; - private final MySqlConnection noPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), + private final MySqlSimpleConnection noPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), codecs, level, 50, Caches.createQueryCache(0), Caches.createPrepareCache(0), product, null); @Test void createStatement() { String condition = "SELECT * FROM test"; - MySqlConnection allPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), + MySqlSimpleConnection allPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), codecs, level, 50, Caches.createQueryCache(0), Caches.createPrepareCache(0), product, sql -> true); - MySqlConnection halfPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), + MySqlSimpleConnection halfPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), codecs, level, 50, Caches.createQueryCache(0), Caches.createPrepareCache(0), product, sql -> false); - MySqlConnection conditionPrepare = new MySqlConnection(client, ConnectionContextTest.mock(), + MySqlSimpleConnection conditionPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), codecs, level, 50, Caches.createQueryCache(0), Caches.createPrepareCache(0), product, sql -> sql.equals(condition)); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java deleted file mode 100644 index 7ff20cb9d..000000000 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTransactionDefinitionTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.r2dbc.spi.IsolationLevel; -import io.r2dbc.spi.TransactionDefinition; -import org.junit.jupiter.api.Test; - -import java.time.Duration; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Unit tests for {@link MySqlTransactionDefinition}. - */ -class MySqlTransactionDefinitionTest { - - @Test - void builder() { - Duration lockWaitTimeout = Duration.ofSeconds(118); - Long sessionId = 123456789L; - MySqlTransactionDefinition definition = MySqlTransactionDefinition.builder() - .isolationLevel(IsolationLevel.READ_COMMITTED) - .lockWaitTimeout(lockWaitTimeout) - .withConsistentSnapshot(true) - .consistentSnapshotEngine(ConsistentSnapshotEngine.ROCKSDB) - .consistentSnapshotFromSession(sessionId) - .build(); - - assertThat(definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) - .isSameAs(IsolationLevel.READ_COMMITTED); - assertThat(definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) - .isSameAs(lockWaitTimeout); - assertThat(definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT)) - .isTrue(); - assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE)) - .isSameAs(ConsistentSnapshotEngine.ROCKSDB); - assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_FROM_SESSION)) - .isSameAs(sessionId); - } - - @Test - void mutate() { - Duration lockWaitTimeout = Duration.ofSeconds(118); - MySqlTransactionDefinition def1 = MySqlTransactionDefinition.builder() - .isolationLevel(IsolationLevel.SERIALIZABLE) - .lockWaitTimeout(lockWaitTimeout) - .readOnly(true) - .build(); - MySqlTransactionDefinition def2 = def1.mutate() - .isolationLevel(IsolationLevel.READ_COMMITTED) - .build(); - - assertThat(def1).isNotEqualTo(def2); - assertThat(def1.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) - .isSameAs(def2.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) - .isSameAs(lockWaitTimeout); - assertThat(def1.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) - .isSameAs(IsolationLevel.SERIALIZABLE); - assertThat(def2.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) - .isSameAs(IsolationLevel.READ_COMMITTED); - assertThat(def1.getAttribute(TransactionDefinition.READ_ONLY)) - .isSameAs(def2.getAttribute(TransactionDefinition.READ_ONLY)) - .isEqualTo(true); - } -} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadataTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadataTest.java new file mode 100644 index 000000000..0edff7805 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTypeMetadataTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + +import io.asyncer.r2dbc.mysql.collation.CharCollation; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link MySqlTypeMetadata}. + */ +class MySqlTypeMetadataTest { + + @Test + void allSet() { + MySqlTypeMetadata metadata = new MySqlTypeMetadata(0, -1, 0); + + assertThat(metadata.isBinary()).isTrue(); + assertThat(metadata.isSet()).isTrue(); + assertThat(metadata.isUnsigned()).isTrue(); + assertThat(metadata.isEnum()).isTrue(); + assertThat(metadata.isNotNull()).isTrue(); + } + + @Test + void noSet() { + MySqlTypeMetadata metadata = new MySqlTypeMetadata(0, 0, 0); + + assertThat(metadata.isBinary()).isFalse(); + assertThat(metadata.isSet()).isFalse(); + assertThat(metadata.isUnsigned()).isFalse(); + assertThat(metadata.isEnum()).isFalse(); + assertThat(metadata.isNotNull()).isFalse(); + } + + @Test + void isBinaryUsesCollationId() { + MySqlTypeMetadata metadata = new MySqlTypeMetadata(0, -1, CharCollation.BINARY_ID); + + assertThat(metadata.isBinary()).isTrue(); + + metadata = new MySqlTypeMetadata(0, -1, 33); + assertThat(metadata.isBinary()).isFalse(); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java index 25041c282..fda842503 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java @@ -17,8 +17,10 @@ package io.asyncer.r2dbc.mysql; import com.fasterxml.jackson.core.type.TypeReference; -import io.r2dbc.spi.Connection; -import io.r2dbc.spi.Result; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.api.MySqlRow; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import org.jetbrains.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIf; @@ -634,12 +636,12 @@ private static JsonNode parseJson(String json) { } } - private static Flux extractFirstInteger(Result result) { + private static Flux extractFirstInteger(MySqlResult result) { return Flux.from(result.map((row, metadata) -> row.get(0, Integer.class))); } @SuppressWarnings("unchecked") - private static Flux> extractOptionalField(Result result, Type type) { + private static Flux> extractOptionalField(MySqlResult result, Type type) { if (type instanceof Class) { return Flux.from(result.map((row, metadata) -> Optional.ofNullable(row.get(0, (Class) type)))); } @@ -647,7 +649,7 @@ private static Flux> extractOptionalField(Result result, Type ty Optional.ofNullable(((MySqlRow) row).get(0, (ParameterizedType) type)))); } - private static Mono testTimeDuration(Connection connection, Duration origin, LocalTime time) { + private static Mono testTimeDuration(MySqlConnection connection, Duration origin, LocalTime time) { return Mono.from(connection.createStatement("INSERT INTO test VALUES(DEFAULT,?)") .bind(0, origin) .returnGeneratedValues("id") diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java index 853b11809..7313be994 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlConnection; import io.asyncer.r2dbc.mysql.constant.SslMode; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java index 14a878d91..132195cbb 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StartTransactionStateTest.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.TransactionDefinition; import org.junit.jupiter.params.ParameterizedTest; @@ -40,32 +41,26 @@ void buildStartTransaction(TransactionDefinition definition, String excepted) { static Stream buildStartTransaction() { return Stream.of( Arguments.of(MySqlTransactionDefinition.empty(), "BEGIN"), - Arguments.of(MySqlTransactionDefinition.builder() - .isolationLevel(IsolationLevel.READ_UNCOMMITTED) - .build(), "BEGIN"), - Arguments.of(MySqlTransactionDefinition.builder() - .readOnly(true) - .build(), "START TRANSACTION READ ONLY"), - Arguments.of(MySqlTransactionDefinition.builder() - .readOnly(false) - .build(), "START TRANSACTION READ WRITE"), - Arguments.of(MySqlTransactionDefinition.builder() - .withConsistentSnapshot(true) - .build(), "START TRANSACTION WITH CONSISTENT SNAPSHOT"), - Arguments.of(MySqlTransactionDefinition.builder() - .withConsistentSnapshot(true) - .readOnly(true) - .build(), "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY"), - Arguments.of(MySqlTransactionDefinition.builder() - .withConsistentSnapshot(true) - .readOnly(false) - .build(), "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE"), - Arguments.of(MySqlTransactionDefinition.builder() - .withConsistentSnapshot(true) - .consistentSnapshotEngine(ConsistentSnapshotEngine.ROCKSDB) - .consistentSnapshotFromSession(3L) - .readOnly(true) - .build(), "START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT FROM SESSION 3, READ ONLY") - ); + Arguments.of(MySqlTransactionDefinition.from(IsolationLevel.READ_UNCOMMITTED), "BEGIN"), + Arguments.of(MySqlTransactionDefinition.mutability(false), "START TRANSACTION READ ONLY"), + Arguments.of(MySqlTransactionDefinition.mutability(true), "START TRANSACTION READ WRITE"), + Arguments.of( + MySqlTransactionDefinition.empty().consistent(), + "START TRANSACTION WITH CONSISTENT SNAPSHOT" + ), + Arguments.of( + MySqlTransactionDefinition.mutability(false).consistent(), + "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY" + ), + Arguments.of( + MySqlTransactionDefinition.mutability(true).consistent(), + "START TRANSACTION WITH CONSISTENT SNAPSHOT, READ WRITE" + ), + Arguments.of( + MySqlTransactionDefinition.mutability(false) + .consistent("ROCKSDB", 3L), + "START TRANSACTION WITH CONSISTENT ROCKSDB SNAPSHOT FROM SESSION 3, READ ONLY" + ) + ); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java index 6732b6ec4..0cab3c695 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java @@ -16,6 +16,7 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlStatement; import org.junit.jupiter.api.Test; import java.util.NoSuchElementException; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java index 0d302494c..99da15e3c 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java @@ -1,6 +1,7 @@ package io.asyncer.r2dbc.mysql; import com.zaxxer.hikari.HikariDataSource; +import io.asyncer.r2dbc.mysql.api.MySqlResult; import org.assertj.core.data.TemporalUnitOffset; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinitionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinitionTest.java new file mode 100644 index 000000000..46fa69ef0 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/api/MySqlTransactionDefinitionTest.java @@ -0,0 +1,172 @@ +/* + * Copyright 2024 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.api; + +import io.asyncer.r2dbc.mysql.ConsistentSnapshotEngine; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.TransactionDefinition; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Duration; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link MySqlTransactionDefinition}. + */ +class MySqlTransactionDefinitionTest { + + @Test + void getAttribute() { + Duration lockWaitTimeout = Duration.ofSeconds(118); + long sessionId = 123456789L; + MySqlTransactionDefinition definition = MySqlTransactionDefinition.from(IsolationLevel.READ_COMMITTED) + .lockWaitTimeout(lockWaitTimeout) + .consistent("ROCKSDB", sessionId); + + assertThat(definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(IsolationLevel.READ_COMMITTED); + assertThat(definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isSameAs(lockWaitTimeout); + assertThat(definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT)) + .isTrue(); + assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_ENGINE)) + .isEqualTo("ROCKSDB"); + assertThat(definition.getAttribute(MySqlTransactionDefinition.CONSISTENT_SNAPSHOT_FROM_SESSION)) + .isEqualTo(sessionId); + } + + @Test + void isolationLevel() { + Duration lockWaitTimeout = Duration.ofSeconds(118); + MySqlTransactionDefinition def1 = MySqlTransactionDefinition.mutability(false) + .isolationLevel(IsolationLevel.SERIALIZABLE) + .lockWaitTimeout(lockWaitTimeout); + MySqlTransactionDefinition def2 = def1.isolationLevel(IsolationLevel.READ_COMMITTED); + + assertThat(def1).isNotEqualTo(def2); + assertThat(def1.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isSameAs(def2.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isSameAs(lockWaitTimeout); + assertThat(def1.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(IsolationLevel.SERIALIZABLE); + assertThat(def2.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(IsolationLevel.READ_COMMITTED); + assertThat(def1.getAttribute(TransactionDefinition.READ_ONLY)) + .isSameAs(def2.getAttribute(TransactionDefinition.READ_ONLY)) + .isEqualTo(true); + } + + @ParameterizedTest + @MethodSource + void withoutIsolationLevel(MySqlTransactionDefinition definition, IsolationLevel level) { + assertThat(definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isSameAs(level); + assertThat(definition.withoutIsolationLevel().getAttribute(TransactionDefinition.ISOLATION_LEVEL)) + .isNull(); + } + + @ParameterizedTest + @MethodSource + void withoutLockWaitTimeout(MySqlTransactionDefinition definition, @Nullable Duration lockWaitTimeout) { + assertThat(definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isEqualTo(lockWaitTimeout); + assertThat(definition.withoutLockWaitTimeout().getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT)) + .isNull(); + } + + @ParameterizedTest + @MethodSource + void withoutMutability(MySqlTransactionDefinition definition, @Nullable Boolean readOnly) { + assertThat(definition.getAttribute(TransactionDefinition.READ_ONLY)) + .isEqualTo(readOnly); + assertThat(definition.withoutMutability().getAttribute(TransactionDefinition.READ_ONLY)) + .isNull(); + } + + @ParameterizedTest + @MethodSource + void withoutConsistent(MySqlTransactionDefinition definition) { + assertThat(definition.getAttribute(MySqlTransactionDefinition.WITH_CONSISTENT_SNAPSHOT)) + .isTrue(); + assertThat(definition) + .isNotEqualTo(MySqlTransactionDefinition.empty()) + .extracting(MySqlTransactionDefinition::withoutConsistent) + .isEqualTo(MySqlTransactionDefinition.empty()); + } + + static Stream withoutIsolationLevel() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), null), + Arguments.of( + MySqlTransactionDefinition.from(IsolationLevel.READ_COMMITTED), + IsolationLevel.READ_COMMITTED + ), + Arguments.of( + MySqlTransactionDefinition.from(IsolationLevel.SERIALIZABLE) + .lockWaitTimeout(Duration.ofSeconds(118)), + IsolationLevel.SERIALIZABLE + ) + ); + } + + static Stream withoutLockWaitTimeout() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), null), + Arguments.of( + MySqlTransactionDefinition.empty() + .lockWaitTimeout(Duration.ofSeconds(118)), + Duration.ofSeconds(118) + ), + Arguments.of( + MySqlTransactionDefinition.empty() + .lockWaitTimeout(Duration.ofSeconds(123)) + .consistent("ROCKSDB", 123456789), + Duration.ofSeconds(123) + ) + ); + } + + static Stream withoutMutability() { + return Stream.of( + Arguments.of(MySqlTransactionDefinition.empty(), null), + Arguments.of(MySqlTransactionDefinition.mutability(true), false), + Arguments.of(MySqlTransactionDefinition.mutability(false), true), + Arguments.of(MySqlTransactionDefinition.mutability(true).consistent(), false), + Arguments.of(MySqlTransactionDefinition.mutability(false).consistent(), true), + Arguments.of( + MySqlTransactionDefinition.mutability(true) + .isolationLevel(IsolationLevel.SERIALIZABLE), + false + ) + ); + } + + static Stream withoutConsistent() { + return Stream.of( + MySqlTransactionDefinition.empty().consistent(), + MySqlTransactionDefinition.empty().consistent("ROCKSDB"), + MySqlTransactionDefinition.empty().consistent("ROCKSDB", 123456789), + MySqlTransactionDefinition.empty().consistentFromSession(123456789) + ); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java index 87d097a86..fefdb2415 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecsTest.java @@ -17,8 +17,7 @@ package io.asyncer.r2dbc.mysql.codec; import io.asyncer.r2dbc.mysql.ConnectionContextTest; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; -import io.asyncer.r2dbc.mysql.MySqlTypeMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.message.FieldValue; @@ -73,21 +72,11 @@ public String getName() { return "mock"; } - @Override - public MySqlTypeMetadata getNativeTypeMetadata() { - return null; - } - @Override public CharCollation getCharCollation(CodecContext context) { return CharCollation.fromId(CharCollation.BINARY_ID, context); } - @Override - public long getNativePrecision() { - return 0; - } - @Override public Nullability getNullability() { return Nullability.NULLABLE; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java index 7faade837..bf17a6dc9 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/Decoding.java @@ -16,8 +16,7 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; -import io.asyncer.r2dbc.mysql.MySqlTypeMetadata; +import io.asyncer.r2dbc.mysql.api.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; @@ -71,11 +70,6 @@ public String getName() { return "mock"; } - @Override - public MySqlTypeMetadata getNativeTypeMetadata() { - return null; - } - @Override public Nullability getNullability() { return Nullability.NON_NULL; @@ -86,9 +80,5 @@ public CharCollation getCharCollation(CodecContext context) { return context.getClientCollation(); } - @Override - public long getNativePrecision() { - return 0; - } } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java index dbaeeb928..9954721d2 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java @@ -17,11 +17,12 @@ package io.asyncer.r2dbc.mysql.json; import com.fasterxml.jackson.databind.ObjectMapper; -import io.asyncer.r2dbc.mysql.MySqlColumnMetadata; import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.CodecContext; import io.asyncer.r2dbc.mysql.codec.ParametrizedCodec; +import io.asyncer.r2dbc.mysql.collation.CharCollation; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -52,7 +53,7 @@ public JacksonCodec(Mode mode) { } @Override - public Object decode(ByteBuf value, MySqlColumnMetadata metadata, Class target, boolean binary, + public Object decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { Charset charset = metadata.getCharCollation(context).getCharset(); @@ -64,7 +65,7 @@ public Object decode(ByteBuf value, MySqlColumnMetadata metadata, Class targe } @Override - public Object decode(ByteBuf value, MySqlColumnMetadata metadata, ParameterizedType target, boolean binary, + public Object decode(ByteBuf value, MySqlReadableMetadata metadata, ParameterizedType target, boolean binary, CodecContext context) { Charset charset = metadata.getCharCollation(context).getCharset(); @@ -81,12 +82,12 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canDecode(MySqlColumnMetadata metadata, Class target) { + public boolean canDecode(MySqlReadableMetadata metadata, Class target) { return doCanDecode(metadata); } @Override - public boolean canDecode(MySqlColumnMetadata metadata, ParameterizedType target) { + public boolean canDecode(MySqlReadableMetadata metadata, ParameterizedType target) { return doCanDecode(metadata); } @@ -95,7 +96,7 @@ public boolean canEncode(Object value) { return mode.isEncode(); } - private boolean doCanDecode(MySqlColumnMetadata metadata) { + private boolean doCanDecode(MySqlReadableMetadata metadata) { return mode.isDecode() && (metadata.getType() == MySqlType.JSON || metadata.getType() == MySqlType.TEXT); } From dcbd67ab6147b09f0dd2463db81cb934eb0e6f89 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 5 Mar 2024 11:46:02 +0900 Subject: [PATCH 33/93] Upgrade dependencies --- r2dbc-mysql/pom.xml | 48 +++++++++++++++++++++++++++------------ test-native-image/pom.xml | 4 ++-- 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 225353812..6a0b4194b 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -75,18 +75,19 @@ false 1.0.0.RELEASE - 2022.0.9 - 3.24.2 + 2022.0.16 + 4.1.106.Final + 3.25.3 1.37 - 5.10.1 - 1.4.14 + 5.10.2 + 1.5.3 4.11.0 - 8.2.0 - 1.19.3 + 8.3.0 + 1.19.6 4.0.3 - 5.3.31 - 2.16.0 - 0.3.0.RELEASE + 5.3.32 + 2.16.1 + 0.4.0.RELEASE 3.0.2 1.5.5-11 24.1.0 @@ -102,6 +103,13 @@ pom import + + io.netty + netty-bom + ${netty.version} + pom + import + org.junit junit-bom @@ -145,7 +153,19 @@ io.projectreactor.netty - reactor-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 @@ -305,7 +325,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.11.0 + 3.12.1 -Xlint:all @@ -379,7 +399,7 @@ org.apache.maven.plugins maven-surefire-plugin - 3.2.3 + 3.2.5 random @@ -395,7 +415,7 @@ org.apache.maven.plugins maven-failsafe-plugin - 3.2.3 + 3.2.5 @@ -488,7 +508,7 @@ org.codehaus.mojo exec-maven-plugin - 3.1.1 + 3.2.0 run-benchmarks diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 4d055475d..f74a74a20 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -15,9 +15,9 @@ 8 true - 2022.0.9 + 2022.0.16 1.0.0.RELEASE - 21.2.0 + 20.3.13 From 9cd241730bb8f7bdb4570f2571a9012e1beea8d0 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 7 Mar 2024 21:20:54 +0900 Subject: [PATCH 34/93] Revise `RowMetaData#getColumnMetadata(String)` (#257) Motivation: The current behavior does not align with the specification Modifications: implement the intended behavior as per the specification. Result: Correct behavior --- .../io/asyncer/r2dbc/mysql/ColumnNameSet.java | 216 ------------------ .../r2dbc/mysql/InsertSyntheticRow.java | 16 +- .../r2dbc/mysql/MySqlColumnDescriptor.java | 4 +- .../io/asyncer/r2dbc/mysql/MySqlNames.java | 145 ------------ .../r2dbc/mysql/MySqlRowDescriptor.java | 85 +++---- .../asyncer/r2dbc/mysql/MySqlNamesTest.java | 120 ---------- .../r2dbc/mysql/MySqlRowDescriptorTest.java | 65 ++++++ 7 files changed, 110 insertions(+), 541 deletions(-) delete mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java delete mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java delete mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java deleted file mode 100644 index b15de4bc9..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ColumnNameSet.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; - -import java.util.AbstractSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.Comparator; -import java.util.Iterator; -import java.util.Objects; -import java.util.Set; -import java.util.Spliterator; -import java.util.Spliterators; -import java.util.function.Consumer; -import java.util.function.Predicate; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.require; -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; - -/** - * An implementation of {@link Set}{@code <}{@link String}{@code >} for {@code RowMetadata.getColumnNames} - * results. - * - * @see MySqlNames column name searching rules. - */ -final class ColumnNameSet extends AbstractSet implements Set { - - static final Comparator NAME_COMPARATOR = (left, right) -> - MySqlNames.compare(left.getName(), right.getName()); - - private final String[] originNames; - - private final String[] sortedNames; - - /** - * Construct a {@link ColumnNameSet} by sorted {@code names} without array copy. - * - * @param originNames must be the original order. - * @param sortedNames must be sorted by {@link MySqlNames#compare(String, String)}. - */ - private ColumnNameSet(String[] originNames, String[] sortedNames) { - this.originNames = originNames; - this.sortedNames = sortedNames; - } - - @Override - public boolean contains(Object o) { - if (o instanceof String) { - return findIndex((String) o) >= 0; - } - - return false; - } - - @Override - public Iterator iterator() { - return InternalArrays.asIterator(originNames); - } - - @Override - public int size() { - return originNames.length; - } - - @Override - public boolean isEmpty() { - return originNames.length == 0; - } - - @Override - public Spliterator spliterator() { - return Spliterators.spliterator(this.originNames, - Spliterator.NONNULL | Spliterator.ORDERED | Spliterator.IMMUTABLE); - } - - @Override - public void forEach(Consumer action) { - Objects.requireNonNull(action); - - for (String name : this.originNames) { - action.accept(name); - } - } - - @Override - public String[] toArray() { - return Arrays.copyOf(originNames, originNames.length); - } - - @SuppressWarnings({ "unchecked", "SuspiciousSystemArraycopy" }) - @Override - public T[] toArray(T[] a) { - Objects.requireNonNull(a); - - int size = originNames.length; - - if (a.length < size) { - return (T[]) Arrays.copyOf(originNames, size, a.getClass()); - } else { - System.arraycopy(originNames, 0, a, 0, size); - - if (a.length > size) { - a[size] = null; - } - - return a; - } - } - - @Override - public boolean add(String s) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean addAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.isEmpty()) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @Override - public boolean remove(Object o) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeIf(Predicate filter) { - Objects.requireNonNull(filter); - - for (String name : this.originNames) { - if (filter.test(name)) { - throw new UnsupportedOperationException(); - } - } - - return false; - } - - @Override - public boolean removeAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.isEmpty()) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @SuppressWarnings("SuspiciousMethodCalls") - @Override - public boolean retainAll(Collection c) { - Objects.requireNonNull(c); - - if (!c.containsAll(this)) { - throw new UnsupportedOperationException(); - } - - return false; - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } - - @Override - public String toString() { - return Arrays.toString(originNames); - } - - int findIndex(String name) { - return MySqlNames.nameSearch(this.sortedNames, name); - } - - String[] getSortedNames() { - return sortedNames; - } - - static ColumnNameSet of(String name) { - requireNonNull(name, "name must not be null"); - - String[] names = new String[] { name }; - return new ColumnNameSet(names, names); - } - - static ColumnNameSet of(String[] originNames, String[] sortedNames) { - requireNonNull(originNames, "originNames must not be null"); - requireNonNull(sortedNames, "sortedNames must not be null"); - require(originNames.length == sortedNames.length, - "The length of origin names the same as sorted names one"); - - return new ColumnNameSet(originNames, sortedNames); - } -} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java index a9865419c..bba48841c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InsertSyntheticRow.java @@ -52,15 +52,11 @@ final class InsertSyntheticRow implements MySqlRow, MySqlRowMetadata, MySqlColum 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 @@ -164,14 +160,14 @@ public T get(String name, ParameterizedType type) { lastInsertId < 0 ? Long.toUnsignedString(lastInsertId) : lastInsertId)); } - private void assertValidName(String name) { - if (!contains0(name)) { - throw new NoSuchElementException("Column name '" + name + "' does not exist in " + this.nameSet); - } + private boolean contains0(final String name) { + return keyName.equalsIgnoreCase(name); } - private boolean contains0(String name) { - return nameSet.contains(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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java index 5f3720c08..8f8d2e9e0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlColumnDescriptor.java @@ -23,6 +23,7 @@ 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; @@ -50,7 +51,8 @@ final class MySqlColumnDescriptor implements MySqlColumnMetadata { private final int collationId; - private MySqlColumnDescriptor(int index, short typeId, String name, int definitions, + @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"); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java deleted file mode 100644 index 29d85dfe3..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlNames.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -/** - * A utility considers column names searching logic which use a special compare rule for sort and special - * binary search. - * - *

  • Sort: compare with case insensitive first, then compare with case sensitive when they equals by - * case insensitive.
  • - *
  • Search: find with case sensitive first, then find with case insensitive when not found in case - * sensitive.
- *

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

- * Note: visible for unit tests. - * - * @param left the {@link String} of left. - * @param right the {@link String} of right. - * @return an integer of both difference. - */ - static int compare(String left, String right) { - return compare0(left, right, 0, right.length()); - } - - private static int compare0(String left, String right, int start, int end) { - int leftSize = left.length(), rightSize = end - start; - int minSize = Math.min(leftSize, rightSize); - // Case sensitive comparator result. - int csCompared = 0; - char leftCh, rightCh; - - for (int i = 0; i < minSize; i++) { - leftCh = left.charAt(i); - rightCh = right.charAt(i + start); - - if (leftCh != rightCh) { - if (csCompared == 0) { - // Compare end if is case-sensitive comparator. - csCompared = leftCh - rightCh; - } - - // Use `Character.toLowerCase` for all latin alphabets, not just ASCII. - leftCh = Character.toLowerCase(leftCh); - rightCh = Character.toLowerCase(rightCh); - - if (leftCh != rightCh) { - // Not equals even case-insensitive. - return leftCh < rightCh ? -4 : 4; - } - } - } - - // Length not equals means both strings not equals even case-insensitive. - if (leftSize != rightSize) { - return leftSize < rightSize ? -4 : 4; - } - - // Equals when case-insensitive, use case-sensitive. - return csCompared < 0 ? -2 : (csCompared > 0 ? 2 : 0); - } - - private MySqlNames() { } -} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java index 1b5311da6..a86d6a9ab 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptor.java @@ -19,54 +19,34 @@ import io.asyncer.r2dbc.mysql.api.MySqlRowMetadata; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.message.server.DefinitionMetadataMessage; +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 MySqlRowMetadata} for MySQL database text/binary results. - * - * @see MySqlNames column name searching rules. */ final class MySqlRowDescriptor implements MySqlRowMetadata { private final MySqlColumnDescriptor[] originMetadata; - private final MySqlColumnDescriptor[] sortedMetadata; - - private final ColumnNameSet nameSet; - - private MySqlRowDescriptor(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,8 +104,7 @@ public List getColumnMetadatas() { @Override public String toString() { - return "MySqlRowDescriptor{metadata=" + Arrays.toString(originMetadata) + ", sortedNames=" + - Arrays.toString(nameSet.getSortedNames()) + '}'; + return "MySqlRowDescriptor{metadata=" + Arrays.toString(originMetadata) + '}'; } MySqlColumnDescriptor[] unwrap() { @@ -123,15 +121,4 @@ static MySqlRowDescriptor create(DefinitionMetadataMessage[] columns) { return new MySqlRowDescriptor(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; - } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java deleted file mode 100644 index ce4f27387..000000000 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlNamesTest.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import org.jetbrains.annotations.Nullable; -import org.junit.jupiter.api.Test; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.TreeSet; -import java.util.function.Consumer; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Unit tests for {@link MySqlNames}. - */ -class MySqlNamesTest { - - private static final String[] NAMES = { "c", "dD", "cBc", "Dca", "ADC", "DcA", "abc", "b", "B", "dA", - "AB", "a", "Abc", "ABC", "A", "ab", "cc", "Da", "CbC" }; - - private static final String[] SINGLETON = { "name" }; - - private static final Set CS_NAME_SET = new HashSet<>(); - - private static final Set CI_NAME_SET = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); - - static { - CS_NAME_SET.addAll(Arrays.asList(NAMES)); - CI_NAME_SET.addAll(Arrays.asList(NAMES)); - Arrays.sort(NAMES, MySqlNames::compare); - } - - @Test - void nameSearch() { - Consumer judge = name -> { - if (CS_NAME_SET.contains(name)) { - assertEquals(NAMES[MySqlNames.nameSearch(NAMES, name)], name); - assertEquals(NAMES[MySqlNames.nameSearch(NAMES, String.format("`%s`", name))], name); - } else if (CI_NAME_SET.contains(name)) { - assertTrue(NAMES[MySqlNames.nameSearch(NAMES, name)].equalsIgnoreCase(name)); - assertEquals(MySqlNames.nameSearch(NAMES, String.format("`%s`", name)), -1); - } else { - assertEquals(MySqlNames.nameSearch(NAMES, name), -1); - assertEquals(MySqlNames.nameSearch(NAMES, String.format("`%s`", name)), -1); - } - }; - nameGenerate(1, judge); - nameGenerate(2, judge); - nameGenerate(3, judge); - - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "name")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "Name")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "nAMe")], "name"); - assertEquals(SINGLETON[MySqlNames.nameSearch(SINGLETON, "`name`")], "name"); - assertEquals(MySqlNames.nameSearch(SINGLETON, "`Name`"), -1); - assertEquals(MySqlNames.nameSearch(SINGLETON, "`nAMe`"), -1); - } - - /** - * A full-arrangement of repeatable selections is generated in 'a' - 'd' and 'A' - 'D' of fixed length - * String. - *

- * e.g. input: 2, publish: aa ab ac ad aA aB aC ... DB DC DD - */ - private static void nameGenerate(int length, Consumer nameConsumer) { - nameGen0(length, null, nameConsumer); - } - - private static void nameGen0(int length, @Nullable String prefix, Consumer nameConsumer) { - if (length <= 1) { - for (char c = 'a'; c < 'e'; ++c) { - if (prefix == null) { - nameConsumer.accept(Character.toString(c)); - } else { - nameConsumer.accept(prefix + c); - } - } - for (char c = 'A'; c < 'E'; ++c) { - if (prefix == null) { - nameConsumer.accept(Character.toString(c)); - } else { - nameConsumer.accept(prefix + c); - } - } - } else { - for (char c = 'a'; c < 'e'; ++c) { - if (prefix == null) { - nameGen0(length - 1, Character.toString(c), nameConsumer); - } else { - nameGen0(length - 1, prefix + c, nameConsumer); - } - } - for (char c = 'A'; c < 'E'; ++c) { - if (prefix == null) { - nameGen0(length - 1, Character.toString(c), nameConsumer); - } else { - nameGen0(length - 1, prefix + c, nameConsumer); - } - } - } - } -} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java new file mode 100644 index 000000000..aceff07ad --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlRowDescriptorTest.java @@ -0,0 +1,65 @@ +package io.asyncer.r2dbc.mysql; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.NoSuchElementException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + + +class MySqlRowDescriptorTest { + @ParameterizedTest + @MethodSource("arguments") + public void findColumnByNameTest(final String q, final int index, final String... names) { + + // given + final MySqlRowDescriptor metadata = create(names); + + // when + if (index == -1) { + assertThrows(NoSuchElementException.class, () -> metadata.getColumnMetadata(q)); + return; + } + final MySqlColumnDescriptor actual = metadata.getColumnMetadata(q); + + // then + assertEquals(index, actual.getIndex()); + } + + private static Stream arguments() { + return Stream.of( + // not found + Arguments.of("`alpha`", -1, + new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("omega", -1, + new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + // found + Arguments.of("alpha", 0, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Alpha", 0, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("beta", 1, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Beta", 1, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("delta", 3, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Delta", 3, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + + Arguments.of("gamma", 4, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }), + Arguments.of("Gamma", 4, new String[] { "alpha", "beta", "Alpha", "DELTA", "gamma", "Gamma", "delta" }) + ); + } + + private static MySqlRowDescriptor create(final String... names) { + MySqlColumnDescriptor[] metadata = new MySqlColumnDescriptor[names.length]; + for (int i = 0; i < names.length; ++i) { + metadata[i] = + new MySqlColumnDescriptor(i, (short) 0, names[i], 0, 0, 0, 1); + } + return new MySqlRowDescriptor(metadata); + } + + +} From d801e8f72f3c63c6b7fcb5aeb08f6d3ec8688c8f Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Tue, 12 Mar 2024 17:42:59 +0900 Subject: [PATCH 35/93] Add support for ignore lock wait timeout --- .../r2dbc/mysql/ConnectionContext.java | 18 ++++ .../r2dbc/mysql/MySqlSimpleConnection.java | 82 +++++++++++------ .../io/asyncer/r2dbc/mysql/QueryFlow.java | 90 ++++++++++--------- 3 files changed, 121 insertions(+), 69 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index 47ac71076..1b43dcdbc 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -53,6 +53,8 @@ public final class ConnectionContext implements CodecContext { @Nullable private ZoneId timeZone; + private boolean lockWaitTimeoutSupported = false; + /** * 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. @@ -162,6 +164,22 @@ public int getLocalInfileBufferSize() { return localInfileBufferSize; } + /** + * Checks if the server supports lock wait timeout. + * + * @return if the server supports lock wait timeout. + */ + public boolean isLockWaitTimeoutSupported() { + return lockWaitTimeoutSupported; + } + + /** + * Enables lock wait timeout supported when loading session variables. + */ + public void enableLockWaitTimeoutSupported() { + this.lockWaitTimeoutSupported = true; + } + /** * Get the bitmap of server statuses. * 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 index f4a2b3746..6e609073c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java @@ -205,7 +205,7 @@ public Mono beginTransaction() { @Override public Mono beginTransaction(TransactionDefinition definition) { - return Mono.defer(() -> QueryFlow.beginTransaction(client, this, batchSupported, definition)); + return Mono.defer(() -> QueryFlow.beginTransaction(client, this, batchSupported, definition, context)); } @Override @@ -306,8 +306,8 @@ public MySqlConnectionMetadata getMetadata() { } /** - * MySQL does not have any way to query the isolation level of the current transaction, only inferred from - * past statements, so driver can not make sure the result is right. + * MySQL does not have any way to query the isolation level of the current transaction, only inferred from past + * statements, so driver can not make sure the result is right. *

* See MySQL Bug 53341 *

@@ -424,6 +424,11 @@ public boolean isInTransaction() { public Mono setLockWaitTimeout(Duration timeout) { requireNonNull(timeout, "timeout must not be null"); + if (!context.isLockWaitTimeoutSupported()) { + logger.warn("Lock wait timeout is not supported by server, setLockWaitTimeout operation is ignored"); + return Mono.empty(); + } + long timeoutSeconds = timeout.getSeconds(); return QueryFlow.executeVoid(client, "SET innodb_lock_wait_timeout=" + timeoutSeconds) .doOnSuccess(ignored -> this.lockWaitTimeout = this.currentLockWaitTimeout = timeoutSeconds); @@ -484,6 +489,7 @@ static Mono init( ) { Mono connection = initSessionVariables(client, sessionVariables) .then(loadSessionVariables(client, codecs, context)) + .flatMap(data -> loadInnoDbEngineStatus(data, client, codecs, context)) .map(data -> { ZoneId timeZone = data.timeZone; if (timeZone != null) { @@ -491,6 +497,12 @@ static Mono init( context.setTimeZone(timeZone); } + if (data.lockWaitTimeoutSupported) { + context.enableLockWaitTimeoutSupported(); + } else { + logger.info("Lock wait timeout is not supported by server, all related operations will be ignored"); + } + return new MySqlSimpleConnection(client, context, codecs, data.level, data.lockWaitTimeout, queryCache, prepareCache, data.product, prepare); }); @@ -534,10 +546,10 @@ private static Mono initSessionVariables(Client client, List sessi private static Mono loadSessionVariables( Client client, Codecs codecs, ConnectionContext context ) { - StringBuilder query = new StringBuilder(160) + StringBuilder query = new StringBuilder(128) .append("SELECT ") .append(transactionIsolationColumn(context)) - .append(",@@innodb_lock_wait_timeout AS l,@@version_comment AS v"); + .append(",@@version_comment AS v"); Function> handler; @@ -554,6 +566,24 @@ private static Mono loadSessionVariables( .last(); } + private static Mono loadInnoDbEngineStatus( + SessionData data, Client client, Codecs codecs, ConnectionContext context + ) { + return new TextSimpleStatement(client, codecs, context, + "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(Long.parseLong(value)); + } + })) + .single(data); + } + private static Mono initDatabase(Client client, String database) { return client.exchange(new InitDbMessage(database), INIT_DB) .last() @@ -572,16 +602,15 @@ private static Mono initDatabase(Client client, String database) { private static Flux convertSessionData(MySqlResult r, boolean timeZone) { return r.map(readable -> { IsolationLevel level = convertIsolationLevel(readable.get(0, String.class)); - long lockWaitTimeout = convertLockWaitTimeout(readable.get(1, Long.class)); - String product = readable.get(2, String.class); + String product = readable.get(1, String.class); - return new SessionData(level, lockWaitTimeout, product, timeZone ? readZoneId(readable) : null); + return new SessionData(level, product, timeZone ? readZoneId(readable) : null); }); } private static ZoneId readZoneId(Readable readable) { - String systemTimeZone = readable.get(3, String.class); - String timeZone = readable.get(4, String.class); + 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()) { @@ -628,24 +657,13 @@ private static IsolationLevel convertIsolationLevel(@Nullable String name) { return IsolationLevel.REPEATABLE_READ; } - private static long convertLockWaitTimeout(@Nullable Long timeout) { - if (timeout == null) { - logger.error("Lock wait timeout is null, fallback to " + DEFAULT_LOCK_WAIT_TIMEOUT + " seconds"); - - return DEFAULT_LOCK_WAIT_TIMEOUT; - } - - return timeout; - } - /** - * Resolves the column of session isolation level, the {@literal @@tx_isolation} has been marked as - * deprecated. + * 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). + * 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(); @@ -664,20 +682,26 @@ private static final class SessionData { private final IsolationLevel level; - private final long lockWaitTimeout; - @Nullable private final String product; @Nullable private final ZoneId timeZone; - private SessionData(IsolationLevel level, long lockWaitTimeout, @Nullable String product, - @Nullable ZoneId timeZone) { + private long lockWaitTimeout = -1; + + private boolean lockWaitTimeoutSupported; + + private SessionData(IsolationLevel level, @Nullable String product, @Nullable ZoneId timeZone) { this.level = level; - this.lockWaitTimeout = lockWaitTimeout; this.product = product; this.timeZone = timeZone; } + + SessionData lockWaitTimeout(long timeout) { + this.lockWaitTimeoutSupported = true; + this.lockWaitTimeout = timeout; + return this; + } } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 7b100cd24..f5911dcb8 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -107,10 +107,10 @@ final class QueryFlow { }; /** - * 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. @@ -133,10 +133,10 @@ static Flux> execute(Client client, String sql, List> 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. @@ -172,9 +172,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. @@ -195,8 +195,8 @@ 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}. + * 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. @@ -219,10 +219,9 @@ static Mono login(Client client, SslMode sslMode, String database, Strin } /** - * 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. * @@ -246,18 +245,19 @@ 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 ConnectionState}. * * @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}. + * @param context the {@link ConnectionContext} for initialization. * @return receives complete signal. */ static Mono beginTransaction(Client client, ConnectionState state, boolean batchSupported, - TransactionDefinition definition) { - final StartTransactionState startState = new StartTransactionState(state, definition); + TransactionDefinition definition, ConnectionContext context) { + final StartTransactionState startState = new StartTransactionState(state, definition, context); if (batchSupported) { return client.exchange(new TransactionBatchExchangeable(startState)).then(); @@ -267,8 +267,8 @@ 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 ConnectionState} in the initial + * connection state. * * @param client the {@link Client} to exchange messages with. * @param state the connection state for checks and resets transaction statuses. @@ -298,9 +298,9 @@ static Mono createSavepoint(Client client, ConnectionState state, String n /** * Execute a simple query statement. Query execution terminates with the last {@link CompleteMessage} or a - * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed - * by {@link CompleteMessage} after receive the last result for the last binding. The exchange will be - * completed by {@link CompleteMessage} after receive the last result for the last binding. + * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed by + * {@link CompleteMessage} after receive the last result for the last binding. The exchange will be completed by + * {@link CompleteMessage} after receive the last result for the last binding. * * @param client the {@link Client} to exchange messages with. * @param sql the query to execute, can be contains multi-statements. @@ -310,7 +310,8 @@ private static Flux execute0(Client client, String sql) { return client.exchange(new SimpleQueryExchangeable(sql)); } - private QueryFlow() { } + private QueryFlow() { + } } /** @@ -523,12 +524,12 @@ protected String offendingSql() { } /** - * An implementation of {@link FluxExchangeable} that considers server-preparing queries. Which contains a - * built-in state machine. + * An implementation of {@link FluxExchangeable} that considers server-preparing queries. Which contains a built-in + * state machine. *

- * It will reset a prepared statement if cache has matched it, otherwise it will prepare statement to a new - * statement ID and put the ID into the cache. If the statement ID does not exist in the cache after the last - * row sent, the ID will be closed. + * It will reset a prepared statement if cache has matched it, otherwise it will prepare statement to a new statement ID + * and put the ID into the cache. If the statement ID does not exist in the cache after the last row sent, the ID will + * be closed. */ final class PrepareExchangeable extends FluxExchangeable { @@ -813,8 +814,8 @@ private void onCompleteMessage(CompleteMessage message, SynchronousSink - * Not like other {@link FluxExchangeable}s, it is started by a server-side message, which should be an - * implementation of {@link HandshakeRequest}. + * Not like other {@link FluxExchangeable}s, it is started by a server-side message, which should be an implementation + * of {@link HandshakeRequest}. */ final class LoginExchangeable extends FluxExchangeable { @@ -1181,6 +1182,7 @@ boolean cancelTasks() { } if (state.isLockWaitTimeoutChanged()) { + // If server does not support lock wait timeout, the state will not be changed, so it is safe. tasks |= LOCK_WAIT_TIMEOUT; statements.add("SET innodb_lock_wait_timeout=" + state.getSessionLockWaitTimeout()); } @@ -1224,9 +1226,12 @@ final class StartTransactionState extends AbstractTransactionState { private final TransactionDefinition definition; - StartTransactionState(ConnectionState state, TransactionDefinition definition) { + private final ConnectionContext context; + + StartTransactionState(ConnectionState state, TransactionDefinition definition, ConnectionContext context) { super(state); this.definition = definition; + this.context = context; } @Override @@ -1237,9 +1242,14 @@ boolean cancelTasks() { } final Duration timeout = definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT); if (timeout != null) { - final long lockWaitTimeout = timeout.getSeconds(); - tasks |= LOCK_WAIT_TIMEOUT; - statements.add("SET innodb_lock_wait_timeout=" + lockWaitTimeout); + if (context.isLockWaitTimeoutSupported()) { + long lockWaitTimeout = timeout.getSeconds(); + tasks |= LOCK_WAIT_TIMEOUT; + statements.add("SET innodb_lock_wait_timeout=" + lockWaitTimeout); + } else { + QueryFlow.logger.warn( + "Lock wait timeout is not supported by server, transaction definition lockWaitTimeout is ignored"); + } } final IsolationLevel isolationLevel = definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL); From e54b69bd601bd360c2bca4d01eb5b6ef1dbb9243 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 14 Mar 2024 13:54:32 +0900 Subject: [PATCH 36/93] Reduce retained references to ConnectionContext - Reduce publicity - Prepare to support redirect and reconnect --- .../io/asyncer/r2dbc/mysql/Capability.java | 8 ++- .../r2dbc/mysql/ConnectionContext.java | 64 +++++++++--------- .../r2dbc/mysql/MySqlBatchingBatch.java | 7 +- .../r2dbc/mysql/MySqlConnectionFactory.java | 7 +- .../io/asyncer/r2dbc/mysql/MySqlDataRow.java | 8 ++- .../r2dbc/mysql/MySqlSegmentResult.java | 26 +++---- .../r2dbc/mysql/MySqlSimpleConnection.java | 67 +++++++++---------- .../r2dbc/mysql/MySqlStatementSupport.java | 12 ++-- .../r2dbc/mysql/MySqlSyntheticBatch.java | 7 +- .../mysql/ParametrizedStatementSupport.java | 11 ++- .../io/asyncer/r2dbc/mysql/PingStatement.java | 24 ++++--- .../mysql/PrepareParametrizedStatement.java | 7 +- .../r2dbc/mysql/PrepareSimpleStatement.java | 7 +- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 44 ++++++------ .../r2dbc/mysql/SimpleStatementSupport.java | 7 +- .../mysql/TextParametrizedStatement.java | 9 ++- .../r2dbc/mysql/TextSimpleStatement.java | 6 +- .../io/asyncer/r2dbc/mysql/client/Client.java | 30 ++++++--- .../mysql/client/MessageDuplexCodec.java | 3 + .../mysql/client/ReactorNettyClient.java | 5 ++ .../r2dbc/mysql/client/SslBridgeHandler.java | 3 + .../r2dbc/mysql/ConnectionContextTest.java | 6 +- .../r2dbc/mysql/MySqlBatchingBatchTest.java | 6 +- .../mysql/MySqlSimpleConnectionTest.java | 23 +++++-- .../r2dbc/mysql/MySqlSyntheticBatchTest.java | 3 +- .../PrepareParametrizedStatementTest.java | 8 ++- .../mysql/PrepareSimpleStatementTest.java | 8 ++- .../mysql/TextParametrizedStatementTest.java | 10 +-- .../r2dbc/mysql/TextSimpleStatementTest.java | 9 ++- 29 files changed, 226 insertions(+), 209 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java index 50b965ad4..26299a08b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Capability.java @@ -177,6 +177,11 @@ public final class Capability { TRANSACTIONS | SECURE_SALT | MULTI_STATEMENTS | MULTI_RESULTS | PS_MULTI_RESULTS | 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; /** @@ -373,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 { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index 1b43dcdbc..acf55812b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -30,17 +30,13 @@ /** * 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. + * 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 volatile int connectionId = -1; - - private volatile ServerVersion serverVersion = NONE_VERSION; - private final ZeroDateOption zeroDateOption; @Nullable @@ -50,20 +46,25 @@ public final class ConnectionContext implements CodecContext { private final boolean preserveInstants; + private int connectionId = -1; + + private ServerVersion serverVersion = NONE_VERSION; + + private Capability capability = Capability.DEFAULT; + @Nullable private ZoneId timeZone; private boolean lockWaitTimeoutSupported = false; /** - * 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. + * 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. + *

+ * It would be updated multiple times, so {@code volatile} is required. */ private volatile short serverStatuses = ServerStatuses.AUTO_COMMIT; - @Nullable - private volatile Capability capability = null; - ConnectionContext( ZeroDateOption zeroDateOption, @Nullable Path localInfilePath, @@ -78,15 +79,6 @@ public final class ConnectionContext implements CodecContext { this.timeZone = timeZone; } - /** - * Get the connection identifier that is specified by server. - * - * @return the connection identifier. - */ - public int getConnectionId() { - return connectionId; - } - /** * Initializes this context. * @@ -94,17 +86,30 @@ public int getConnectionId() { * @param version the server version. * @param capability the connection capabilities. */ - public void init(int connectionId, ServerVersion version, Capability capability) { + void init(int connectionId, ServerVersion version, Capability capability) { this.connectionId = connectionId; this.serverVersion = version; this.capability = capability; } + /** + * 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(); @@ -123,7 +128,7 @@ public ZoneId getTimeZone() { return timeZone; } - public boolean isTimeZoneInitialized() { + boolean isTimeZoneInitialized() { return timeZone != null; } @@ -133,9 +138,9 @@ public boolean isMariaDb() { return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb(); } - void setTimeZone(ZoneId timeZone) { + void initTimeZone(ZoneId timeZone) { if (isTimeZoneInitialized()) { - throw new IllegalStateException("Server timezone have been initialized"); + throw new IllegalStateException("Connection timezone have been initialized"); } this.timeZone = timeZone; } @@ -176,7 +181,7 @@ public boolean isLockWaitTimeoutSupported() { /** * Enables lock wait timeout supported when loading session variables. */ - public void enableLockWaitTimeoutSupported() { + void enableLockWaitTimeoutSupported() { this.lockWaitTimeoutSupported = true; } @@ -197,13 +202,4 @@ public short getServerStatuses() { public void setServerStatuses(short serverStatuses) { this.serverStatuses = serverStatuses; } - - /** - * Get the connection capability. Should use it after this context initialized. - * - * @return the connection capability. - */ - public Capability getCapability() { - return capability; - } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java index d85ebfd9e..a71c31986 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatch.java @@ -36,14 +36,11 @@ final class MySqlBatchingBatch implements MySqlBatch { 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 @@ -65,7 +62,7 @@ public MySqlBatch add(String sql) { @Override public Flux execute() { return QueryFlow.execute(client, getSql()) - .map(messages -> MySqlSegmentResult.toResult(false, codecs, context, null, messages)); + .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, null, messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index 9e269eda5..495f4ecec 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -151,7 +151,7 @@ private static Mono getMySqlConnection( final String user, final SslMode sslMode, final Set compressionAlgorithms, - final int zstdCompressionLevel, + final int zstdLevel, final ConnectionContext context, final Extensions extensions, final List sessionVariables, @@ -163,8 +163,7 @@ private static Mono getMySqlConnection( .flatMap(client -> { // Lazy init database after handshake/login String db = createDbIfNotExist ? "" : database; - return QueryFlow.login(client, sslMode, db, user, password, compressionAlgorithms, - zstdCompressionLevel, context); + return QueryFlow.login(client, sslMode, db, user, password, compressionAlgorithms, zstdLevel); }) .flatMap(client -> { ByteBufAllocator allocator = client.getByteBufAllocator(); @@ -175,7 +174,7 @@ private static Mono getMySqlConnection( extensions.forEach(CodecRegistrar.class, registrar -> registrar.register(allocator, builder)); - return MySqlSimpleConnection.init(client, builder.build(), context, db, queryCache.get(), + return MySqlSimpleConnection.init(client, builder.build(), db, queryCache.get(), prepareCache, sessionVariables, prepare); }); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java index a9e56b747..05add4758 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlDataRow.java @@ -18,6 +18,7 @@ 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; @@ -42,10 +43,13 @@ final class MySqlDataRow implements MySqlRow { */ 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; MySqlDataRow(FieldValue[] fields, MySqlRowDescriptor rowMetadata, Codecs codecs, boolean binary, - ConnectionContext context) { + 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"); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java index d38f6cd91..3aafb1a3e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSegmentResult.java @@ -18,6 +18,7 @@ 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; @@ -53,8 +54,8 @@ /** * 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}. */ final class MySqlSegmentResult implements MySqlResult { @@ -156,15 +157,15 @@ public Flux flatMap(Function 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 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 { @@ -269,9 +270,9 @@ private static final class MySqlSegments implements BiConsumer 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(); @@ -322,7 +322,7 @@ public void accept(ServerMessage message, SynchronousSink sink) { } 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 index 6e609073c..e59a70c6d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java @@ -64,8 +64,6 @@ final class MySqlSimpleConnection implements MySqlConnection, ConnectionState { private static final InternalLogger logger = InternalLoggerFactory.getInstance(MySqlSimpleConnection.class); - private static final int DEFAULT_LOCK_WAIT_TIMEOUT = 50; - private static final String PING_MARKER = "/* ping */"; private static final ServerVersion MARIA_11_1_1 = ServerVersion.create(11, 1, 1, true); @@ -139,8 +137,6 @@ final class MySqlSimpleConnection implements MySqlConnection, ConnectionState { private final boolean batchSupported; - private final ConnectionContext context; - private final MySqlConnectionMetadata metadata; private volatile IsolationLevel sessionLevel; @@ -174,11 +170,12 @@ final class MySqlSimpleConnection implements MySqlConnection, ConnectionState { */ private volatile long currentLockWaitTimeout; - MySqlSimpleConnection(Client client, ConnectionContext context, Codecs codecs, IsolationLevel level, + MySqlSimpleConnection(Client client, Codecs codecs, IsolationLevel level, long lockWaitTimeout, QueryCache queryCache, PrepareCache prepareCache, @Nullable String product, @Nullable Predicate prepare) { + ConnectionContext context = client.getContext(); + this.client = client; - this.context = context; this.sessionLevel = level; this.currentLevel = level; this.codecs = codecs; @@ -205,7 +202,7 @@ public Mono beginTransaction() { @Override public Mono beginTransaction(TransactionDefinition definition) { - return Mono.defer(() -> QueryFlow.beginTransaction(client, this, batchSupported, definition, context)); + return Mono.defer(() -> QueryFlow.beginTransaction(client, this, batchSupported, definition)); } @Override @@ -227,9 +224,7 @@ public Mono commitTransaction() { @Override public MySqlBatch createBatch() { - return batchSupported ? new MySqlBatchingBatch(client, codecs, context) : - new MySqlSyntheticBatch(client, codecs, context); - + return batchSupported ? new MySqlBatchingBatch(client, codecs) : new MySqlSyntheticBatch(client, codecs); } @Override @@ -244,7 +239,7 @@ public MySqlStatement createStatement(String sql) { requireNonNull(sql, "sql must not be null"); if (sql.startsWith(PING_MARKER)) { - return new PingStatement(codecs, context, Flux.defer(this::doPingInternal)); + return new PingStatement(client, codecs); } Query query = queryCache.get(sql); @@ -252,22 +247,22 @@ public MySqlStatement createStatement(String sql) { if (query.isSimple()) { if (prepare != null && prepare.test(sql)) { logger.debug("Create a simple statement provided by prepare query"); - return new PrepareSimpleStatement(client, codecs, context, sql, prepareCache); + return new PrepareSimpleStatement(client, codecs, sql, prepareCache); } logger.debug("Create a simple statement provided by text query"); - return new TextSimpleStatement(client, codecs, context, sql); + return new TextSimpleStatement(client, codecs, sql); } if (prepare == null) { logger.debug("Create a parametrized statement provided by text query"); - return new TextParametrizedStatement(client, codecs, query, context); + return new TextParametrizedStatement(client, codecs, query); } logger.debug("Create a parametrized statement provided by prepare query"); - return new PrepareParametrizedStatement(client, codecs, query, context, prepareCache); + return new PrepareParametrizedStatement(client, codecs, query, prepareCache); } @Override @@ -355,7 +350,7 @@ public Mono validate(ValidationDepth depth) { return Mono.just(false); } - return doPingInternal() + return doPingInternal(client) .last() .map(VALIDATE) .onErrorResume(e -> { @@ -417,14 +412,14 @@ public void resetCurrentLockWaitTimeout() { @Override public boolean isInTransaction() { - return (context.getServerStatuses() & ServerStatuses.IN_TRANSACTION) != 0; + return (client.getContext().getServerStatuses() & ServerStatuses.IN_TRANSACTION) != 0; } @Override public Mono setLockWaitTimeout(Duration timeout) { requireNonNull(timeout, "timeout must not be null"); - if (!context.isLockWaitTimeoutSupported()) { + if (!client.getContext().isLockWaitTimeoutSupported()) { logger.warn("Lock wait timeout is not supported by server, setLockWaitTimeout operation is ignored"); return Mono.empty(); } @@ -437,6 +432,8 @@ public Mono setLockWaitTimeout(Duration timeout) { @Override public Mono setStatementTimeout(Duration timeout) { requireNonNull(timeout, "timeout must not be null"); + + final ConnectionContext context = client.getContext(); final boolean isMariaDb = context.isMariaDb(); final ServerVersion serverVersion = context.getServerVersion(); final long timeoutMs = timeout.toMillis(); @@ -461,12 +458,12 @@ public Mono setStatementTimeout(Duration timeout) { ); } - private Flux doPingInternal() { - return client.exchange(PingMessage.INSTANCE, PING); + private boolean isSessionAutoCommit() { + return (client.getContext().getServerStatuses() & ServerStatuses.AUTO_COMMIT) != 0; } - private boolean isSessionAutoCommit() { - return (context.getServerStatuses() & ServerStatuses.AUTO_COMMIT) != 0; + static Flux doPingInternal(Client client) { + return client.exchange(PingMessage.INSTANCE, PING); } /** @@ -474,7 +471,6 @@ private boolean isSessionAutoCommit() { * * @param client must be logged-in. * @param codecs the {@link Codecs}. - * @param context must be initialized. * @param database the database that should be lazy init. * @param queryCache the cache of {@link Query}. * @param prepareCache the cache of server-preparing result. @@ -483,18 +479,19 @@ private boolean isSessionAutoCommit() { * @return a {@link Mono} will emit an initialized {@link MySqlConnection}. */ static Mono init( - Client client, Codecs codecs, ConnectionContext context, String database, + Client client, Codecs codecs, String database, QueryCache queryCache, PrepareCache prepareCache, List sessionVariables, @Nullable Predicate prepare ) { Mono connection = initSessionVariables(client, sessionVariables) - .then(loadSessionVariables(client, codecs, context)) - .flatMap(data -> loadInnoDbEngineStatus(data, client, codecs, context)) + .then(loadSessionVariables(client, codecs)) + .flatMap(data -> loadInnoDbEngineStatus(data, client, codecs)) .map(data -> { + ConnectionContext context = client.getContext(); ZoneId timeZone = data.timeZone; if (timeZone != null) { logger.debug("Got server time zone {} from loading session variables", timeZone); - context.setTimeZone(timeZone); + context.initTimeZone(timeZone); } if (data.lockWaitTimeoutSupported) { @@ -503,7 +500,7 @@ static Mono init( logger.info("Lock wait timeout is not supported by server, all related operations will be ignored"); } - return new MySqlSimpleConnection(client, context, codecs, data.level, data.lockWaitTimeout, + return new MySqlSimpleConnection(client, codecs, data.level, data.lockWaitTimeout, queryCache, prepareCache, data.product, prepare); }); @@ -543,9 +540,8 @@ private static Mono initSessionVariables(Client client, List sessi return QueryFlow.executeVoid(client, query.toString()); } - private static Mono loadSessionVariables( - Client client, Codecs codecs, ConnectionContext context - ) { + private static Mono loadSessionVariables(Client client, Codecs codecs) { + ConnectionContext context = client.getContext(); StringBuilder query = new StringBuilder(128) .append("SELECT ") .append(transactionIsolationColumn(context)) @@ -560,17 +556,14 @@ private static Mono loadSessionVariables( handler = r -> convertSessionData(r, true); } - return new TextSimpleStatement(client, codecs, context, query.toString()) + return new TextSimpleStatement(client, codecs, query.toString()) .execute() .flatMap(handler) .last(); } - private static Mono loadInnoDbEngineStatus( - SessionData data, Client client, Codecs codecs, ConnectionContext context - ) { - return new TextSimpleStatement(client, codecs, context, - "SHOW VARIABLES LIKE 'innodb\\\\_lock\\\\_wait\\\\_timeout'") + private static Mono loadInnoDbEngineStatus(SessionData data, Client client, Codecs codecs) { + 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); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java index d976b6155..5b40500ee 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlStatementSupport.java @@ -17,6 +17,7 @@ package io.asyncer.r2dbc.mysql; import io.asyncer.r2dbc.mysql.api.MySqlStatement; +import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import org.jetbrains.annotations.Nullable; @@ -32,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) { @@ -71,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; } @@ -85,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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java index efc677beb..73640a82e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatch.java @@ -37,14 +37,11 @@ final class MySqlSyntheticBatch implements MySqlBatch { 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 @@ -56,7 +53,7 @@ public MySqlBatch add(String sql) { @Override public Flux execute() { return QueryFlow.execute(client, statements) - .map(messages -> MySqlSegmentResult.toResult(false, codecs, context, null, messages)); + .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, null, messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java index 41ea8e465..bb37b31ec 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java @@ -41,8 +41,6 @@ */ abstract class ParametrizedStatementSupport extends MySqlStatementSupport { - protected final Client client; - protected final Codecs codecs; protected final Query query; @@ -51,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); + ParametrizedStatementSupport(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()); @@ -75,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; } @@ -84,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; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java index f8b1936e2..e64aec930 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java @@ -18,25 +18,23 @@ 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.message.server.ServerMessage; 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 Codecs codecs; - - private final ConnectionContext context; + private final Client client; - private final Flux deferred; + private final Codecs codecs; - PingStatement(Codecs codecs, ConnectionContext context, Flux deferred) { + PingStatement(Client client, Codecs codecs) { + this.client = client; this.codecs = codecs; - this.context = context; - this.deferred = deferred; } @Override @@ -66,8 +64,12 @@ public MySqlStatement bindNull(String name, Class type) { @Override public Flux execute() { - return Flux.just( - MySqlSegmentResult.toResult(false, codecs, context, null, deferred) - ); + return Flux.from(Mono.fromSupplier(() -> MySqlSegmentResult.toResult( + false, + client, + codecs, + null, + MySqlSimpleConnection.doPingInternal(client) + ))); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java index 9395a1309..d3f898e89 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java @@ -37,9 +37,8 @@ final class PrepareParametrizedStatement extends ParametrizedStatementSupport { private int fetchSize = 0; - PrepareParametrizedStatement(Client client, Codecs codecs, Query query, ConnectionContext context, - PrepareCache prepareCache) { - super(client, codecs, query, context); + PrepareParametrizedStatement(Client client, Codecs codecs, Query query, PrepareCache prepareCache) { + super(client, codecs, query); this.prepareCache = prepareCache; } @@ -49,7 +48,7 @@ public Flux execute(List bindings) { StringUtils.extendReturning(query.getFormattedSql(), returningIdentifiers()), bindings, fetchSize, prepareCache )) - .map(messages -> MySqlSegmentResult.toResult(true, codecs, context, syntheticKeyName(), messages)); + .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java index d037eda39..d78bb3488 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java @@ -40,9 +40,8 @@ final class PrepareSimpleStatement extends SimpleStatementSupport { private int fetchSize = 0; - PrepareSimpleStatement(Client client, Codecs codecs, ConnectionContext context, String sql, - PrepareCache prepareCache) { - super(client, codecs, context, sql); + PrepareSimpleStatement(Client client, Codecs codecs, String sql, PrepareCache prepareCache) { + super(client, codecs, sql); this.prepareCache = prepareCache; } @@ -50,7 +49,7 @@ final class PrepareSimpleStatement extends SimpleStatementSupport { public Flux execute() { return Flux.defer(() -> QueryFlow.execute(client, StringUtils.extendReturning(sql, returningIdentifiers()), BINDINGS, fetchSize, prepareCache)) - .map(messages -> MySqlSegmentResult.toResult(true, codecs, context, syntheticKeyName(), messages)); + .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages)); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index f5911dcb8..8204955ac 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -210,10 +210,9 @@ static Flux> execute(Client client, List statements) */ static Mono login(Client client, SslMode sslMode, String database, String user, @Nullable CharSequence password, - Set compressionAlgorithms, int zstdCompressionLevel, - ConnectionContext context) { + Set compressionAlgorithms, int zstdCompressionLevel) { return client.exchange(new LoginExchangeable(client, sslMode, database, user, password, - compressionAlgorithms, zstdCompressionLevel, context)) + compressionAlgorithms, zstdCompressionLevel)) .onErrorResume(e -> client.forceClose().then(Mono.error(e))) .then(Mono.just(client)); } @@ -252,12 +251,11 @@ static Mono executeVoid(Client client, String sql) { * @param state the connection state for checks and sets transaction statuses. * @param batchSupported if connection supports batch query. * @param definition the {@link TransactionDefinition}. - * @param context the {@link ConnectionContext} for initialization. * @return receives complete signal. */ static Mono beginTransaction(Client client, ConnectionState state, boolean batchSupported, - TransactionDefinition definition, ConnectionContext context) { - final StartTransactionState startState = new StartTransactionState(state, definition, context); + TransactionDefinition definition) { + final StartTransactionState startState = new StartTransactionState(state, definition, client); if (batchSupported) { return client.exchange(new TransactionBatchExchangeable(startState)).then(); @@ -845,8 +843,6 @@ final class LoginExchangeable extends FluxExchangeable { private final int zstdCompressionLevel; - private final ConnectionContext context; - private boolean handshake = true; private MySqlAuthProvider authProvider; @@ -857,7 +853,7 @@ final class LoginExchangeable extends FluxExchangeable { LoginExchangeable(Client client, SslMode sslMode, String database, String user, @Nullable CharSequence password, Set compressions, - int zstdCompressionLevel, ConnectionContext context) { + int zstdCompressionLevel) { this.client = client; this.sslMode = sslMode; this.database = database; @@ -865,7 +861,6 @@ final class LoginExchangeable extends FluxExchangeable { this.password = password; this.compressions = compressions; this.zstdCompressionLevel = zstdCompressionLevel; - this.context = context; this.sslCompleted = sslMode == SslMode.TUNNEL; } @@ -889,7 +884,7 @@ public void accept(ServerMessage message, SynchronousSink sink) { Capability capability = initHandshake(request); if (capability.isSslEnabled()) { - emitNext(SslRequest.from(capability, context.getClientCollation().getId()), sink); + emitNext(SslRequest.from(capability, client.getContext().getClientCollation().getId()), sink); } else { emitNext(createHandshakeResponse(capability), sink); } @@ -906,14 +901,14 @@ public void accept(ServerMessage message, SynchronousSink sink) { sink.complete(); } else if (message instanceof SyntheticSslResponseMessage) { sslCompleted = true; - emitNext(createHandshakeResponse(context.getCapability()), sink); + 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", - context.getConnectionId()); + client.getContext().getConnectionId()); } emitNext(createAuthResponse("full authentication"), sink); @@ -952,7 +947,7 @@ private AuthResponse createAuthResponse(String phase) { throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), phase), CLI_SPECIFIC); } - return new AuthResponse(authProvider.authentication(password, salt, context.getClientCollation())); + return new AuthResponse(authProvider.authentication(password, salt, client.getContext().getClientCollation())); } private Capability clientCapability(Capability serverCapability) { @@ -969,8 +964,9 @@ private Capability clientCapability(Capability serverCapability) { } else if (!serverCapability.isSslEnabled()) { // Server unsupported SSL. if (sslMode.requireSsl()) { - throw new R2dbcPermissionDeniedException("Server version '" + context.getServerVersion() + - "' does not support SSL but mode '" + sslMode + "' requires SSL", CLI_SPECIFIC); + // 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(); @@ -1012,7 +1008,7 @@ private Capability clientCapability(Capability serverCapability) { builder.disableConnectWithDatabase(); } - if (context.getLocalInfilePath() == null) { + if (client.getContext().getLocalInfilePath() == null) { builder.disableLoadDataLocalInfile(); } @@ -1036,7 +1032,7 @@ private Capability initHandshake(HandshakeRequest message) { Capability capability = clientCapability(message.getServerCapability()); // No need initialize server statuses because it has initialized by read filter. - this.context.init(header.getConnectionId(), serverVersion, capability); + this.client.getContext().init(header.getConnectionId(), serverVersion, capability); this.authProvider = MySqlAuthProvider.build(message.getAuthType()); this.salt = message.getSalt(); @@ -1057,7 +1053,7 @@ private HandshakeResponse createHandshakeResponse(Capability capability) { CLI_SPECIFIC); } - byte[] authorization = authProvider.authentication(password, salt, context.getClientCollation()); + byte[] authorization = authProvider.authentication(password, salt, client.getContext().getClientCollation()); String authType = authProvider.getType(); if (MySqlAuthProvider.NO_AUTH_PROVIDER.equals(authType)) { @@ -1066,7 +1062,7 @@ private HandshakeResponse createHandshakeResponse(Capability capability) { authType = MySqlAuthProvider.CACHING_SHA2_PASSWORD; } - return HandshakeResponse.from(capability, context.getClientCollation().getId(), user, authorization, + return HandshakeResponse.from(capability, client.getContext().getClientCollation().getId(), user, authorization, authType, database, ATTRIBUTES, zstdCompressionLevel); } @@ -1226,12 +1222,12 @@ final class StartTransactionState extends AbstractTransactionState { private final TransactionDefinition definition; - private final ConnectionContext context; + private final Client client; - StartTransactionState(ConnectionState state, TransactionDefinition definition, ConnectionContext context) { + StartTransactionState(ConnectionState state, TransactionDefinition definition, Client client) { super(state); this.definition = definition; - this.context = context; + this.client = client; } @Override @@ -1242,7 +1238,7 @@ boolean cancelTasks() { } final Duration timeout = definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT); if (timeout != null) { - if (context.isLockWaitTimeoutSupported()) { + if (client.getContext().isLockWaitTimeoutSupported()) { long lockWaitTimeout = timeout.getSeconds(); tasks |= LOCK_WAIT_TIMEOUT; statements.add("SET innodb_lock_wait_timeout=" + lockWaitTimeout); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java index 42ba279e3..78a6ec781 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/SimpleStatementSupport.java @@ -27,16 +27,13 @@ */ abstract class SimpleStatementSupport extends MySqlStatementSupport { - protected final Client client; - protected final Codecs codecs; protected final String sql; - SimpleStatementSupport(Client client, Codecs codecs, ConnectionContext context, String sql) { - super(context); + SimpleStatementSupport(Client client, Codecs codecs, String sql) { + super(client); - this.client = requireNonNull(client, "client must not be null"); this.codecs = requireNonNull(codecs, "codecs must not be null"); this.sql = requireNonNull(sql, "sql must not be null"); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java index 88a10d1a1..109fb7ba6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java @@ -28,14 +28,13 @@ */ final class TextParametrizedStatement extends ParametrizedStatementSupport { - TextParametrizedStatement(Client client, Codecs codecs, Query query, ConnectionContext context) { - super(client, codecs, query, context); + TextParametrizedStatement(Client client, Codecs codecs, Query query) { + super(client, codecs, query); } @Override protected Flux execute(List bindings) { - return Flux.defer(() -> QueryFlow.execute(client, query, returningIdentifiers(), - bindings)) - .map(messages -> MySqlSegmentResult.toResult(false, codecs, context, syntheticKeyName(), messages)); + return Flux.defer(() -> QueryFlow.execute(client, query, returningIdentifiers(), bindings)) + .map(messages -> MySqlSegmentResult.toResult(false, client, codecs, syntheticKeyName(), messages)); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java index a265f7af2..f1e0f7083 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextSimpleStatement.java @@ -27,8 +27,8 @@ */ final class TextSimpleStatement extends SimpleStatementSupport { - TextSimpleStatement(Client client, Codecs codecs, ConnectionContext context, String sql) { - super(client, codecs, context, sql); + TextSimpleStatement(Client client, Codecs codecs, String sql) { + super(client, codecs, sql); } @Override @@ -36,6 +36,6 @@ public Flux execute() { return Flux.defer(() -> QueryFlow.execute( client, StringUtils.extendReturning(sql, returningIdentifiers()) - ).map(messages -> MySqlSegmentResult.toResult(false, codecs, context, syntheticKeyName(), messages))); + ).map(messages -> MySqlSegmentResult.toResult(false, client, codecs, syntheticKeyName(), messages))); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java index bf2b8a219..d7c3ac28a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java @@ -46,21 +46,21 @@ public interface Client { InternalLogger logger = InternalLoggerFactory.getInstance(Client.class); /** - * Perform an exchange of a request message. Calling this method while a previous exchange is active will - * return a deferred handle and queue the request until the previous exchange terminates. + * Perform an exchange of a request message. Calling this method while a previous exchange is active will return a + * deferred handle and queue the request until the previous exchange terminates. * * @param request one and only one request message for get server responses - * @param handler response handler, {@link SynchronousSink#complete()} should be called after the last - * response frame is sent to complete the stream and prevent multiple subscribers from - * consuming previous, active response streams + * @param handler response handler, {@link SynchronousSink#complete()} should be called after the last response + * frame is sent to complete the stream and prevent multiple subscribers from consuming previous, + * active response streams * @param handling response type * @return A {@link Flux} of incoming messages that ends with the end of the frame */ Flux exchange(ClientMessage request, BiConsumer> handler); /** - * Perform an exchange of multi-request messages. Calling this method while a previous exchange is active - * will return a deferred handle and queue the request until the previous exchange terminates. + * Perform an exchange of multi-request messages. Calling this method while a previous exchange is active will + * return a deferred handle and queue the request until the previous exchange terminates. * * @param exchangeable request messages and response handler * @param handling response type @@ -91,6 +91,14 @@ public interface Client { */ ByteBufAllocator getByteBufAllocator(); + /** + * Returns the current {@link ConnectionContext}. It should not be retained long-term as it may change on reconnects + * or redirects. + * + * @return the {@link ConnectionContext} + */ + ConnectionContext getContext(); + /** * Checks if the connection is open. * @@ -117,20 +125,20 @@ public interface Client { * @param tcpNoDelay if enable the {@link ChannelOption#TCP_NODELAY} * @param context the connection context * @param connectTimeout connect timeout, or {@code null} if it has no timeout - * @param loopResources the loop resources to use + * @param loopResources the loop resources to use * @return A {@link Mono} that will emit a connected {@link Client}. * @throws IllegalArgumentException if {@code ssl}, {@code address} or {@code context} is {@code null}. * @throws ArithmeticException if {@code connectTimeout} milliseconds overflow as an int */ static Mono connect(MySqlSslConfiguration ssl, SocketAddress address, boolean tcpKeepAlive, - boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout, - LoopResources loopResources) { + boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout, + LoopResources loopResources) { requireNonNull(ssl, "ssl must not be null"); requireNonNull(address, "address must not be null"); requireNonNull(context, "context must not be null"); TcpClient tcpClient = TcpClient.newConnection() - .runOn(loopResources); + .runOn(loopResources); if (connectTimeout != null) { tcpClient = tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java index 09f231b67..8fb3d47e1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/MessageDuplexCodec.java @@ -67,6 +67,9 @@ final class MessageDuplexCodec extends ByteToMessageDecoder implements ChannelOu private DecodeContext decodeContext = DecodeContext.login(); + /** + * It can be retained because reconnect and redirect will re-create the {@link MessageDuplexCodec}. + */ private final ConnectionContext context; private final ServerMessageDecoder decoder = new ServerMessageDecoder(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index b9a12f3cc..81cb5f21e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -240,6 +240,11 @@ public ByteBufAllocator getByteBufAllocator() { return connection.outbound().alloc(); } + @Override + public ConnectionContext getContext() { + return context; + } + @Override public boolean isConnected() { return state < ST_CLOSED && connection.channel().isOpen(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java index 9d5166132..ce3361fd4 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java @@ -77,6 +77,9 @@ final class SslBridgeHandler extends ChannelDuplexHandler { private static final ServerVersion MYSQL_5_7_28 = ServerVersion.create(5, 7, 28); + /** + * It can be retained because reconnect and redirect will re-create the {@link SslBridgeHandler}. + */ private final ConnectionContext context; private final MySqlSslConfiguration ssl; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java index 7e98e5d6c..5e2be6114 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java @@ -46,15 +46,15 @@ void getTimeZone() { void setTwiceTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, 8192, true, null); - context.setTimeZone(ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.setTimeZone(ZoneId.systemDefault())); + context.initTimeZone(ZoneId.systemDefault()); + assertThatIllegalStateException().isThrownBy(() -> context.initTimeZone(ZoneId.systemDefault())); } @Test void badSetTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, 8192, true, ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.setTimeZone(ZoneId.systemDefault())); + assertThatIllegalStateException().isThrownBy(() -> context.initTimeZone(ZoneId.systemDefault())); } public static ConnectionContext mock() { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java index 2eaab1e9b..ef764b18d 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlBatchingBatchTest.java @@ -29,8 +29,7 @@ */ class MySqlBatchingBatchTest { - private final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class), - ConnectionContextTest.mock()); + private final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class)); @Test void add() { @@ -62,8 +61,7 @@ void badAdd() { @Test void addNothing() { - final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class), - ConnectionContextTest.mock()); + final MySqlBatchingBatch batch = new MySqlBatchingBatch(mock(Client.class), mock(Codecs.class)); assertEquals(batch.getSql(), ""); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java index 9fa2395b8..9a21c84d6 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java @@ -39,7 +39,7 @@ */ class MySqlSimpleConnectionTest { - private final Client client = mock(Client.class); + private final Client client; private final Codecs codecs = mock(Codecs.class); @@ -47,20 +47,29 @@ class MySqlSimpleConnectionTest { private final String product = "MockConnection"; - private final MySqlSimpleConnection noPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, null); + private final MySqlSimpleConnection noPrepare; + + MySqlSimpleConnectionTest() { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock()); + + this.client = client; + this.noPrepare = new MySqlSimpleConnection(client, + codecs, level, 50, Caches.createQueryCache(0), + Caches.createPrepareCache(0), product, null); + } @Test void createStatement() { String condition = "SELECT * FROM test"; - MySqlSimpleConnection allPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), + MySqlSimpleConnection allPrepare = new MySqlSimpleConnection(client, codecs, level, 50, Caches.createQueryCache(0), Caches.createPrepareCache(0), product, sql -> true); - MySqlSimpleConnection halfPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), + MySqlSimpleConnection halfPrepare = new MySqlSimpleConnection(client, codecs, level, 50, Caches.createQueryCache(0), Caches.createPrepareCache(0), product, sql -> false); - MySqlSimpleConnection conditionPrepare = new MySqlSimpleConnection(client, ConnectionContextTest.mock(), + MySqlSimpleConnection conditionPrepare = new MySqlSimpleConnection(client, codecs, level, 50, Caches.createQueryCache(0), Caches.createPrepareCache(0), product, sql -> sql.equals(condition)); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java index c381c8418..ebcb4589e 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSyntheticBatchTest.java @@ -28,8 +28,7 @@ */ class MySqlSyntheticBatchTest { - private final MySqlSyntheticBatch batch = new MySqlSyntheticBatch(mock(Client.class), mock(Codecs.class), - ConnectionContextTest.mock()); + private final MySqlSyntheticBatch batch = new MySqlSyntheticBatch(mock(Client.class), mock(Codecs.class)); @SuppressWarnings("ConstantConditions") @Test diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java index c44bc5b28..f49d476e0 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java @@ -23,14 +23,13 @@ import java.lang.reflect.Field; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link PrepareParametrizedStatement}. */ class PrepareParametrizedStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); - private final Codecs codecs = Codecs.builder().build(); private final Field fetchSize = PrepareParametrizedStatement.class.getDeclaredField("fetchSize"); @@ -46,11 +45,14 @@ public int getFetchSize(PrepareParametrizedStatement statement) throws IllegalAc @Override public PrepareParametrizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + return new PrepareParametrizedStatement( client, codecs, Query.parse(sql), - ConnectionContextTest.mock(isMariaDB), Caches.createPrepareCache(0) ); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java index 947af752d..0e18e7233 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java @@ -23,14 +23,13 @@ import java.lang.reflect.Field; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link PrepareSimpleStatement}. */ class PrepareSimpleStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); - private final Codecs codecs = mock(Codecs.class); private final Field fetchSize = PrepareSimpleStatement.class.getDeclaredField("fetchSize"); @@ -61,10 +60,13 @@ public int getFetchSize(PrepareSimpleStatement statement) throws IllegalAccessEx @Override public PrepareSimpleStatement makeInstance(boolean isMariaDB, String ignored, String sql) { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + return new PrepareSimpleStatement( client, codecs, - ConnectionContextTest.mock(isMariaDB), sql, Caches.createPrepareCache(0) ); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java index 4e833369b..1175fe5db 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java @@ -20,14 +20,13 @@ import io.asyncer.r2dbc.mysql.codec.Codecs; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link TextParametrizedStatement}. */ class TextParametrizedStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); - private final Codecs codecs = Codecs.builder().build(); @Override @@ -37,11 +36,14 @@ public void fetchSize() { @Override public TextParametrizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + return new TextParametrizedStatement( client, codecs, - Query.parse(sql), - ConnectionContextTest.mock(isMariaDB) + Query.parse(sql) ); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java index 43cb1025c..5c74543b0 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextSimpleStatementTest.java @@ -20,14 +20,13 @@ import io.asyncer.r2dbc.mysql.codec.Codecs; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Unit tests for {@link TextSimpleStatement}. */ class TextSimpleStatementTest implements StatementTestSupport { - private final Client client = mock(Client.class); - private final Codecs codecs = mock(Codecs.class); @Override @@ -52,7 +51,11 @@ public void fetchSize() { @Override public TextSimpleStatement makeInstance(boolean isMariaDB, String ignored, String sql) { - return new TextSimpleStatement(client, codecs, ConnectionContextTest.mock(isMariaDB), sql); + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); + + return new TextSimpleStatement(client, codecs, sql); } @Override From 39e55e281d4d14a7c70e239f3b3d1ce8887b3c58 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Fri, 15 Mar 2024 12:56:57 +0900 Subject: [PATCH 37/93] Add lockWaitTimeout and statementTimeout options --- .../mysql/MySqlConnectionConfiguration.java | 73 +++++++++++++++++-- .../r2dbc/mysql/MySqlConnectionFactory.java | 14 +++- .../mysql/MySqlConnectionFactoryProvider.java | 6 ++ .../MySqlConnectionConfigurationTest.java | 3 + .../MySqlConnectionFactoryProviderTest.java | 6 +- .../mysql/SessionStateIntegrationTest.java | 54 ++++++++++++++ 6 files changed, 146 insertions(+), 10 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 5953495ce..c165cf4b7 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -101,6 +101,12 @@ public final class MySqlConnectionConfiguration { private final List sessionVariables; + @Nullable + private final Duration lockWaitTimeout; + + @Nullable + private final Duration statementTimeout; + @Nullable private final Path loadLocalInfilePath; @@ -130,7 +136,7 @@ private MySqlConnectionConfiguration( boolean forceConnectionTimeZoneToSession, String user, @Nullable CharSequence password, @Nullable String database, boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement, - List sessionVariables, + List sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout, @Nullable Path loadLocalInfilePath, int localInfileBufferSize, int queryCacheSize, int prepareCacheSize, Set compressionAlgorithms, int zstdCompressionLevel, @@ -154,6 +160,8 @@ private MySqlConnectionConfiguration( this.createDatabaseIfNotExist = createDatabaseIfNotExist; this.preferPrepareStatement = preferPrepareStatement; this.sessionVariables = sessionVariables; + this.lockWaitTimeout = lockWaitTimeout; + this.statementTimeout = statementTimeout; this.loadLocalInfilePath = loadLocalInfilePath; this.localInfileBufferSize = localInfileBufferSize; this.queryCacheSize = queryCacheSize; @@ -245,6 +253,16 @@ List getSessionVariables() { return sessionVariables; } + @Nullable + Duration getLockWaitTimeout() { + return lockWaitTimeout; + } + + @Nullable + Duration getStatementTimeout() { + return statementTimeout; + } + @Nullable Path getLoadLocalInfilePath() { return loadLocalInfilePath; @@ -309,6 +327,8 @@ public boolean equals(Object o) { 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 && @@ -325,9 +345,14 @@ public int hashCode() { return Objects.hash(isHost, domain, port, ssl, tcpKeepAlive, tcpNoDelay, connectTimeout, preserveInstants, connectionTimeZone, forceConnectionTimeZoneToSession, zeroDateOption, user, password, database, createDatabaseIfNotExist, - preferPrepareStatement, sessionVariables, loadLocalInfilePath, - localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, - zstdCompressionLevel, loopResources, extensions, passwordPublisher); + preferPrepareStatement, + sessionVariables, + lockWaitTimeout, + statementTimeout, + loadLocalInfilePath, localInfileBufferSize, + queryCacheSize, prepareCacheSize, + compressionAlgorithms, zstdCompressionLevel, + loopResources, extensions, passwordPublisher); } @Override @@ -343,6 +368,8 @@ public String toString() { ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + ", sessionVariables=" + sessionVariables + + ", lockWaitTimeout=" + lockWaitTimeout + + ", statementTimeout=" + statementTimeout + ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize + @@ -361,6 +388,8 @@ public String toString() { ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + ", sessionVariables=" + sessionVariables + + ", lockWaitTimeout=" + lockWaitTimeout + + ", statementTimeout=" + statementTimeout + ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + ", queryCacheSize=" + queryCacheSize + @@ -433,6 +462,12 @@ public static final class Builder { @Nullable private Predicate preferPrepareStatement; + @Nullable + private Duration lockWaitTimeout; + + @Nullable + private Duration statementTimeout; + private List sessionVariables = Collections.emptyList(); @Nullable @@ -486,7 +521,11 @@ public MySqlConnectionConfiguration build() { connectionTimeZone, forceConnectionTimeZoneToSession, user, password, database, - createDatabaseIfNotExist, preferPrepareStatement, sessionVariables, loadLocalInfilePath, + createDatabaseIfNotExist, preferPrepareStatement, + sessionVariables, + lockWaitTimeout, + statementTimeout, + loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, Extensions.from(extensions, autodetectExtensions), passwordPublisher); @@ -911,6 +950,30 @@ public Builder sessionVariables(String... 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. diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index 495f4ecec..6d76a8bed 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -174,8 +174,20 @@ private static Mono getMySqlConnection( extensions.forEach(CodecRegistrar.class, registrar -> registrar.register(allocator, builder)); - return MySqlSimpleConnection.init(client, builder.build(), db, queryCache.get(), + Mono c = MySqlSimpleConnection.init(client, builder.build(), db, queryCache.get(), prepareCache, sessionVariables, prepare); + + if (configuration.getLockWaitTimeout() != null) { + c = c.flatMap(connection -> connection.setLockWaitTimeout(configuration.getLockWaitTimeout()) + .thenReturn(connection)); + } + + if (configuration.getStatementTimeout() != null) { + c = c.flatMap(connection -> connection.setStatementTimeout(configuration.getStatementTimeout()) + .thenReturn(connection)); + } + + return c; }); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index 652bfd5fe..88771a372 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -42,9 +42,11 @@ 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; /** @@ -393,6 +395,10 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { 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); return builder.build(); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java index 717ecaa0f..f050f4e4a 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java @@ -250,6 +250,9 @@ private static MySqlConnectionConfiguration filledUp() { .sslHostnameVerifier((host, s) -> true) .queryCacheSize(128) .prepareCacheSize(0) + .sessionVariables("sql_mode=ANSI_QUOTES") + .lockWaitTimeout(Duration.ofSeconds(5)) + .statementTimeout(Duration.ofSeconds(10)) .autodetectExtensions(false) .build(); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index 41b2ef45a..ab75161c1 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -455,7 +455,7 @@ void validPasswordSupplier() { @Test void allConfigurationOptions() { - List exceptConfigs = Arrays.asList( + List exceptConfigs = Arrays.asList( "extendWith", "username", "zeroDateOption"); @@ -463,9 +463,7 @@ void allConfigurationOptions() { "driver", "ssl", "protocol", - "zeroDate", - "lockWaitTimeout", - "statementTimeout"); + "zeroDate"); Set allOptions = Stream.concat( Arrays.stream(ConnectionFactoryOptions.class.getFields()), Arrays.stream(MySqlConnectionFactoryProvider.class.getFields()) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java index a121374e9..d44608d55 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java @@ -17,7 +17,9 @@ package io.asyncer.r2dbc.mysql; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; +import io.r2dbc.spi.R2dbcTimeoutException; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -25,6 +27,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import java.time.Duration; import java.time.ZoneId; import java.util.LinkedHashMap; import java.util.Map; @@ -122,6 +125,57 @@ void sessionVariables(Map variables) { .verifyComplete(); } + @ParameterizedTest + @ValueSource(strings = { "PT1S", "PT10S", "PT1M" }) + void initLockWaitTimeout(String timeout) { + Duration lockWaitTimeout = Duration.parse(timeout); + + connectionFactory(builder -> builder.lockWaitTimeout(lockWaitTimeout)) + .create() + .flatMapMany(connection -> connection.createStatement("SELECT @@innodb_lock_wait_timeout").execute() + .flatMap(result -> result.map(r -> r.get(0, Long.class))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .expectNext(lockWaitTimeout.getSeconds()) + .verifyComplete(); + } + + @EnabledIf("isGreaterThanOrEqualToMariaDB10_1_1MySql5_7_4") + @ParameterizedTest + @ValueSource(strings = { "PT0.1S", "PT0.5S" }) + void initStatementTimeout(String timeout) { + Duration statementTimeout = Duration.parse(timeout); + + connectionFactory(builder -> builder.statementTimeout(statementTimeout)) + .create() + .flatMapMany(connection -> connection.createStatement("SELECT 1 WHERE SLEEP(1) > 1").execute() + .flatMap(result -> result.map(r -> r.get(0))) + .onErrorResume(e -> connection.close().then(Mono.error(e))) + .concatWith(connection.close().then(Mono.empty())) + ) + .as(StepVerifier::create) + .verifyError(R2dbcTimeoutException.class); + } + + static boolean isGreaterThanOrEqualToMariaDB10_1_1MySql5_7_4() { + String version = System.getProperty("test.mysql.version"); + + if (version == null || version.isEmpty()) { + return false; + } + + ServerVersion ver = ServerVersion.parse(version); + String type = System.getProperty("test.db.type"); + + if ("mariadb".equalsIgnoreCase(type)) { + return ver.isGreaterThanOrEqualTo(ServerVersion.create(10, 1, 1)); + } + + return ver.isGreaterThanOrEqualTo(ServerVersion.create(5, 7, 4)); + } + static Stream sessionVariables() { return Stream.of( Arguments.of(mapOf("sql_mode", "ANSI_QUOTES")), From 3e70b4ae8be2093f78cb88eff42b9f7cc53f89be Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Fri, 15 Mar 2024 14:47:57 +0900 Subject: [PATCH 38/93] Consistent spelling style --- .../java/io/asyncer/r2dbc/mysql/Binding.java | 6 ++-- .../mysql/MySqlConnectionConfiguration.java | 8 +++--- .../mysql/MySqlConnectionFactoryProvider.java | 8 +++--- .../r2dbc/mysql/MySqlSimpleConnection.java | 8 +++--- .../asyncer/r2dbc/mysql/ParameterWriter.java | 2 +- ...ava => ParameterizedStatementSupport.java} | 8 +++--- ...ava => PrepareParameterizedStatement.java} | 6 ++-- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 4 +-- ...t.java => TextParameterizedStatement.java} | 6 ++-- .../r2dbc/mysql/api/MySqlStatement.java | 4 +-- .../io/asyncer/r2dbc/mysql/codec/Codec.java | 2 +- .../r2dbc/mysql/codec/DefaultCodecs.java | 24 ++++++++-------- .../r2dbc/mysql/codec/LocalDateTimeCodec.java | 2 +- ...ec.java => MassiveParameterizedCodec.java} | 2 +- ...izedCodec.java => ParameterizedCodec.java} | 2 +- .../asyncer/r2dbc/mysql/codec/SetCodec.java | 2 +- .../r2dbc/mysql/codec/ZonedDateTimeCodec.java | 2 +- .../mysql/MySqlSimpleConnectionTest.java | 8 +++--- ...=> PrepareParameterizedStatementTest.java} | 14 +++++----- .../mysql/PrepareQueryIntegrationTest.java | 2 +- .../r2dbc/mysql/StatementTestSupport.java | 28 +++++++++---------- ...va => TextParameterizedStatementTest.java} | 8 +++--- .../r2dbc/mysql/TextQueryIntegrationTest.java | 2 +- .../r2dbc/mysql/json/JacksonCodec.java | 5 ++-- 24 files changed, 81 insertions(+), 82 deletions(-) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{ParametrizedStatementSupport.java => ParameterizedStatementSupport.java} (95%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{PrepareParametrizedStatement.java => PrepareParameterizedStatement.java} (86%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{TextParametrizedStatement.java => TextParameterizedStatement.java} (82%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/{MassiveParametrizedCodec.java => MassiveParameterizedCodec.java} (94%) rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/{ParametrizedCodec.java => ParameterizedCodec.java} (97%) rename r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/{PrepareParametrizedStatementTest.java => PrepareParameterizedStatementTest.java} (70%) rename r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/{TextParametrizedStatementTest.java => TextParameterizedStatementTest.java} (80%) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/Binding.java index a98b612d0..fc0166a09 100644 --- a/r2dbc-mysql/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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index c165cf4b7..3856b58bd 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -883,7 +883,7 @@ public Builder tcpNoDelay(boolean enabled) { } /** - * Configures 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. @@ -897,7 +897,7 @@ public Builder useClientPrepareStatement() { } /** - * Configures 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. @@ -910,7 +910,7 @@ public Builder useServerPrepareStatement() { } /** - * Configures 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 @@ -1025,7 +1025,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 diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index 88771a372..f6dc1a57a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -201,14 +201,14 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr Option.valueOf("createDatabaseIfNotExist"); /** - * Enable server preparing for parametrized statements and prefer server preparing simple statements. + * 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 - * parametrized statements and text query for simple statements. If it is {@code false}, driver will use - * client preparing for parametrized statements and text query for simple statements. + * 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 parametrized statements, it configures whether to prefer prepare execution on a + * 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. *

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 index e59a70c6d..660e25e06 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java @@ -256,13 +256,13 @@ public MySqlStatement createStatement(String sql) { } if (prepare == null) { - logger.debug("Create a parametrized statement provided by text query"); - return new TextParametrizedStatement(client, codecs, query); + logger.debug("Create a parameterized statement provided by text query"); + return new TextParameterizedStatement(client, codecs, query); } - logger.debug("Create a parametrized statement provided by prepare query"); + logger.debug("Create a parameterized statement provided by prepare query"); - return new PrepareParametrizedStatement(client, codecs, query, prepareCache); + return new PrepareParameterizedStatement(client, codecs, query, prepareCache); } @Override diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterWriter.java index fa52a1f69..590762bc4 100644 --- a/r2dbc-mysql/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 { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java similarity index 95% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java index bb37b31ec..2b1d64ed5 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParametrizedStatementSupport.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ParameterizedStatementSupport.java @@ -34,12 +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 use * {@link Query} to supports named parameters. */ -abstract class ParametrizedStatementSupport extends MySqlStatementSupport { +abstract class ParameterizedStatementSupport extends MySqlStatementSupport { protected final Codecs codecs; @@ -49,7 +49,7 @@ abstract class ParametrizedStatementSupport extends MySqlStatementSupport { private final AtomicBoolean executed = new AtomicBoolean(); - ParametrizedStatementSupport(Client client, Codecs codecs, Query query) { + ParameterizedStatementSupport(Client client, Codecs codecs, Query query) { super(client); requireNonNull(query, "query must not be null"); @@ -113,7 +113,7 @@ 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); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java similarity index 86% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java index d3f898e89..d9e290811 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java @@ -29,15 +29,15 @@ 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 { +final class PrepareParameterizedStatement extends ParameterizedStatementSupport { private final PrepareCache prepareCache; private int fetchSize = 0; - PrepareParametrizedStatement(Client client, Codecs codecs, Query query, PrepareCache prepareCache) { + PrepareParameterizedStatement(Client client, Codecs codecs, Query query, PrepareCache prepareCache) { super(client, codecs, query); this.prepareCache = prepareCache; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 8204955ac..e7a5de4bc 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -87,8 +87,8 @@ 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 { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParameterizedStatement.java similarity index 82% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParameterizedStatement.java index 109fb7ba6..84249cb03 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParametrizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/TextParameterizedStatement.java @@ -24,11 +24,11 @@ import java.util.List; /** - * An implementation of {@link ParametrizedStatementSupport} based on MySQL text query. + * An implementation of {@link ParameterizedStatementSupport} based on MySQL text query. */ -final class TextParametrizedStatement extends ParametrizedStatementSupport { +final class TextParameterizedStatement extends ParameterizedStatementSupport { - TextParametrizedStatement(Client client, Codecs codecs, Query query) { + TextParameterizedStatement(Client client, Codecs codecs, Query query) { super(client, codecs, query); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java index a1eff204d..6bc57c99f 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/api/MySqlStatement.java @@ -35,7 +35,7 @@ public interface MySqlStatement extends Statement { * {@inheritDoc} * * @return {@link MySqlStatement this} - * @throws IllegalStateException if the statement is parametrized and not all parameters are provided + * @throws IllegalStateException if the statement is parameterized and not all parameters are provided */ @Override MySqlStatement add(); @@ -96,7 +96,7 @@ public interface MySqlStatement extends Statement { * {@inheritDoc} * * @return a {@link Flux} representing {@link MySqlResult}s of the statement - * @throws IllegalStateException if the statement is parametrized and not all parameters are provided + * @throws IllegalStateException if the statement is parameterized and not all parameters are provided */ @Override Flux execute(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java index c970c212f..9bd116198 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java @@ -24,7 +24,7 @@ /** * Codec to encode and decode values based on MySQL data binary/text protocol. *

- * Use {@link ParametrizedCodec} for support {@code ParameterizedType} encoding/decoding. + * Use {@link ParameterizedCodec} for support {@code ParameterizedType} encoding/decoding. * * @param the type that is handled by this codec. */ diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index 882637f10..f609af74f 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -44,11 +44,11 @@ final class DefaultCodecs implements Codecs { private final Codec[] codecs; - private final ParametrizedCodec[] parametrizedCodecs; + private final ParameterizedCodec[] parameterizedCodecs; private final MassiveCodec[] massiveCodecs; - private final MassiveParametrizedCodec[] massiveParametrizedCodecs; + private final MassiveParameterizedCodec[] massiveParameterizedCodecs; private final Map> primitiveCodecs; @@ -56,32 +56,32 @@ private DefaultCodecs(Codec[] codecs) { this.codecs = requireNonNull(codecs, "codecs must not be null"); Map> primitiveCodecs = new HashMap<>(); - List> parametrizedCodecs = new ArrayList<>(); + List> parameterizedCodecs = new ArrayList<>(); List> massiveCodecs = new ArrayList<>(); - List> massiveParamCodecs = new ArrayList<>(); + List> massiveParamCodecs = new ArrayList<>(); for (Codec codec : codecs) { if (codec instanceof PrimitiveCodec) { // Primitive codec must be class-based codec, cannot support ParameterizedType. PrimitiveCodec c = (PrimitiveCodec) codec; primitiveCodecs.put(c.getPrimitiveClass(), c); - } else if (codec instanceof ParametrizedCodec) { - parametrizedCodecs.add((ParametrizedCodec) codec); + } else if (codec instanceof ParameterizedCodec) { + parameterizedCodecs.add((ParameterizedCodec) codec); } if (codec instanceof MassiveCodec) { massiveCodecs.add((MassiveCodec) codec); - if (codec instanceof MassiveParametrizedCodec) { - massiveParamCodecs.add((MassiveParametrizedCodec) codec); + if (codec instanceof MassiveParameterizedCodec) { + massiveParamCodecs.add((MassiveParameterizedCodec) codec); } } } this.primitiveCodecs = primitiveCodecs; this.massiveCodecs = massiveCodecs.toArray(new MassiveCodec[0]); - this.massiveParametrizedCodecs = massiveParamCodecs.toArray(new MassiveParametrizedCodec[0]); - this.parametrizedCodecs = parametrizedCodecs.toArray(new ParametrizedCodec[0]); + this.massiveParameterizedCodecs = massiveParamCodecs.toArray(new MassiveParameterizedCodec[0]); + this.parameterizedCodecs = parameterizedCodecs.toArray(new ParameterizedCodec[0]); } /** @@ -230,7 +230,7 @@ private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadat @Nullable private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { - for (ParametrizedCodec codec : parametrizedCodecs) { + for (ParameterizedCodec codec : parameterizedCodecs) { if (codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") T result = (T) codec.decode(value.getBufferSlice(), metadata, type, binary, context); @@ -258,7 +258,7 @@ private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat @Nullable private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { - for (MassiveParametrizedCodec codec : massiveParametrizedCodecs) { + for (MassiveParameterizedCodec codec : massiveParameterizedCodecs) { if (codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") T result = (T) codec.decodeMassive(value.getBufferSlices(), metadata, type, binary, context); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java index 17b09b276..424d969e5 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java @@ -36,7 +36,7 @@ *

* For now, supports only A.D. calendar in {@link ChronoLocalDateTime}. */ -final class LocalDateTimeCodec implements ParametrizedCodec { +final class LocalDateTimeCodec implements ParameterizedCodec { static final LocalDateTimeCodec INSTANCE = new LocalDateTimeCodec(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParameterizedCodec.java similarity index 94% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParameterizedCodec.java index e43af83d8..7c501bda0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParametrizedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/MassiveParameterizedCodec.java @@ -28,7 +28,7 @@ * * @param the type that is handled by this codec. */ -public interface MassiveParametrizedCodec extends ParametrizedCodec, MassiveCodec { +public interface MassiveParameterizedCodec extends ParameterizedCodec, MassiveCodec { /** * Decode a massive value as specified {@link ParameterizedType}. diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParameterizedCodec.java similarity index 97% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParameterizedCodec.java index 2950b08bb..3326b4742 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParametrizedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ParameterizedCodec.java @@ -29,7 +29,7 @@ * * @param the type without parameter that is handled by this codec. */ -public interface ParametrizedCodec extends Codec { +public interface ParameterizedCodec extends Codec { /** * Decodes a {@link ByteBuf} as specified {@link ParameterizedType}. diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java index a4ce64e06..82d4f8b2e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java @@ -41,7 +41,7 @@ * Codec for {@link Set}{@code <}{@link String}{@code >}, {@link Set}{@code <}{@link Enum}{@code >} and the * {@link String}{@code []}. */ -final class SetCodec implements ParametrizedCodec { +final class SetCodec implements ParameterizedCodec { static final SetCodec INSTANCE = new SetCodec(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java index 9af4aaa61..fedad10d8 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java @@ -37,7 +37,7 @@ *

* For now, supports only A.D. calendar in {@link ChronoZonedDateTime}. */ -final class ZonedDateTimeCodec implements ParametrizedCodec { +final class ZonedDateTimeCodec implements ParameterizedCodec { static final ZonedDateTimeCodec INSTANCE = new ZonedDateTimeCodec(); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java index 9a21c84d6..c8d50c633 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java @@ -78,28 +78,28 @@ void createStatement() { assertThat(noPrepare.createStatement(condition)) .isExactlyInstanceOf(TextSimpleStatement.class); assertThat(noPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(TextParametrizedStatement.class); + .isExactlyInstanceOf(TextParameterizedStatement.class); assertThat(allPrepare.createStatement("SELECT * FROM test WHERE id=1")) .isExactlyInstanceOf(PrepareSimpleStatement.class); assertThat(allPrepare.createStatement(condition)) .isExactlyInstanceOf(PrepareSimpleStatement.class); assertThat(allPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(PrepareParametrizedStatement.class); + .isExactlyInstanceOf(PrepareParameterizedStatement.class); assertThat(halfPrepare.createStatement("SELECT * FROM test WHERE id=1")) .isExactlyInstanceOf(TextSimpleStatement.class); assertThat(halfPrepare.createStatement(condition)) .isExactlyInstanceOf(TextSimpleStatement.class); assertThat(halfPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(PrepareParametrizedStatement.class); + .isExactlyInstanceOf(PrepareParameterizedStatement.class); assertThat(conditionPrepare.createStatement("SELECT * FROM test WHERE id=1")) .isExactlyInstanceOf(TextSimpleStatement.class); assertThat(conditionPrepare.createStatement(condition)) .isExactlyInstanceOf(PrepareSimpleStatement.class); assertThat(conditionPrepare.createStatement("SELECT * FROM test WHERE id=?")) - .isExactlyInstanceOf(PrepareParametrizedStatement.class); + .isExactlyInstanceOf(PrepareParameterizedStatement.class); } @SuppressWarnings("ConstantConditions") diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java similarity index 70% rename from r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java index f49d476e0..345704af5 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParametrizedStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java @@ -26,30 +26,30 @@ import static org.mockito.Mockito.when; /** - * Unit tests for {@link PrepareParametrizedStatement}. + * Unit tests for {@link PrepareParameterizedStatement}. */ -class PrepareParametrizedStatementTest implements StatementTestSupport { +class PrepareParameterizedStatementTest implements StatementTestSupport { private final Codecs codecs = Codecs.builder().build(); - private final Field fetchSize = PrepareParametrizedStatement.class.getDeclaredField("fetchSize"); + private final Field fetchSize = PrepareParameterizedStatement.class.getDeclaredField("fetchSize"); - PrepareParametrizedStatementTest() throws NoSuchFieldException { + PrepareParameterizedStatementTest() throws NoSuchFieldException { fetchSize.setAccessible(true); } @Override - public int getFetchSize(PrepareParametrizedStatement statement) throws IllegalAccessException { + public int getFetchSize(PrepareParameterizedStatement statement) throws IllegalAccessException { return fetchSize.getInt(statement); } @Override - public PrepareParametrizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { + public PrepareParameterizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { Client client = mock(Client.class); when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); - return new PrepareParametrizedStatement( + return new PrepareParameterizedStatement( client, codecs, Query.parse(sql), diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java index 3d53c2965..855e8d3a9 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareQueryIntegrationTest.java @@ -24,7 +24,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Integration tests for {@link PrepareParametrizedStatement} and {@link PrepareSimpleStatement}. + * Integration tests for {@link PrepareParameterizedStatement} and {@link PrepareSimpleStatement}. */ class PrepareQueryIntegrationTest extends QueryIntegrationTestSupport { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java index 0cab3c695..ebe788ed9 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/StatementTestSupport.java @@ -32,11 +32,11 @@ */ interface StatementTestSupport { - String PARAMETRIZED = "SELECT * FROM test WHERE id = ?id AND name = ?"; + String PARAMETERIZED = "SELECT * FROM test WHERE id = ?id AND name = ?"; String SIMPLE = "SELECT * FROM test WHERE id = 1 AND name = 'Mirrors'"; - T makeInstance(boolean isMariaDB, String parametrizedSql, String simpleSql); + T makeInstance(boolean isMariaDB, String parameterizedSql, String simpleSql); boolean supportsBinding(); @@ -48,7 +48,7 @@ default int getFetchSize(T statement) throws IllegalAccessException { default void bind() { assertTrue(supportsBinding(), "Must skip test case #bind() for simple statements"); - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); statement.bind(0, 1); statement.bind("id", 1); statement.bind(1, 1); @@ -57,7 +57,7 @@ default void bind() { @SuppressWarnings("ConstantConditions") @Test default void badBind() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); if (supportsBinding()) { assertThrows(IllegalArgumentException.class, () -> statement.bind(0, null)); @@ -89,7 +89,7 @@ default void badBind() { default void bindNull() { assertTrue(supportsBinding(), "Must skip test case #bindNull() for simple statements"); - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); statement.bindNull(0, Integer.class); statement.bindNull("id", Integer.class); statement.bindNull(1, Integer.class); @@ -98,7 +98,7 @@ default void bindNull() { @SuppressWarnings("ConstantConditions") @Test default void badBindNull() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); if (supportsBinding()) { assertThrows(IllegalArgumentException.class, () -> statement.bindNull(0, null)); @@ -128,7 +128,7 @@ default void badBindNull() { @Test default void add() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); if (!supportsBinding()) { statement.add(); @@ -146,14 +146,14 @@ default void add() { default void badAdd() { assertTrue(supportsBinding(), "Must skip test case #badAdd() for simple statements"); - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); statement.bind(0, 1); assertThrows(IllegalStateException.class, statement::add); } @Test default void mySqlReturnGeneratedValues() { - T s = makeInstance(false, PARAMETRIZED, SIMPLE); + T s = makeInstance(false, PARAMETERIZED, SIMPLE); s.returnGeneratedValues(); @@ -173,7 +173,7 @@ default void mySqlReturnGeneratedValues() { @Test default void mariaDbReturnGeneratedValues() { - T s = makeInstance(true, PARAMETRIZED, SIMPLE); + T s = makeInstance(true, PARAMETERIZED, SIMPLE); s.returnGeneratedValues(); @@ -203,7 +203,7 @@ default void mariaDbReturnGeneratedValues() { @SuppressWarnings("ConstantConditions") @Test default void badReturnGeneratedValues() { - T s = makeInstance(false, PARAMETRIZED, SIMPLE); + T s = makeInstance(false, PARAMETERIZED, SIMPLE); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String) null)); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String[]) null)); @@ -215,7 +215,7 @@ default void badReturnGeneratedValues() { @SuppressWarnings("ConstantConditions") @Test default void mariaDbBadReturnGeneratedValues() { - T s = makeInstance(true, PARAMETRIZED, SIMPLE); + T s = makeInstance(true, PARAMETERIZED, SIMPLE); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String) null)); assertThatIllegalArgumentException().isThrownBy(() -> s.returnGeneratedValues((String[]) null)); @@ -229,7 +229,7 @@ default void mariaDbBadReturnGeneratedValues() { @Test default void fetchSize() throws IllegalAccessException { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); assertEquals(0, getFetchSize(statement), "Must skip test case #fetchSize() for text-based queries"); for (int i = 1; i <= 10; ++i) { @@ -247,7 +247,7 @@ default void fetchSize() throws IllegalAccessException { @Test default void badFetchSize() { - T statement = makeInstance(false, PARAMETRIZED, SIMPLE); + T statement = makeInstance(false, PARAMETERIZED, SIMPLE); assertThrows(IllegalArgumentException.class, () -> statement.fetchSize(-1)); assertThrows(IllegalArgumentException.class, () -> statement.fetchSize(-10)); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParameterizedStatementTest.java similarity index 80% rename from r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java rename to r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParameterizedStatementTest.java index 1175fe5db..fb58ea526 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParametrizedStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextParameterizedStatementTest.java @@ -23,9 +23,9 @@ import static org.mockito.Mockito.when; /** - * Unit tests for {@link TextParametrizedStatement}. + * Unit tests for {@link TextParameterizedStatement}. */ -class TextParametrizedStatementTest implements StatementTestSupport { +class TextParameterizedStatementTest implements StatementTestSupport { private final Codecs codecs = Codecs.builder().build(); @@ -35,12 +35,12 @@ public void fetchSize() { } @Override - public TextParametrizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { + public TextParameterizedStatement makeInstance(boolean isMariaDB, String sql, String ignored) { Client client = mock(Client.class); when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); - return new TextParametrizedStatement( + return new TextParameterizedStatement( client, codecs, Query.parse(sql) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java index 76f3b95c6..b3e7478af 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TextQueryIntegrationTest.java @@ -17,7 +17,7 @@ package io.asyncer.r2dbc.mysql; /** - * Integration tests for {@link TextSimpleStatement} and {@link TextParametrizedStatement}. + * Integration tests for {@link TextSimpleStatement} and {@link TextParameterizedStatement}. */ class TextQueryIntegrationTest extends QueryIntegrationTestSupport { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java index 9954721d2..ec9127cc7 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/json/JacksonCodec.java @@ -21,8 +21,7 @@ import io.asyncer.r2dbc.mysql.ParameterWriter; import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.codec.CodecContext; -import io.asyncer.r2dbc.mysql.codec.ParametrizedCodec; -import io.asyncer.r2dbc.mysql.collation.CharCollation; +import io.asyncer.r2dbc.mysql.codec.ParameterizedCodec; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; import io.netty.buffer.ByteBuf; @@ -42,7 +41,7 @@ /** * A JSON codec based on Jackson. */ -public final class JacksonCodec implements ParametrizedCodec { +public final class JacksonCodec implements ParameterizedCodec { private static final ObjectMapper MAPPER = new ObjectMapper(); From 2371476218e672d409bc58d66793b114dd1e05e3 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Fri, 15 Mar 2024 20:22:19 +0900 Subject: [PATCH 39/93] Add fast path for determining `Codec` - Add `Codec.getMainClass()` for fast path, it allows users to extend `Codec` to change fast path - Add fast path to default codecs - Merge primitive type fast path into new fast path --- .../mysql/codec/AbstractClassedCodec.java | 5 + .../mysql/codec/AbstractPrimitiveCodec.java | 9 +- .../asyncer/r2dbc/mysql/codec/BlobCodec.java | 5 + .../r2dbc/mysql/codec/BooleanCodec.java | 2 +- .../asyncer/r2dbc/mysql/codec/ByteCodec.java | 2 +- .../asyncer/r2dbc/mysql/codec/ClobCodec.java | 5 + .../io/asyncer/r2dbc/mysql/codec/Codec.java | 15 +- .../r2dbc/mysql/codec/DefaultCodecs.java | 191 +++++++++++------- .../r2dbc/mysql/codec/DoubleCodec.java | 2 +- .../asyncer/r2dbc/mysql/codec/EnumCodec.java | 8 +- .../asyncer/r2dbc/mysql/codec/FloatCodec.java | 2 +- .../r2dbc/mysql/codec/InstantCodec.java | 5 + .../r2dbc/mysql/codec/IntegerCodec.java | 2 +- .../r2dbc/mysql/codec/LocalDateTimeCodec.java | 5 + .../asyncer/r2dbc/mysql/codec/LongCodec.java | 2 +- .../mysql/codec/OffsetDateTimeCodec.java | 5 + .../r2dbc/mysql/codec/PrimitiveCodec.java | 25 --- .../asyncer/r2dbc/mysql/codec/SetCodec.java | 5 + .../asyncer/r2dbc/mysql/codec/ShortCodec.java | 2 +- .../r2dbc/mysql/codec/ZonedDateTimeCodec.java | 5 + .../r2dbc/mysql/codec/EnumCodecTest.java | 3 +- 21 files changed, 196 insertions(+), 109 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java index 2462c46dc..2c22d1105 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractClassedCodec.java @@ -36,5 +36,10 @@ public final boolean canDecode(MySqlReadableMetadata metadata, Class target) return target.isAssignableFrom(this.type) && doCanDecode(metadata); } + @Override + public final Class getMainClass() { + return this.type; + } + protected abstract boolean doCanDecode(MySqlReadableMetadata metadata); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java index 295b0d18d..499a5fc98 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java @@ -41,11 +41,18 @@ abstract class AbstractPrimitiveCodec implements PrimitiveCodec { @Override public final boolean canDecode(MySqlReadableMetadata metadata, Class target) { - return target.isAssignableFrom(boxedClass) && canPrimitiveDecode(metadata); + return (target.isAssignableFrom(boxedClass) || target.equals(primitiveClass)) && doCanDecode(metadata); } @Override public final Class getPrimitiveClass() { return primitiveClass; } + + @Override + public final Class getMainClass() { + return boxedClass; + } + + protected abstract boolean doCanDecode(MySqlReadableMetadata metadata); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java index dc3a75b5e..29ec42b14 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BlobCodec.java @@ -49,6 +49,11 @@ final class BlobCodec implements MassiveCodec { private BlobCodec() { } + @Override + public Class getMainClass() { + return Blob.class; + } + @Override public Blob decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index 3b8035c5f..f546ba751 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java @@ -52,7 +52,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); return (type == MySqlType.BIT || type == MySqlType.TINYINT) && Integer.valueOf(1).equals(metadata.getPrecision()); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java index 649a6266d..e21c029c8 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteCodec.java @@ -52,7 +52,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java index b3bae3689..84d990a39 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ClobCodec.java @@ -52,6 +52,11 @@ final class ClobCodec implements MassiveCodec { private ClobCodec() { } + @Override + public Class getMainClass() { + return Clob.class; + } + @Override public Clob decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java index 9bd116198..744652c74 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/Codec.java @@ -41,8 +41,7 @@ public interface Codec { * @return the decoded result. */ @Nullable - T decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, - CodecContext context); + T decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context); /** * Checks if the field value can be decoded as specified {@link Class}. @@ -69,4 +68,16 @@ T decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean * @return encoded {@link MySqlParameter}. */ MySqlParameter encode(Object value, CodecContext context); + + /** + * Gets the main {@link Class} that is handled by this codec. It is used to fast path the codec lookup if it is not + * {@code null}. If same main {@link Class} is handled by multiple codecs, the codec with the highest priority will + * be used. The priority of the fast path is determined by its order in {@link Codecs}. + * + * @return the main {@link Class}, or {@code null} if it is not in fast path. + */ + @Nullable + default Class getMainClass() { + return null; + } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index f609af74f..793253858 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -22,13 +22,15 @@ import io.asyncer.r2dbc.mysql.message.FieldValue; import io.asyncer.r2dbc.mysql.message.LargeFieldValue; import io.asyncer.r2dbc.mysql.message.NormalFieldValue; +import io.r2dbc.spi.Blob; +import io.r2dbc.spi.Clob; import io.r2dbc.spi.Parameter; import org.jetbrains.annotations.Nullable; import javax.annotation.concurrent.GuardedBy; import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; import java.math.BigInteger; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -42,7 +44,46 @@ */ final class DefaultCodecs implements Codecs { - private final Codec[] codecs; + private static final List> DEFAULT_CODECS = InternalArrays.asImmutableList( + ByteCodec.INSTANCE, + ShortCodec.INSTANCE, + IntegerCodec.INSTANCE, + LongCodec.INSTANCE, + BigIntegerCodec.INSTANCE, + + BigDecimalCodec.INSTANCE, // Only all decimals + FloatCodec.INSTANCE, // Decimal (precision < 7) or float + DoubleCodec.INSTANCE, // Decimal (precision < 16) or double or float + + BooleanCodec.INSTANCE, + BitSetCodec.INSTANCE, + + ZonedDateTimeCodec.INSTANCE, + LocalDateTimeCodec.INSTANCE, + InstantCodec.INSTANCE, + OffsetDateTimeCodec.INSTANCE, + + LocalDateCodec.INSTANCE, + + LocalTimeCodec.INSTANCE, + DurationCodec.INSTANCE, + OffsetTimeCodec.INSTANCE, + + YearCodec.INSTANCE, + + StringCodec.INSTANCE, + + EnumCodec.INSTANCE, + SetCodec.INSTANCE, + + ClobCodec.INSTANCE, + BlobCodec.INSTANCE, + + ByteBufferCodec.INSTANCE, + ByteArrayCodec.INSTANCE + ); + + private final List> codecs; private final ParameterizedCodec[] parameterizedCodecs; @@ -50,21 +91,28 @@ final class DefaultCodecs implements Codecs { private final MassiveParameterizedCodec[] massiveParameterizedCodecs; - private final Map> primitiveCodecs; + private final Map, Codec> fastPath; - private DefaultCodecs(Codec[] codecs) { - this.codecs = requireNonNull(codecs, "codecs must not be null"); + private DefaultCodecs(List> codecs) { + requireNonNull(codecs, "codecs must not be null"); - Map> primitiveCodecs = new HashMap<>(); + Map, Codec> fastPath = new HashMap<>(); List> parameterizedCodecs = new ArrayList<>(); List> massiveCodecs = new ArrayList<>(); List> massiveParamCodecs = new ArrayList<>(); for (Codec codec : codecs) { + Class mainClass = codec.getMainClass(); + + if (mainClass != null) { + fastPath.putIfAbsent(mainClass, codec); + } + if (codec instanceof PrimitiveCodec) { // Primitive codec must be class-based codec, cannot support ParameterizedType. PrimitiveCodec c = (PrimitiveCodec) codec; - primitiveCodecs.put(c.getPrimitiveClass(), c); + + fastPath.putIfAbsent(c.getPrimitiveClass(), c); } else if (codec instanceof ParameterizedCodec) { parameterizedCodecs.add((ParameterizedCodec) codec); } @@ -78,15 +126,16 @@ private DefaultCodecs(Codec[] codecs) { } } - this.primitiveCodecs = primitiveCodecs; + this.fastPath = fastPath; + this.codecs = codecs; this.massiveCodecs = massiveCodecs.toArray(new MassiveCodec[0]); this.massiveParameterizedCodecs = massiveParamCodecs.toArray(new MassiveParameterizedCodec[0]); this.parameterizedCodecs = parameterizedCodecs.toArray(new ParameterizedCodec[0]); } /** - * Note: this method should NEVER release {@code buf} because of it come from {@code MySqlRow} which will - * release this buffer. + * Note: this method should NEVER release {@code buf} because of it come from {@code MySqlRow} which will release + * this buffer. */ @Override public T decode(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, @@ -104,10 +153,7 @@ public T decode(FieldValue value, MySqlReadableMetadata metadata, Class t Class target = chooseClass(metadata, type); - // Fast map for primitive classes. - if (target.isPrimitive()) { - return decodePrimitive(value, metadata, target, binary, context); - } else if (value instanceof NormalFieldValue) { + if (value instanceof NormalFieldValue) { return decodeNormal((NormalFieldValue) value, metadata, target, binary, context); } else if (value instanceof LargeFieldValue) { return decodeMassive((LargeFieldValue) value, metadata, target, binary, context); @@ -171,11 +217,18 @@ public MySqlParameter encode(Object value, CodecContext context) { requireNonNull(value, "value must not be null"); requireNonNull(context, "context must not be null"); - final Object valueToEncode = getValueToEncode(value); + Object valueToEncode = getValueToEncode(value); + if (null == valueToEncode) { return encodeNull(); } + Codec fast = encodeFast(valueToEncode); + + if (fast != null && fast.canEncode(valueToEncode)) { + return fast.encode(valueToEncode, context); + } + for (Codec codec : codecs) { if (codec.canEncode(valueToEncode)) { return codec.encode(valueToEncode, context); @@ -199,23 +252,46 @@ public MySqlParameter encodeNull() { } @Nullable - private T decodePrimitive(FieldValue value, MySqlReadableMetadata metadata, Class type, - boolean binary, CodecContext context) { - @SuppressWarnings("unchecked") - PrimitiveCodec codec = (PrimitiveCodec) this.primitiveCodecs.get(type); + @SuppressWarnings("unchecked") + private Codec decodeFast(Class type) { + Codec codec = (Codec) fastPath.get(type); + + if (codec == null && type.isEnum()) { + return (Codec) fastPath.get(Enum.class); + } + + return codec; + } - if (codec != null && value instanceof NormalFieldValue && codec.canPrimitiveDecode(metadata)) { - return codec.decode(((NormalFieldValue) value).getBufferSlice(), metadata, type, binary, context); + @Nullable + @SuppressWarnings("unchecked") + private Codec encodeFast(Object value) { + Codec codec = (Codec) fastPath.get(value.getClass()); + + if (codec == null) { + if (value instanceof ByteBuffer) { + return (Codec) fastPath.get(ByteBuffer.class); + } else if (value instanceof Blob) { + return (Codec) fastPath.get(Blob.class); + } else if (value instanceof Clob) { + return (Codec) fastPath.get(Clob.class); + } else if (value instanceof Enum) { + return (Codec) fastPath.get(Enum.class); + } } - // Mismatch, no one else can support this primitive class. - throw new IllegalArgumentException("Cannot decode " + value.getClass().getSimpleName() + " of " + - type + " for " + metadata.getType()); + return codec; } @Nullable private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { + Codec fast = decodeFast(type); + + if (fast != null && fast.canDecode(metadata, type)) { + return fast.decode(value.getBufferSlice(), metadata, type, binary, context); + } + for (Codec codec : codecs) { if (codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") @@ -244,6 +320,12 @@ private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadat @Nullable private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { + Codec fast = decodeFast(type); + + if (fast instanceof MassiveCodec && fast.canDecode(metadata, type)) { + return ((MassiveCodec) fast).decodeMassive(value.getBufferSlices(), metadata, type, binary, context); + } + for (MassiveCodec codec : massiveCodecs) { if (codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") @@ -269,52 +351,19 @@ private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat throw new IllegalArgumentException("Cannot decode massive " + type + " for " + metadata.getType()); } + /** + * Chooses the {@link Class} to use for decoding. It helps to find {@link Codec} on the fast path. e.g. + * {@link Object} -> {@link String} for {@code TEXT}, {@link Number} -> {@link Integer} for {@code INT}, etc. + * + * @param metadata the metadata of the column or the {@code OUT} parameter. + * @param type the {@link Class} specified by the user. + * @return the {@link Class} to use for decoding. + */ private static Class chooseClass(MySqlReadableMetadata metadata, Class type) { Class javaType = metadata.getType().getJavaType(); return type.isAssignableFrom(javaType) ? javaType : type; } - private static Codec[] defaultCodecs() { - return new Codec[] { - ByteCodec.INSTANCE, - ShortCodec.INSTANCE, - IntegerCodec.INSTANCE, - LongCodec.INSTANCE, - BigIntegerCodec.INSTANCE, - - BigDecimalCodec.INSTANCE, // Only all decimals - FloatCodec.INSTANCE, // Decimal (precision < 7) or float - DoubleCodec.INSTANCE, // Decimal (precision < 16) or double or float - - BooleanCodec.INSTANCE, - BitSetCodec.INSTANCE, - - ZonedDateTimeCodec.INSTANCE, - LocalDateTimeCodec.INSTANCE, - InstantCodec.INSTANCE, - OffsetDateTimeCodec.INSTANCE, - - LocalDateCodec.INSTANCE, - - LocalTimeCodec.INSTANCE, - DurationCodec.INSTANCE, - OffsetTimeCodec.INSTANCE, - - YearCodec.INSTANCE, - - StringCodec.INSTANCE, - - EnumCodec.INSTANCE, - SetCodec.INSTANCE, - - ClobCodec.INSTANCE, - BlobCodec.INSTANCE, - - ByteBufferCodec.INSTANCE, - ByteArrayCodec.INSTANCE - }; - } - static final class Builder implements CodecsBuilder { @GuardedBy("lock") @@ -327,12 +376,10 @@ public CodecsBuilder addFirst(Codec codec) { lock.lock(); try { if (codecs.isEmpty()) { - Codec[] defaultCodecs = defaultCodecs(); - - codecs.ensureCapacity(defaultCodecs.length + 1); + codecs.ensureCapacity(DEFAULT_CODECS.size() + 1); // Add first. codecs.add(codec); - codecs.addAll(InternalArrays.asImmutableList(defaultCodecs)); + codecs.addAll(DEFAULT_CODECS); } else { codecs.add(0, codec); } @@ -347,7 +394,7 @@ public CodecsBuilder addLast(Codec codec) { lock.lock(); try { if (codecs.isEmpty()) { - codecs.addAll(InternalArrays.asImmutableList(defaultCodecs())); + codecs.addAll(DEFAULT_CODECS); } codecs.add(codec); } finally { @@ -362,9 +409,9 @@ public Codecs build() { try { try { if (codecs.isEmpty()) { - return new DefaultCodecs(defaultCodecs()); + return new DefaultCodecs(DEFAULT_CODECS); } - return new DefaultCodecs(codecs.toArray(new Codec[0])); + return new DefaultCodecs(InternalArrays.asImmutableList(codecs.toArray(new Codec[0]))); } finally { codecs.clear(); codecs.trimToSize(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java index fa544965b..986666ab8 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DoubleCodec.java @@ -68,7 +68,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java index a658b3598..b1ebf638e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/EnumCodec.java @@ -29,13 +29,19 @@ /** * Codec for {@code enum class}. */ -final class EnumCodec implements Codec> { +@SuppressWarnings("rawtypes") +final class EnumCodec implements Codec { static final EnumCodec INSTANCE = new EnumCodec(); private EnumCodec() { } + @Override + public Class getMainClass() { + return Enum.class; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public Enum decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java index 91dd20b46..4e45c00e3 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/FloatCodec.java @@ -68,7 +68,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java index 0a14c6ee3..78894a24d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/InstantCodec.java @@ -39,6 +39,11 @@ final class InstantCodec implements Codec { private InstantCodec() { } + @Override + public Class getMainClass() { + return Instant.class; + } + @Override public Instant decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java index 73b9d702c..3ec04aa59 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/IntegerCodec.java @@ -65,7 +65,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java index 424d969e5..a9567cae6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LocalDateTimeCodec.java @@ -45,6 +45,11 @@ final class LocalDateTimeCodec implements ParameterizedCodec { private LocalDateTimeCodec() { } + @Override + public Class getMainClass() { + return LocalDateTime.class; + } + @Override public LocalDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java index 30c495db6..d57c57c8f 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/LongCodec.java @@ -73,7 +73,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java index b99714638..df1ffedf1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/OffsetDateTimeCodec.java @@ -39,6 +39,11 @@ final class OffsetDateTimeCodec implements Codec { private OffsetDateTimeCodec() { } + @Override + public Class getMainClass() { + return OffsetDateTime.class; + } + @Override public OffsetDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java index b8c420777..a880825a3 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java @@ -16,9 +16,6 @@ package io.asyncer.r2dbc.mysql.codec; -import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; -import io.netty.buffer.ByteBuf; - /** * Base class considers primitive class for {@link Codec} implementations. It should be an internal * abstraction. @@ -29,28 +26,6 @@ */ interface PrimitiveCodec extends Codec { - /** - * Decodes a {@link ByteBuf} as specified {@link Class}. - * - * @param value the {@link ByteBuf}. - * @param metadata the metadata of the column or the {@code OUT} parameter. - * @param target the specified {@link Class}, which can be a primitive type. - * @param binary if the value should be decoded by binary protocol. - * @param context the codec context. - * @return the decoded data that is boxed. - */ - @Override - T decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, - CodecContext context); - - /** - * Checks if the field value can be decoded as a primitive data. - * - * @param metadata the metadata of the column or the {@code OUT} parameter. - * @return if it can decode. - */ - boolean canPrimitiveDecode(MySqlReadableMetadata metadata); - /** * Gets the primitive {@link Class}, such as {@link Integer#TYPE}, etc. * diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java index 82d4f8b2e..1b88e9269 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/SetCodec.java @@ -48,6 +48,11 @@ final class SetCodec implements ParameterizedCodec { private SetCodec() { } + @Override + public Class getMainClass() { + return String[].class; + } + @Override public String[] decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java index c3c42948c..34bcaf305 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ShortCodec.java @@ -59,7 +59,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } @Override - public boolean canPrimitiveDecode(MySqlReadableMetadata metadata) { + public boolean doCanDecode(MySqlReadableMetadata metadata) { return metadata.getType().isNumeric(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java index fedad10d8..f16fcc274 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ZonedDateTimeCodec.java @@ -44,6 +44,11 @@ final class ZonedDateTimeCodec implements ParameterizedCodec { private ZonedDateTimeCodec() { } + @Override + public Class getMainClass() { + return ZonedDateTime.class; + } + @Override public ZonedDateTime decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java index d97feca07..87666ddac 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/EnumCodecTest.java @@ -27,7 +27,8 @@ /** * Unit tests for {@link EnumCodec}. */ -class EnumCodecTest implements CodecTestSupport> { +@SuppressWarnings("rawtypes") +class EnumCodecTest implements CodecTestSupport { private final Enum[] enums = { // Java has no way to create an element of enum with special character. From d533e1efbabe6235bdd3f0f8848070839691c9a7 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Mon, 18 Mar 2024 10:29:51 +0900 Subject: [PATCH 40/93] Remove PrimitiveCodec --- .../mysql/codec/AbstractPrimitiveCodec.java | 3 +- .../r2dbc/mysql/codec/DefaultCodecs.java | 11 +++--- .../r2dbc/mysql/codec/PrimitiveCodec.java | 35 ------------------- 3 files changed, 6 insertions(+), 43 deletions(-) delete mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java index 499a5fc98..a3844d28e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/AbstractPrimitiveCodec.java @@ -25,7 +25,7 @@ * * @param the boxed type of handling primitive data. */ -abstract class AbstractPrimitiveCodec implements PrimitiveCodec { +abstract class AbstractPrimitiveCodec implements Codec { private final Class primitiveClass; @@ -44,7 +44,6 @@ public final boolean canDecode(MySqlReadableMetadata metadata, Class target) return (target.isAssignableFrom(boxedClass) || target.equals(primitiveClass)) && doCanDecode(metadata); } - @Override public final Class getPrimitiveClass() { return primitiveClass; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index 793253858..d76b398e2 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -108,9 +108,8 @@ private DefaultCodecs(List> codecs) { fastPath.putIfAbsent(mainClass, codec); } - if (codec instanceof PrimitiveCodec) { - // Primitive codec must be class-based codec, cannot support ParameterizedType. - PrimitiveCodec c = (PrimitiveCodec) codec; + if (codec instanceof AbstractPrimitiveCodec) { + AbstractPrimitiveCodec c = (AbstractPrimitiveCodec) codec; fastPath.putIfAbsent(c.getPrimitiveClass(), c); } else if (codec instanceof ParameterizedCodec) { @@ -230,7 +229,7 @@ public MySqlParameter encode(Object value, CodecContext context) { } for (Codec codec : codecs) { - if (codec.canEncode(valueToEncode)) { + if (codec != fast && codec.canEncode(valueToEncode)) { return codec.encode(valueToEncode, context); } } @@ -293,7 +292,7 @@ private T decodeNormal(NormalFieldValue value, MySqlReadableMetadata metadat } for (Codec codec : codecs) { - if (codec.canDecode(metadata, type)) { + if (codec != fast && codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") Codec c = (Codec) codec; return c.decode(value.getBufferSlice(), metadata, type, binary, context); @@ -327,7 +326,7 @@ private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat } for (MassiveCodec codec : massiveCodecs) { - if (codec.canDecode(metadata, type)) { + if (codec != fast && codec.canDecode(metadata, type)) { @SuppressWarnings("unchecked") MassiveCodec c = (MassiveCodec) codec; return c.decodeMassive(value.getBufferSlices(), metadata, type, binary, context); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java deleted file mode 100644 index a880825a3..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/PrimitiveCodec.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql.codec; - -/** - * Base class considers primitive class for {@link Codec} implementations. It should be an internal - * abstraction. - *

- * Primitive types should never return {@code null} when decoding. - * - * @param the boxed type that is handled by this codec. - */ -interface PrimitiveCodec extends Codec { - - /** - * Gets the primitive {@link Class}, such as {@link Integer#TYPE}, etc. - * - * @return the primitive {@link Class}. - */ - Class getPrimitiveClass(); -} From aa90297f3b0c3fd9bf8b6cd9d09e46713eeb5283 Mon Sep 17 00:00:00 2001 From: jchrys Date: Wed, 27 Mar 2024 20:19:23 +0900 Subject: [PATCH 41/93] remove invalid benchmark Motivation: `MySqlNames` is removed by #257 but related bench is not removed properly. Modifications: Removed MySqlNames bench Result: Clean up --- .../mysql/MySqlNamesCompareBenchmark.java | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java diff --git a/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java b/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java deleted file mode 100644 index 351e47951..000000000 --- a/r2dbc-mysql/src/jmh/java/io/asyncer/r2dbc/mysql/MySqlNamesCompareBenchmark.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import org.junit.platform.commons.annotation.Testable; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Threads; -import org.openjdk.jmh.annotations.Timeout; - -/** - * Benchmark between {@link MySqlNames#compare} and {@link String#compareToIgnoreCase}. - */ -@State(Scope.Benchmark) -@Threads(1) -@Timeout(time = 1) -@Testable -public class MySqlNamesCompareBenchmark extends BenchmarkSupport { - - private static final String LEFT = "This is a test message for testing String compare " + - "with case insensitive or case sensitive"; - - private static final String RIGHT = LEFT.toUpperCase(); - - @Benchmark - @Testable - public int compareCi() { - return MySqlNames.compare(LEFT, RIGHT); - } - - @Benchmark - @Testable - public int nativeCompareCi() { - return String.CASE_INSENSITIVE_ORDER.compare(LEFT, RIGHT); - } - - @Benchmark - @Testable - public int compareCs() { - return MySqlNames.compare(LEFT, LEFT); - } - - @SuppressWarnings("EqualsWithItself") - @Benchmark - @Testable - public int nativeCompareCs() { - return String.CASE_INSENSITIVE_ORDER.compare(LEFT, LEFT); - } -} From 2f3cc2252c79ec4e3947205083fe9208698a12dc Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Wed, 27 Mar 2024 19:10:31 +0900 Subject: [PATCH 42/93] Add InitFlow and move session states to context --- .../r2dbc/mysql/ConnectionContext.java | 159 +++- .../asyncer/r2dbc/mysql/ConnectionState.java | 70 -- .../java/io/asyncer/r2dbc/mysql/InitFlow.java | 747 ++++++++++++++++++ ...ava => MySqlClientConnectionMetadata.java} | 24 +- .../r2dbc/mysql/MySqlConnectionFactory.java | 191 ++--- .../r2dbc/mysql/MySqlSimpleConnection.java | 441 +---------- .../mysql/PrepareParameterizedStatement.java | 8 +- .../r2dbc/mysql/PrepareSimpleStatement.java | 8 +- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 483 +++-------- .../mysql/internal/util/StringUtils.java | 43 +- .../r2dbc/mysql/ConnectionContextTest.java | 32 +- .../mysql/ConnectionIntegrationTest.java | 85 +- .../mysql/MySqlSimpleConnectionTest.java | 168 +++- .../PrepareParameterizedStatementTest.java | 3 +- .../mysql/PrepareSimpleStatementTest.java | 8 +- 15 files changed, 1378 insertions(+), 1092 deletions(-) delete mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java rename r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/{MySqlSimpleConnectionMetadata.java => MySqlClientConnectionMetadata.java} (60%) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index acf55812b..34011c6e1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -16,13 +16,16 @@ 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; @@ -37,6 +40,10 @@ 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 @@ -52,16 +59,47 @@ public final class ConnectionContext implements CodecContext { private Capability capability = Capability.DEFAULT; + private PrepareCache prepareCache; + @Nullable private ZoneId timeZone; + private String product = "Unknown"; + + /** + * Current isolation level inferred by past statements. + *

+ * Inference rules: + *

  1. In the beginning, it is also {@link #sessionIsolationLevel}.
  2. + *
  3. A transaction has began with a {@link IsolationLevel}, it will be changed to the value
  4. + *
  5. The transaction end (commit or rollback), it will recover to {@link #sessionIsolationLevel}.
+ */ + private volatile IsolationLevel currentIsolationLevel; + + /** + * Session isolation level. + * + *
  1. It is applied to all subsequent transactions performed within the current session.
  2. + *
  3. Calls {@link io.r2dbc.spi.Connection#setTransactionIsolationLevel}, it will change to the value.
  4. + *
  5. It can be changed within transactions, but does not affect the current ongoing transaction.
+ */ + private volatile IsolationLevel sessionIsolationLevel; + private boolean lockWaitTimeoutSupported = false; + /** + * Current lock wait timeout in seconds. + */ + private volatile Duration currentLockWaitTimeout; + + /** + * Session lock wait timeout in seconds. + */ + private volatile Duration sessionLockWaitTimeout; + /** * Assume that the auto commit is always turned on, it will be set after handshake V10 request message, or OK * message which means handshake V9 completed. - *

- * It would be updated multiple times, so {@code volatile} is required. */ private volatile short serverStatuses = ServerStatuses.AUTO_COMMIT; @@ -80,18 +118,50 @@ public final class ConnectionContext implements CodecContext { } /** - * Initializes this context. + * 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 init(int connectionId, ServerVersion version, Capability capability) { + 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. * @@ -128,6 +198,14 @@ public ZoneId getTimeZone() { return timeZone; } + String getProduct() { + return product; + } + + PrepareCache getPrepareCache() { + return prepareCache; + } + boolean isTimeZoneInitialized() { return timeZone != null; } @@ -138,13 +216,6 @@ public boolean isMariaDb() { return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb(); } - void initTimeZone(ZoneId timeZone) { - if (isTimeZoneInitialized()) { - throw new IllegalStateException("Connection timezone have been initialized"); - } - this.timeZone = timeZone; - } - @Override public ZeroDateOption getZeroDateOption() { return zeroDateOption; @@ -170,19 +241,23 @@ public int getLocalInfileBufferSize() { } /** - * Checks if the server supports lock wait timeout. + * Checks if the server supports InnoDB lock wait timeout. * - * @return if the server supports lock wait timeout. + * @return if the server supports InnoDB lock wait timeout. */ public boolean isLockWaitTimeoutSupported() { return lockWaitTimeoutSupported; } /** - * Enables lock wait timeout supported when loading session variables. + * Checks if the server supports statement timeout. + * + * @return if the server supports statement timeout. */ - void enableLockWaitTimeoutSupported() { - this.lockWaitTimeoutSupported = true; + public boolean isStatementTimeoutSupported() { + boolean isMariaDb = isMariaDb(); + return (isMariaDb && serverVersion.isGreaterThanOrEqualTo(MARIA_10_1_1)) || + (!isMariaDb && serverVersion.isGreaterThanOrEqualTo(MYSQL_5_7_4)); } /** @@ -202,4 +277,56 @@ public short getServerStatuses() { 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/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java deleted file mode 100644 index 73a9caf09..000000000 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionState.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2023 asyncer.io projects - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.asyncer.r2dbc.mysql; - -import io.r2dbc.spi.IsolationLevel; - -/** - * An internal interface for check, set and reset connection states. - */ -interface ConnectionState { - - /** - * Sets current isolation level. - * - * @param level current level. - */ - void setIsolationLevel(IsolationLevel level); - - /** - * Returns session lock wait timeout. - * - * @return Session lock wait timeout. - */ - long getSessionLockWaitTimeout(); - - /** - * Sets current lock wait timeout. - * - * @param timeoutSeconds seconds of current lock wait timeout. - */ - void setCurrentLockWaitTimeout(long timeoutSeconds); - - /** - * Checks if lock wait timeout has been changed by {@link #setCurrentLockWaitTimeout(long)}. - * - * @return if lock wait timeout changed. - */ - boolean isLockWaitTimeoutChanged(); - - /** - * Resets current isolation level in initial state. - */ - void resetIsolationLevel(); - - /** - * Resets current isolation level in initial state. - */ - void resetCurrentLockWaitTimeout(); - - /** - * Checks if connection is processing a transaction. - * - * @return if in a transaction. - */ - boolean isInTransaction(); -} diff --git a/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..32dcc1c8a --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java @@ -0,0 +1,747 @@ +/* + * 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 Flux} that indicates the initialization is done, or an error if the initialization failed. + */ + static Flux 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)); + } + + /** + * Initializes the session and {@link Codecs} of a {@link Client}. + * + * @param client the client + * @param database the database to use after session initialization + * @param prepareCacheSize the size of prepare cache + * @param sessionVariables the session variables to set + * @param forceTimeZone if the timezone should be set to session + * @param lockWaitTimeout the lock wait timeout that should be set to session + * @param statementTimeout the statement timeout that should be set to session + * @return a {@link Mono} that indicates the {@link Codecs}, or an error if the initialization failed + */ + static Mono initSession( + Client client, + String database, + int prepareCacheSize, + List sessionVariables, + boolean forceTimeZone, + @Nullable Duration lockWaitTimeout, + @Nullable Duration statementTimeout, + Extensions extensions + ) { + return Mono.defer(() -> { + ByteBufAllocator allocator = client.getByteBufAllocator(); + CodecsBuilder builder = Codecs.builder(); + + extensions.forEach(CodecRegistrar.class, registrar -> + registrar.register(allocator, builder)); + + Codecs codecs = builder.build(); + + List variables = mergeSessionVariables(client, sessionVariables, forceTimeZone, statementTimeout); + + logger.debug("Initializing client session: {}", variables); + + return QueryFlow.setSessionVariables(client, variables) + .then(loadSessionVariables(client, codecs)) + .flatMap(data -> loadAndInitInnoDbEngineStatus(data, client, codecs, lockWaitTimeout)) + .flatMap(data -> { + ConnectionContext context = client.getContext(); + + logger.debug("Initializing connection {} context: {}", context.getConnectionId(), data); + context.initSession( + Caches.createPrepareCache(prepareCacheSize), + data.level, + data.lockWaitTimeoutSupported, + data.lockWaitTimeout, + data.product, + data.timeZone + ); + + if (!data.lockWaitTimeoutSupported) { + logger.info( + "Lock wait timeout is not supported by server, all related operations will be ignored"); + } + + return database.isEmpty() ? Mono.just(codecs) : + initDatabase(client, database).then(Mono.just(codecs)); + }); + }); + } + + private static Mono loadAndInitInnoDbEngineStatus( + SessionState data, + Client client, + Codecs codecs, + @Nullable Duration lockWaitTimeout + ) { + return new TextSimpleStatement(client, codecs, "SHOW VARIABLES LIKE 'innodb\\\\_lock\\\\_wait\\\\_timeout'") + .execute() + .flatMap(r -> r.map(readable -> { + String value = readable.get(1, String.class); + + if (value == null || value.isEmpty()) { + return data; + } else { + return data.lockWaitTimeout(Duration.ofSeconds(Long.parseLong(value))); + } + })) + .single(data) + .flatMap(d -> { + if (lockWaitTimeout != null) { + // Do not use context.isLockWaitTimeoutSupported() here, because its session variable is not set + if (d.lockWaitTimeoutSupported) { + return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(lockWaitTimeout)) + .then(Mono.fromSupplier(() -> d.lockWaitTimeout(lockWaitTimeout))); + } + + logger.warn("Lock wait timeout is not supported by server, ignore initial setting"); + return Mono.just(d); + } + return Mono.just(d); + }); + } + + private static Mono loadSessionVariables(Client client, Codecs codecs) { + ConnectionContext context = client.getContext(); + StringBuilder query = new StringBuilder(128) + .append("SELECT ") + .append(transactionIsolationColumn(context)) + .append(",@@version_comment AS v"); + + Function> handler; + + if (context.isTimeZoneInitialized()) { + handler = r -> convertSessionData(r, false); + } else { + query.append(",@@system_time_zone AS s,@@time_zone AS t"); + handler = r -> convertSessionData(r, true); + } + + return new TextSimpleStatement(client, codecs, query.toString()) + .execute() + .flatMap(handler) + .last(); + } + + private static Mono initDatabase(Client client, String database) { + return client.exchange(new InitDbMessage(database), INIT_DB) + .last() + .flatMap(success -> { + if (success) { + return Mono.empty(); + } + + String sql = "CREATE DATABASE IF NOT EXISTS " + StringUtils.quoteIdentifier(database); + + return QueryFlow.executeVoid(client, sql) + .then(client.exchange(new InitDbMessage(database), INIT_DB_AFTER).then()); + }); + } + + private static List mergeSessionVariables( + Client client, + List sessionVariables, + boolean forceTimeZone, + @Nullable Duration statementTimeout + ) { + ConnectionContext context = client.getContext(); + + if ((!forceTimeZone || !context.isTimeZoneInitialized()) && statementTimeout == null) { + return sessionVariables; + } + + List variables = new ArrayList<>(sessionVariables.size() + 2); + + variables.addAll(sessionVariables); + + if (forceTimeZone && context.isTimeZoneInitialized()) { + variables.add(timeZoneVariable(context.getTimeZone())); + } + + if (statementTimeout != null) { + if (context.isStatementTimeoutSupported()) { + variables.add(StringUtils.statementTimeoutVariable(statementTimeout, context.isMariaDb())); + } else { + logger.warn("Statement timeout is not supported in {}, ignore initial setting", + context.getServerVersion()); + } + } + + return variables; + } + + private static String timeZoneVariable(ZoneId timeZone) { + String offerStr = timeZone instanceof ZoneOffset && "Z".equalsIgnoreCase(timeZone.getId()) ? + "+00:00" : timeZone.getId(); + + return "time_zone='" + offerStr + "'"; + } + + private static Flux convertSessionData(MySqlResult r, boolean timeZone) { + return r.map(readable -> { + IsolationLevel level = convertIsolationLevel(readable.get(0, String.class)); + String product = readable.get(1, String.class); + + return new SessionState(level, product, timeZone ? readZoneId(readable) : null); + }); + } + + /** + * Resolves the column of session isolation level, the {@literal @@tx_isolation} has been marked as deprecated. + *

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

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

+ * Not like other {@link FluxExchangeable}s, it is started by a server-side message, which should be an implementation + * of {@link HandshakeRequest}. + */ +final class HandshakeExchangeable extends FluxExchangeable { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(HandshakeExchangeable.class); + + private static final Map ATTRIBUTES = Collections.emptyMap(); + + private static final String CLI_SPECIFIC = "HY000"; + + private static final int HANDSHAKE_VERSION = 10; + + private final Sinks.Many requests = Sinks.many().unicast() + .onBackpressureBuffer(Queues.one().get()); + + private final Client client; + + private final SslMode sslMode; + + private final String database; + + private final String user; + + @Nullable + private final CharSequence password; + + private final Set compressions; + + private final int zstdCompressionLevel; + + private boolean handshake = true; + + private MySqlAuthProvider authProvider; + + private byte[] salt; + + private boolean sslCompleted; + + HandshakeExchangeable(Client client, SslMode sslMode, String database, String user, + @Nullable CharSequence password, Set compressions, + int zstdCompressionLevel) { + this.client = client; + this.sslMode = sslMode; + this.database = database; + this.user = user; + this.password = password; + this.compressions = compressions; + this.zstdCompressionLevel = zstdCompressionLevel; + this.sslCompleted = sslMode == SslMode.TUNNEL; + } + + @Override + public void subscribe(CoreSubscriber actual) { + requests.asFlux().subscribe(actual); + } + + @Override + public void accept(ServerMessage message, SynchronousSink sink) { + if (message instanceof ErrorMessage) { + sink.error(((ErrorMessage) message).toException()); + return; + } + + // Ensures it will be initialized only once. + if (handshake) { + handshake = false; + if (message instanceof HandshakeRequest) { + HandshakeRequest request = (HandshakeRequest) message; + Capability capability = initHandshake(request); + + if (capability.isSslEnabled()) { + emitNext(SslRequest.from(capability, client.getContext().getClientCollation().getId()), sink); + } else { + emitNext(createHandshakeResponse(capability), sink); + } + } else { + sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + + message.getClass().getSimpleName() + "' in init phase")); + } + + return; + } + + if (message instanceof OkMessage) { + logger.trace("Connection (id {}) login success", client.getContext().getConnectionId()); + client.loginSuccess(); + sink.complete(); + } else if (message instanceof SyntheticSslResponseMessage) { + sslCompleted = true; + emitNext(createHandshakeResponse(client.getContext().getCapability()), sink); + } else if (message instanceof AuthMoreDataMessage) { + AuthMoreDataMessage msg = (AuthMoreDataMessage) message; + + if (msg.isFailed()) { + if (logger.isDebugEnabled()) { + logger.debug("Connection (id {}) fast authentication failed, use full authentication", + client.getContext().getConnectionId()); + } + + emitNext(createAuthResponse("full authentication"), sink); + } + // Otherwise success, wait until OK message or Error message. + } else if (message instanceof ChangeAuthMessage) { + ChangeAuthMessage msg = (ChangeAuthMessage) message; + + authProvider = MySqlAuthProvider.build(msg.getAuthType()); + salt = msg.getSalt(); + emitNext(createAuthResponse("change authentication"), sink); + } else { + sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + + message.getClass().getSimpleName() + "' in login phase")); + } + } + + @Override + public void dispose() { + // No particular error condition handling for complete signal. + this.requests.tryEmitComplete(); + } + + private void emitNext(SubsequenceClientMessage message, SynchronousSink sink) { + Sinks.EmitResult result = requests.tryEmitNext(message); + + if (result != Sinks.EmitResult.OK) { + sink.error(new IllegalStateException("Fail to emit a login request due to " + result)); + } + } + + private AuthResponse createAuthResponse(String phase) { + MySqlAuthProvider authProvider = getAndNextProvider(); + + if (authProvider.isSslNecessary() && !sslCompleted) { + throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), phase), CLI_SPECIFIC); + } + + return new AuthResponse(authProvider.authentication(password, salt, client.getContext().getClientCollation())); + } + + private Capability clientCapability(Capability serverCapability) { + Capability.Builder builder = serverCapability.mutate(); + + builder.disableSessionTrack(); + builder.disableDatabasePinned(); + builder.disableIgnoreAmbiguitySpace(); + builder.disableInteractiveTimeout(); + + if (sslMode == SslMode.TUNNEL) { + // Tunnel does not use MySQL SSL protocol, disable it. + builder.disableSsl(); + } else if (!serverCapability.isSslEnabled()) { + // Server unsupported SSL. + if (sslMode.requireSsl()) { + // Before handshake, Client.context does not be initialized + throw new R2dbcPermissionDeniedException("Server does not support SSL but mode '" + sslMode + + "' requires SSL", CLI_SPECIFIC); + } else if (sslMode.startSsl()) { + // SSL has start yet, and client can disable SSL, disable now. + client.sslUnsupported(); + } + } else { + // The server supports SSL, but the user does not want to use SSL, disable it. + if (!sslMode.startSsl()) { + builder.disableSsl(); + } + } + + if (isZstdAllowed(serverCapability)) { + if (isZstdSupported()) { + builder.disableZlibCompression(); + } else { + logger.warn("Server supports zstd, but zstd-jni dependency is missing"); + + if (isZlibAllowed(serverCapability)) { + builder.disableZstdCompression(); + } else if (compressions.contains(CompressionAlgorithm.UNCOMPRESSED)) { + builder.disableCompression(); + } else { + throw new R2dbcNonTransientResourceException( + "Environment does not support a compression algorithm in " + compressions + + ", config does not allow uncompressed mode", CLI_SPECIFIC); + } + } + } else if (isZlibAllowed(serverCapability)) { + builder.disableZstdCompression(); + } else if (compressions.contains(CompressionAlgorithm.UNCOMPRESSED)) { + builder.disableCompression(); + } else { + throw new R2dbcPermissionDeniedException( + "Environment does not support a compression algorithm in " + compressions + + ", config does not allow uncompressed mode", CLI_SPECIFIC); + } + + if (database.isEmpty()) { + builder.disableConnectWithDatabase(); + } + + if (client.getContext().getLocalInfilePath() == null) { + builder.disableLoadDataLocalInfile(); + } + + if (ATTRIBUTES.isEmpty()) { + builder.disableConnectAttributes(); + } + + return builder.build(); + } + + private Capability initHandshake(HandshakeRequest message) { + HandshakeHeader header = message.getHeader(); + int handshakeVersion = header.getProtocolVersion(); + ServerVersion serverVersion = header.getServerVersion(); + + if (handshakeVersion < HANDSHAKE_VERSION) { + logger.warn("MySQL use handshake V{}, server version is {}, maybe most features are unavailable", + handshakeVersion, serverVersion); + } + + Capability capability = clientCapability(message.getServerCapability()); + + // No need initialize server statuses because it has initialized by read filter. + this.client.getContext().initHandshake(header.getConnectionId(), serverVersion, capability); + this.authProvider = MySqlAuthProvider.build(message.getAuthType()); + this.salt = message.getSalt(); + + return capability; + } + + private MySqlAuthProvider getAndNextProvider() { + MySqlAuthProvider authProvider = this.authProvider; + this.authProvider = authProvider.next(); + return authProvider; + } + + private HandshakeResponse createHandshakeResponse(Capability capability) { + MySqlAuthProvider authProvider = getAndNextProvider(); + + if (authProvider.isSslNecessary() && !sslCompleted) { + throw new R2dbcPermissionDeniedException(authFails(authProvider.getType(), "handshake"), + CLI_SPECIFIC); + } + + byte[] authorization = authProvider.authentication(password, salt, client.getContext().getClientCollation()); + String authType = authProvider.getType(); + + if (MySqlAuthProvider.NO_AUTH_PROVIDER.equals(authType)) { + // Authentication type is not matter because of it has no authentication type. + // Server need send a Change Authentication Message after handshake response. + authType = MySqlAuthProvider.CACHING_SHA2_PASSWORD; + } + + return HandshakeResponse.from(capability, client.getContext().getClientCollation().getId(), user, authorization, + authType, database, ATTRIBUTES, zstdCompressionLevel); + } + + private boolean isZstdAllowed(Capability capability) { + return capability.isZstdCompression() && compressions.contains(CompressionAlgorithm.ZSTD); + } + + private boolean isZlibAllowed(Capability capability) { + return capability.isZlibCompression() && compressions.contains(CompressionAlgorithm.ZLIB); + } + + private static String authFails(String authType, String phase) { + return "Authentication type '" + authType + "' must require SSL in " + phase + " phase"; + } + + private static boolean isZstdSupported() { + try { + ClassLoader loader = AccessController.doPrivileged((PrivilegedAction) () -> { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + return cl == null ? ClassLoader.getSystemClassLoader() : cl; + }); + Class.forName("com.github.luben.zstd.Zstd", false, loader); + return true; + } catch (ClassNotFoundException e) { + return false; + } + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionMetadata.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java similarity index 60% rename from r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionMetadata.java rename to r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java index ee7faf42d..61cb1d0b8 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionMetadata.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlClientConnectionMetadata.java @@ -17,39 +17,31 @@ package io.asyncer.r2dbc.mysql; import io.asyncer.r2dbc.mysql.api.MySqlConnectionMetadata; -import org.jetbrains.annotations.Nullable; - -import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; +import io.asyncer.r2dbc.mysql.client.Client; /** * Connection metadata for a connection connected to MySQL database. */ -final class MySqlSimpleConnectionMetadata implements MySqlConnectionMetadata { - - private final String version; - - private final String product; +final class MySqlClientConnectionMetadata implements MySqlConnectionMetadata { - private final boolean isMariaDb; + private final Client client; - MySqlSimpleConnectionMetadata(String version, @Nullable String product, boolean isMariaDb) { - this.version = requireNonNull(version, "version must not be null"); - this.product = product == null ? "Unknown" : product; - this.isMariaDb = isMariaDb; + MySqlClientConnectionMetadata(Client client) { + this.client = client; } @Override public String getDatabaseVersion() { - return version; + return client.getContext().getServerVersion().toString(); } @Override public boolean isMariaDb() { - return isMariaDb; + return client.getContext().isMariaDb(); } @Override public String getDatabaseProductName() { - return product; + return client.getContext().getProduct(); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index 6d76a8bed..d003db2b0 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -18,16 +18,9 @@ import io.asyncer.r2dbc.mysql.api.MySqlConnection; import io.asyncer.r2dbc.mysql.cache.Caches; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; import io.asyncer.r2dbc.mysql.cache.QueryCache; import io.asyncer.r2dbc.mysql.client.Client; -import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.asyncer.r2dbc.mysql.codec.CodecsBuilder; -import io.asyncer.r2dbc.mysql.constant.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.netty.buffer.ByteBufAllocator; import io.netty.channel.unix.DomainSocketAddress; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; @@ -38,13 +31,9 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.time.ZoneId; -import java.time.ZoneOffset; -import java.util.ArrayList; -import java.util.List; import java.util.Objects; -import java.util.Set; import java.util.concurrent.locks.ReentrantLock; -import java.util.function.Predicate; +import java.util.function.Supplier; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; @@ -93,102 +82,103 @@ public static MySqlConnectionFactory from(MySqlConnectionConfiguration configura address = new DomainSocketAddress(configuration.getDomain()); } - String database = configuration.getDatabase(); - boolean createDbIfNotExist = configuration.isCreateDatabaseIfNotExist(); String user = configuration.getUser(); CharSequence password = configuration.getPassword(); - SslMode sslMode = ssl.getSslMode(); - int zstdCompressionLevel = configuration.getZstdCompressionLevel(); - ZoneId connectionTimeZone = retrieveZoneId(configuration.getConnectionTimeZone()); - ConnectionContext context = new ConnectionContext( - configuration.getZeroDateOption(), - configuration.getLoadLocalInfilePath(), - configuration.getLocalInfileBufferSize(), - configuration.isPreserveInstants(), - connectionTimeZone - ); - Set compressionAlgorithms = configuration.getCompressionAlgorithms(); - Extensions extensions = configuration.getExtensions(); - Predicate prepare = configuration.getPreferPrepareStatement(); - int prepareCacheSize = configuration.getPrepareCacheSize(); Publisher passwordPublisher = configuration.getPasswordPublisher(); - boolean forceTimeZone = configuration.isForceConnectionTimeZoneToSession(); - List sessionVariables = forceTimeZone && connectionTimeZone != null ? - mergeSessionVariables(configuration.getSessionVariables(), connectionTimeZone) : - configuration.getSessionVariables(); if (Objects.nonNull(passwordPublisher)) { return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( - configuration, queryCache, - ssl, address, - database, createDbIfNotExist, - user, sslMode, - compressionAlgorithms, zstdCompressionLevel, - context, extensions, sessionVariables, prepare, - prepareCacheSize, token + configuration, ssl, + queryCache, + address, + user, + token )); } return getMySqlConnection( - configuration, queryCache, - ssl, address, - database, createDbIfNotExist, - user, sslMode, - compressionAlgorithms, zstdCompressionLevel, - context, extensions, sessionVariables, prepare, - prepareCacheSize, password + configuration, ssl, + queryCache, + address, + user, + password ); })); } + /** + * Gets an initialized {@link MySqlConnection} from authentication credential and configurations. + *

+ * It contains following steps: + *

  1. Create connection context
  2. + *
  3. Connect to MySQL server with TCP or Unix Domain Socket
  4. + *
  5. Handshake/login and init handshake states
  6. + *
  7. Init session states
+ * + * @param configuration the connection configuration. + * @param ssl the SSL configuration. + * @param queryCache lazy-init query cache, it is shared among all connections from the same factory. + * @param address TCP or Unix Domain Socket address. + * @param user the user of the authentication. + * @param password the password of the authentication. + * @return a {@link MySqlConnection}. + */ private static Mono getMySqlConnection( - final MySqlConnectionConfiguration configuration, - final LazyQueryCache queryCache, - final MySqlSslConfiguration ssl, - final SocketAddress address, - final String database, - final boolean createDbIfNotExist, - final String user, - final SslMode sslMode, - final Set compressionAlgorithms, - final int zstdLevel, - final ConnectionContext context, - final Extensions extensions, - final List sessionVariables, - @Nullable final Predicate prepare, - final int prepareCacheSize, - @Nullable final CharSequence password) { - return Client.connect(ssl, address, configuration.isTcpKeepAlive(), configuration.isTcpNoDelay(), - context, configuration.getConnectTimeout(), configuration.getLoopResources()) - .flatMap(client -> { - // Lazy init database after handshake/login - String db = createDbIfNotExist ? "" : database; - return QueryFlow.login(client, sslMode, db, user, password, compressionAlgorithms, zstdLevel); - }) - .flatMap(client -> { - ByteBufAllocator allocator = client.getByteBufAllocator(); - CodecsBuilder builder = Codecs.builder(); - PrepareCache prepareCache = Caches.createPrepareCache(prepareCacheSize); - String db = createDbIfNotExist ? database : ""; - - extensions.forEach(CodecRegistrar.class, registrar -> - registrar.register(allocator, builder)); - - Mono c = MySqlSimpleConnection.init(client, builder.build(), db, queryCache.get(), - prepareCache, sessionVariables, prepare); - - if (configuration.getLockWaitTimeout() != null) { - c = c.flatMap(connection -> connection.setLockWaitTimeout(configuration.getLockWaitTimeout()) - .thenReturn(connection)); - } - - if (configuration.getStatementTimeout() != null) { - c = c.flatMap(connection -> connection.setStatementTimeout(configuration.getStatementTimeout()) - .thenReturn(connection)); - } - - return c; - }); + 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.isPreserveInstants(), + connectionTimeZone + ); + }).flatMap(context -> Client.connect( + ssl, + address, + configuration.isTcpKeepAlive(), + configuration.isTcpNoDelay(), + context, + configuration.getConnectTimeout(), + configuration.getLoopResources() + )).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 @@ -202,19 +192,7 @@ private static ZoneId retrieveZoneId(String timeZone) { return StringUtils.parseZoneId(timeZone); } - private static List mergeSessionVariables(List sessionVariables, ZoneId timeZone) { - List res = new ArrayList<>(sessionVariables.size() + 1); - - String offerStr = timeZone instanceof ZoneOffset && "Z".equalsIgnoreCase(timeZone.getId()) ? - "+00:00" : timeZone.getId(); - - res.addAll(sessionVariables); - res.add("time_zone='" + offerStr + "'"); - - return res; - } - - private static final class LazyQueryCache { + private static final class LazyQueryCache implements Supplier { private final int capacity; @@ -227,6 +205,7 @@ private LazyQueryCache(int capacity) { this.capacity = capacity; } + @Override public QueryCache get() { QueryCache cache = this.cache; if (cache == null) { 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 index 660e25e06..829359040 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java @@ -19,16 +19,12 @@ 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.MySqlResult; import io.asyncer.r2dbc.mysql.api.MySqlStatement; import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; import io.asyncer.r2dbc.mysql.cache.QueryCache; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; -import io.asyncer.r2dbc.mysql.constant.ServerStatuses; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; -import io.asyncer.r2dbc.mysql.message.client.InitDbMessage; import io.asyncer.r2dbc.mysql.message.client.PingMessage; import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; import io.asyncer.r2dbc.mysql.message.server.ErrorMessage; @@ -38,18 +34,15 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import io.r2dbc.spi.IsolationLevel; import io.r2dbc.spi.R2dbcNonTransientResourceException; -import io.r2dbc.spi.Readable; import io.r2dbc.spi.TransactionDefinition; import io.r2dbc.spi.ValidationDepth; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SynchronousSink; -import java.time.DateTimeException; import java.time.Duration; -import java.time.ZoneId; -import java.util.List; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -60,24 +53,12 @@ /** * An implementation of {@link MySqlConnection} for connecting to the MySQL database. */ -final class MySqlSimpleConnection implements MySqlConnection, ConnectionState { +final class MySqlSimpleConnection implements MySqlConnection { private static final InternalLogger logger = InternalLoggerFactory.getInstance(MySqlSimpleConnection.class); private static final String PING_MARKER = "/* ping */"; - private static final ServerVersion MARIA_11_1_1 = ServerVersion.create(11, 1, 1, true); - - private static final ServerVersion MYSQL_8_0_3 = ServerVersion.create(8, 0, 3); - - private static final ServerVersion MYSQL_5_7_20 = ServerVersion.create(5, 7, 20); - - private static final ServerVersion MYSQL_8 = ServerVersion.create(8, 0, 0); - - private static final ServerVersion MYSQL_5_7_4 = ServerVersion.create(5, 7, 4); - - private static final ServerVersion MARIA_10_1_1 = ServerVersion.create(10, 1, 1, true); - private static final Function VALIDATE = message -> { if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { return true; @@ -106,87 +87,29 @@ final class MySqlSimpleConnection implements MySqlConnection, ConnectionState { } }; - private static final BiConsumer> INIT_DB = (message, sink) -> { - if (message instanceof ErrorMessage) { - ErrorMessage msg = (ErrorMessage) message; - logger.debug("Use database failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), - msg.getMessage()); - sink.next(false); - sink.complete(); - } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { - sink.next(true); - sink.complete(); - } else { - ReferenceCountUtil.safeRelease(message); - } - }; - - private static final BiConsumer> INIT_DB_AFTER = (message, sink) -> { - if (message instanceof ErrorMessage) { - sink.error(((ErrorMessage) message).toException()); - } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { - sink.complete(); - } else { - ReferenceCountUtil.safeRelease(message); - } - }; - private final Client client; private final Codecs codecs; - private final boolean batchSupported; - private final MySqlConnectionMetadata metadata; - private volatile IsolationLevel sessionLevel; - private final QueryCache queryCache; - private final PrepareCache prepareCache; - @Nullable private final Predicate prepare; - /** - * Current isolation level inferred by past statements. - *

- * Inference rules: - *

  1. In the beginning, it is also {@link #sessionLevel}.
  2. - *
  3. After the user calls {@link #setTransactionIsolationLevel(IsolationLevel)}, it will change to - * the user-specified value.
  4. - *
  5. After the end of a transaction (commit or rollback), it will recover to {@link #sessionLevel}.
  6. - *
- */ - private volatile IsolationLevel currentLevel; - - /** - * Session lock wait timeout. - */ - private volatile long lockWaitTimeout; - - /** - * Current transaction lock wait timeout. - */ - private volatile long currentLockWaitTimeout; + // TODO: Check it when executing + private final boolean batchSupported; - MySqlSimpleConnection(Client client, Codecs codecs, IsolationLevel level, - long lockWaitTimeout, QueryCache queryCache, PrepareCache prepareCache, @Nullable String product, - @Nullable Predicate prepare) { + MySqlSimpleConnection(Client client, Codecs codecs, QueryCache queryCache, @Nullable Predicate prepare) { ConnectionContext context = client.getContext(); this.client = client; - this.sessionLevel = level; - this.currentLevel = level; this.codecs = codecs; - this.lockWaitTimeout = lockWaitTimeout; - this.currentLockWaitTimeout = lockWaitTimeout; + this.metadata = new MySqlClientConnectionMetadata(client); this.queryCache = queryCache; - this.prepareCache = prepareCache; - this.metadata = new MySqlSimpleConnectionMetadata(context.getServerVersion().toString(), product, - context.isMariaDb()); - this.batchSupported = context.getCapability().isMultiStatementsAllowed(); this.prepare = prepare; + this.batchSupported = context.getCapability().isMultiStatementsAllowed(); if (this.batchSupported) { logger.debug("Batch is supported by server"); @@ -202,7 +125,7 @@ public Mono beginTransaction() { @Override public Mono beginTransaction(TransactionDefinition definition) { - return Mono.defer(() -> QueryFlow.beginTransaction(client, this, batchSupported, definition)); + return Mono.defer(() -> QueryFlow.beginTransaction(client, batchSupported, definition)); } @Override @@ -219,7 +142,7 @@ public Mono close() { @Override public Mono commitTransaction() { - return Mono.defer(() -> QueryFlow.doneTransaction(client, this, true, batchSupported)); + return Mono.defer(() -> QueryFlow.doneTransaction(client, true, batchSupported)); } @Override @@ -231,7 +154,7 @@ public MySqlBatch createBatch() { public Mono createSavepoint(String name) { requireNonEmpty(name, "Savepoint name must not be empty"); - return QueryFlow.createSavepoint(client, this, name, batchSupported); + return QueryFlow.createSavepoint(client, name, batchSupported); } @Override @@ -247,7 +170,7 @@ public MySqlStatement createStatement(String 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, prepareCache); + return new PrepareSimpleStatement(client, codecs, sql); } logger.debug("Create a simple statement provided by text query"); @@ -262,7 +185,7 @@ public MySqlStatement createStatement(String sql) { logger.debug("Create a parameterized statement provided by prepare query"); - return new PrepareParameterizedStatement(client, codecs, query, prepareCache); + return new PrepareParameterizedStatement(client, codecs, query); } @Override @@ -285,7 +208,7 @@ public Mono releaseSavepoint(String name) { @Override public Mono rollbackTransaction() { - return Mono.defer(() -> QueryFlow.doneTransaction(client, this, false, batchSupported)); + return Mono.defer(() -> QueryFlow.doneTransaction(client, false, batchSupported)); } @Override @@ -301,7 +224,7 @@ public MySqlConnectionMetadata getMetadata() { } /** - * MySQL does not have any way to query the isolation level of the current transaction, only inferred from past + * 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 @@ -310,16 +233,7 @@ public MySqlConnectionMetadata getMetadata() { */ @Override public IsolationLevel getTransactionIsolationLevel() { - return currentLevel; - } - - /** - * Gets session transaction isolation level(Only for testing). - * - * @return session transaction isolation level. - */ - IsolationLevel getSessionTransactionIsolationLevel() { - return sessionLevel; + return client.getContext().getCurrentIsolationLevel(); } @Override @@ -330,9 +244,11 @@ public Mono setTransactionIsolationLevel(IsolationLevel isolationLevel) { return QueryFlow.executeVoid(client, "SET SESSION TRANSACTION ISOLATION LEVEL " + isolationLevel.asSql()) .doOnSuccess(ignored -> { - this.sessionLevel = isolationLevel; - if (!this.isInTransaction()) { - this.currentLevel = isolationLevel; + ConnectionContext context = client.getContext(); + + context.setSessionIsolationLevel(isolationLevel); + if (!context.isInTransaction()) { + context.setCurrentIsolationLevel(isolationLevel); } }); } @@ -364,337 +280,62 @@ public Mono validate(ValidationDepth depth) { @Override public boolean isAutoCommit() { - // Within transaction, autocommit remains disabled until end the transaction with COMMIT or ROLLBACK. - // The autocommit mode then reverts to its previous state. - return !isInTransaction() && isSessionAutoCommit(); + return client.getContext().isAutoCommit(); } @Override public Mono setAutoCommit(boolean autoCommit) { - return Mono.defer(() -> { - if (autoCommit == isSessionAutoCommit()) { - return Mono.empty(); - } - - return QueryFlow.executeVoid(client, "SET autocommit=" + (autoCommit ? 1 : 0)); - }); - } - - @Override - public void setIsolationLevel(IsolationLevel level) { - this.currentLevel = level; - } - - @Override - public long getSessionLockWaitTimeout() { - return lockWaitTimeout; - } - - @Override - public void setCurrentLockWaitTimeout(long timeoutSeconds) { - this.currentLockWaitTimeout = timeoutSeconds; - } - - @Override - public void resetIsolationLevel() { - this.currentLevel = this.sessionLevel; - } - - @Override - public boolean isLockWaitTimeoutChanged() { - return currentLockWaitTimeout != lockWaitTimeout; - } - - @Override - public void resetCurrentLockWaitTimeout() { - this.currentLockWaitTimeout = this.lockWaitTimeout; - } - - @Override - public boolean isInTransaction() { - return (client.getContext().getServerStatuses() & ServerStatuses.IN_TRANSACTION) != 0; + 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()) { - logger.warn("Lock wait timeout is not supported by server, setLockWaitTimeout operation is ignored"); - return Mono.empty(); + if (client.getContext().isLockWaitTimeoutSupported()) { + return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(timeout)) + .doOnSuccess(ignored -> client.getContext().setAllLockWaitTimeout(timeout)); } - long timeoutSeconds = timeout.getSeconds(); - return QueryFlow.executeVoid(client, "SET innodb_lock_wait_timeout=" + timeoutSeconds) - .doOnSuccess(ignored -> this.lockWaitTimeout = this.currentLockWaitTimeout = timeoutSeconds); + 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"); - final ConnectionContext context = client.getContext(); - final boolean isMariaDb = context.isMariaDb(); - final ServerVersion serverVersion = context.getServerVersion(); - final long timeoutMs = timeout.toMillis(); - final String sql = isMariaDb ? "SET max_statement_time=" + timeoutMs / 1000.0 - : "SET SESSION MAX_EXECUTION_TIME=" + timeoutMs; + 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 (isMariaDb && serverVersion.isGreaterThanOrEqualTo(MARIA_10_1_1) - || !isMariaDb && serverVersion.isGreaterThanOrEqualTo(MYSQL_5_7_4)) { - return QueryFlow.executeVoid(client, sql); + 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 " + serverVersion, + "Statement timeout is not supported by server version " + context.getServerVersion(), "HY000", - -1, - sql + -1 ) ); } - private boolean isSessionAutoCommit() { - return (client.getContext().getServerStatuses() & ServerStatuses.AUTO_COMMIT) != 0; - } - - static Flux doPingInternal(Client client) { - return client.exchange(PingMessage.INSTANCE, PING); - } - /** - * Initialize a {@link MySqlConnection} after login. + * Visible only for testing. * - * @param client must be logged-in. - * @param codecs the {@link Codecs}. - * @param database the database that should be lazy init. - * @param queryCache the cache of {@link Query}. - * @param prepareCache the cache of server-preparing result. - * @param sessionVariables the session variables to set. - * @param prepare judging for prefer use prepare statement to execute simple query. - * @return a {@link Mono} will emit an initialized {@link MySqlConnection}. + * @return current connection context */ - static Mono init( - Client client, Codecs codecs, String database, - QueryCache queryCache, PrepareCache prepareCache, - List sessionVariables, @Nullable Predicate prepare - ) { - Mono connection = initSessionVariables(client, sessionVariables) - .then(loadSessionVariables(client, codecs)) - .flatMap(data -> loadInnoDbEngineStatus(data, client, codecs)) - .map(data -> { - ConnectionContext context = client.getContext(); - ZoneId timeZone = data.timeZone; - if (timeZone != null) { - logger.debug("Got server time zone {} from loading session variables", timeZone); - context.initTimeZone(timeZone); - } - - if (data.lockWaitTimeoutSupported) { - context.enableLockWaitTimeoutSupported(); - } else { - logger.info("Lock wait timeout is not supported by server, all related operations will be ignored"); - } - - return new MySqlSimpleConnection(client, codecs, data.level, data.lockWaitTimeout, - queryCache, prepareCache, data.product, prepare); - }); - - if (database.isEmpty()) { - return connection; - } - - return connection.flatMap(c -> initDatabase(client, database).thenReturn(c)); - } - - private static Mono initSessionVariables(Client client, List sessionVariables) { - if (sessionVariables.isEmpty()) { - return Mono.empty(); - } - - StringBuilder query = new StringBuilder(sessionVariables.size() * 32 + 16).append("SET "); - boolean comma = false; - - for (String variable : sessionVariables) { - if (variable.isEmpty()) { - continue; - } - - if (comma) { - query.append(','); - } else { - comma = true; - } - - if (variable.startsWith("@")) { - query.append(variable); - } else { - query.append("SESSION ").append(variable); - } - } - - return QueryFlow.executeVoid(client, query.toString()); + @TestOnly + ConnectionContext context() { + return client.getContext(); } - 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 loadInnoDbEngineStatus(SessionData data, Client client, Codecs codecs) { - 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(Long.parseLong(value)); - } - })) - .single(data); - } - - 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 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 SessionData(level, product, timeZone ? readZoneId(readable) : null); - }); - } - - 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; - } - - /** - * 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 final class SessionData { - - private final IsolationLevel level; - - @Nullable - private final String product; - - @Nullable - private final ZoneId timeZone; - - private long lockWaitTimeout = -1; - - private boolean lockWaitTimeoutSupported; - - private SessionData(IsolationLevel level, @Nullable String product, @Nullable ZoneId timeZone) { - this.level = level; - this.product = product; - this.timeZone = timeZone; - } - - SessionData lockWaitTimeout(long timeout) { - this.lockWaitTimeoutSupported = true; - this.lockWaitTimeout = timeout; - return this; - } + static Flux doPingInternal(Client client) { + return client.exchange(PingMessage.INSTANCE, PING); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java index d9e290811..44edd9509 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatement.java @@ -18,7 +18,6 @@ import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.api.MySqlStatement; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; @@ -33,20 +32,17 @@ */ final class PrepareParameterizedStatement extends ParameterizedStatementSupport { - private final PrepareCache prepareCache; - private int fetchSize = 0; - PrepareParameterizedStatement(Client client, Codecs codecs, Query query, PrepareCache prepareCache) { + PrepareParameterizedStatement(Client client, Codecs codecs, Query query) { super(client, codecs, query); - this.prepareCache = prepareCache; } @Override public Flux execute(List bindings) { return Flux.defer(() -> QueryFlow.execute(client, StringUtils.extendReturning(query.getFormattedSql(), returningIdentifiers()), - bindings, fetchSize, prepareCache + bindings, fetchSize )) .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages)); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java index d78bb3488..29a2b8232 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatement.java @@ -18,7 +18,6 @@ import io.asyncer.r2dbc.mysql.api.MySqlResult; import io.asyncer.r2dbc.mysql.api.MySqlStatement; -import io.asyncer.r2dbc.mysql.cache.PrepareCache; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; @@ -36,19 +35,16 @@ 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, String sql, PrepareCache prepareCache) { + PrepareSimpleStatement(Client client, Codecs codecs, String sql) { super(client, codecs, sql); - this.prepareCache = prepareCache; } @Override public Flux execute() { return Flux.defer(() -> QueryFlow.execute(client, - StringUtils.extendReturning(sql, returningIdentifiers()), BINDINGS, fetchSize, prepareCache)) + StringUtils.extendReturning(sql, returningIdentifiers()), BINDINGS, fetchSize)) .map(messages -> MySqlSegmentResult.toResult(true, client, codecs, syntheticKeyName(), messages)); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index e7a5de4bc..23ce5e806 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -18,19 +18,12 @@ import io.asyncer.r2dbc.mysql.api.MySqlBatch; import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; -import io.asyncer.r2dbc.mysql.authentication.MySqlAuthProvider; -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.constant.CompressionAlgorithm; 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; @@ -38,29 +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.R2dbcNonTransientResourceException; -import io.r2dbc.spi.R2dbcPermissionDeniedException; import io.r2dbc.spi.TransactionDefinition; import org.jetbrains.annotations.Nullable; import reactor.core.CoreSubscriber; @@ -72,15 +57,10 @@ import reactor.core.publisher.SynchronousSink; import reactor.util.concurrent.Queues; -import java.security.AccessController; -import java.security.PrivilegedAction; 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.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -116,18 +96,16 @@ final class QueryFlow { * @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); }); } @@ -194,29 +172,6 @@ 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 compressionAlgorithms the list of compression algorithms. - * @param zstdCompressionLevel the zstd compression level. - * @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, - Set compressionAlgorithms, int zstdCompressionLevel) { - return client.exchange(new LoginExchangeable(client, sslMode, database, user, password, - compressionAlgorithms, zstdCompressionLevel)) - .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. @@ -245,17 +200,15 @@ 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}. + * 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, client); + 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(); @@ -265,18 +218,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 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(); @@ -285,15 +235,80 @@ static Mono doneTransaction(Client client, ConnectionState state, boolean return client.exchange(new TransactionMultiExchangeable(commitState)).then(); } - static Mono createSavepoint(Client client, ConnectionState state, String name, - boolean batchSupported) { - final CreateSavepointState savepointState = new CreateSavepointState(state, name); + /** + * Creates a savepoint with a name. It will begin a new transaction before creating a savepoint if the connection is + * not in a transaction. + * + * @param client the {@link Client} to exchange messages with. + * @param name the name of the savepoint. + * @param batchSupported if connection supports batch query. + * @return a {@link Mono} receives complete signal. + */ + static Mono createSavepoint(Client client, String name, boolean batchSupported) { + final CreateSavepointState savepointState = new CreateSavepointState(client, name); if (batchSupported) { return client.exchange(new TransactionBatchExchangeable(savepointState)).then(); } return client.exchange(new TransactionMultiExchangeable(savepointState)).then(); } + /** + * Sets a session variable to the server. + * + * @param client the {@link Client} to exchange messages with. + * @param variable the session variable to set, e.g. {@code "sql_mode='ANSI'"}. + * @return a {@link Mono} receives complete signal. + */ + static Mono setSessionVariable(Client client, String variable) { + if (variable.isEmpty()) { + return Mono.empty(); + } else if (variable.startsWith("@")) { + return executeVoid(client, "SET " + variable); + } + + return executeVoid(client, "SET SESSION " + variable); + } + + /** + * Sets multiple session variables to the server. + * + * @param client the {@link Client} to exchange messages with. + * @param sessionVariables the session variables to set, e.g. {@code ["sql_mode='ANSI'", "time_zone='+09:00'"]}. + * @return a {@link Mono} receives complete signal. + */ + static Mono setSessionVariables(Client client, List sessionVariables) { + switch (sessionVariables.size()) { + case 0: + return Mono.empty(); + case 1: + return setSessionVariable(client, sessionVariables.get(0)); + default: { + StringBuilder query = new StringBuilder(sessionVariables.size() * 32 + 16).append("SET "); + boolean comma = false; + + for (String variable : sessionVariables) { + if (variable.isEmpty()) { + continue; + } + + if (comma) { + query.append(','); + } else { + comma = true; + } + + if (variable.startsWith("@")) { + query.append(variable); + } else { + query.append("SESSION ").append(variable); + } + } + + return executeVoid(client, query.toString()); + } + } + } + /** * Execute a simple query statement. Query execution terminates with the last {@link CompleteMessage} or a * {@link ErrorMessage}. The {@link ErrorMessage} will emit an exception. The exchange will be completed by @@ -544,7 +559,7 @@ final class PrepareExchangeable extends FluxExchangeable { private final Sinks.Many requests = Sinks.many().unicast() .onBackpressureBuffer(Queues.one().get()); - private final PrepareCache cache; + private final Client client; private final String sql; @@ -559,8 +574,8 @@ final class PrepareExchangeable extends FluxExchangeable { private boolean shouldClose; - PrepareExchangeable(PrepareCache cache, String sql, Iterator bindings, int fetchSize) { - this.cache = cache; + PrepareExchangeable(Client client, String sql, Iterator bindings, int fetchSize) { + this.client = client; this.sql = sql; this.bindings = bindings; this.fetchSize = fetchSize; @@ -572,7 +587,7 @@ public void subscribe(CoreSubscriber actual) { requests.asFlux().subscribe(actual); // After subscribe. - Integer statementId = cache.getIfPresent(sql); + Integer statementId = client.getContext().getPrepareCache().getIfPresent(sql); if (statementId == null) { logger.debug("Prepare cache mismatch, try to preparing"); this.shouldClose = true; @@ -713,7 +728,7 @@ private void putToCache(Integer statementId) { boolean putSucceed; try { - putSucceed = cache.putIfAbsent(sql, statementId, evictId -> { + putSucceed = client.getContext().getPrepareCache().putIfAbsent(sql, statementId, evictId -> { logger.debug("Prepare cache evicts statement {} when putting", evictId); Sinks.EmitResult result = requests.tryEmitNext(new PreparedCloseMessage(evictId)); @@ -809,292 +824,9 @@ private void onCompleteMessage(CompleteMessage message, SynchronousSink - * Not like other {@link FluxExchangeable}s, it is started by a server-side message, which should be an implementation - * of {@link HandshakeRequest}. - */ -final class LoginExchangeable extends FluxExchangeable { - - private static final InternalLogger logger = InternalLoggerFactory.getInstance(LoginExchangeable.class); - - private static final Map ATTRIBUTES = Collections.emptyMap(); - - private static final String CLI_SPECIFIC = "HY000"; - - private static final int HANDSHAKE_VERSION = 10; - - private final Sinks.Many requests = Sinks.many().unicast() - .onBackpressureBuffer(Queues.one().get()); - - private final Client client; - - private final SslMode sslMode; - - private final String database; - - private final String user; - - @Nullable - private final CharSequence password; - - private final Set compressions; - - private final int zstdCompressionLevel; - - private boolean handshake = true; - - private MySqlAuthProvider authProvider; - - private byte[] salt; - - private boolean sslCompleted; - - LoginExchangeable(Client client, SslMode sslMode, String database, String user, - @Nullable CharSequence password, Set compressions, - int zstdCompressionLevel) { - this.client = client; - this.sslMode = sslMode; - this.database = database; - this.user = user; - this.password = password; - this.compressions = compressions; - this.zstdCompressionLevel = zstdCompressionLevel; - this.sslCompleted = sslMode == SslMode.TUNNEL; - } - - @Override - public void subscribe(CoreSubscriber actual) { - requests.asFlux().subscribe(actual); - } - - @Override - public void accept(ServerMessage message, SynchronousSink sink) { - if (message instanceof ErrorMessage) { - sink.error(((ErrorMessage) message).toException()); - return; - } - - // Ensures it will be initialized only once. - if (handshake) { - handshake = false; - if (message instanceof HandshakeRequest) { - HandshakeRequest request = (HandshakeRequest) message; - Capability capability = initHandshake(request); - - if (capability.isSslEnabled()) { - emitNext(SslRequest.from(capability, client.getContext().getClientCollation().getId()), sink); - } else { - emitNext(createHandshakeResponse(capability), sink); - } - } else { - sink.error(new R2dbcPermissionDeniedException("Unexpected message type '" + - message.getClass().getSimpleName() + "' in init phase")); - } - - return; - } - - if (message instanceof OkMessage) { - 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().init(header.getConnectionId(), serverVersion, capability); - this.authProvider = MySqlAuthProvider.build(message.getAuthType()); - this.salt = message.getSalt(); - - return capability; - } - - private MySqlAuthProvider getAndNextProvider() { - MySqlAuthProvider authProvider = this.authProvider; - this.authProvider = authProvider.next(); - return authProvider; - } - - private HandshakeResponse createHandshakeResponse(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; - } - } -} - abstract class AbstractTransactionState { - final ConnectionState state; + final Client client; final List statements = new ArrayList<>(5); @@ -1106,8 +838,8 @@ abstract class AbstractTransactionState { @Nullable private String sql; - protected AbstractTransactionState(ConnectionState state) { - this.state = state; + protected AbstractTransactionState(Client client) { + this.client = client; } final void setSql(String sql) { @@ -1165,22 +897,24 @@ final class CommitRollbackState extends AbstractTransactionState { private final boolean commit; - CommitRollbackState(ConnectionState state, boolean commit) { - super(state); + CommitRollbackState(Client client, boolean commit) { + super(client); this.commit = commit; } @Override boolean cancelTasks() { - if (!state.isInTransaction()) { + ConnectionContext context = client.getContext(); + + if (!context.isInTransaction()) { tasks |= CANCEL; return true; } - if (state.isLockWaitTimeoutChanged()) { + if (context.isLockWaitTimeoutChanged()) { // If server does not support lock wait timeout, the state will not be changed, so it is safe. tasks |= LOCK_WAIT_TIMEOUT; - statements.add("SET innodb_lock_wait_timeout=" + state.getSessionLockWaitTimeout()); + statements.add(StringUtils.lockWaitTimeoutStatement(context.getSessionLockWaitTimeout())); } tasks |= COMMIT_OR_ROLLBACK; @@ -1193,10 +927,10 @@ boolean cancelTasks() { protected boolean process(int task, SynchronousSink sink) { switch (task) { case LOCK_WAIT_TIMEOUT: - state.resetCurrentLockWaitTimeout(); + client.getContext().resetCurrentLockWaitTimeout(); return true; case COMMIT_OR_ROLLBACK: - state.resetIsolationLevel(); + client.getContext().resetCurrentIsolationLevel(); sink.complete(); return false; case CANCEL: @@ -1222,26 +956,24 @@ final class StartTransactionState extends AbstractTransactionState { private final TransactionDefinition definition; - private final Client client; - - StartTransactionState(ConnectionState state, TransactionDefinition definition, Client client) { - super(state); + StartTransactionState(Client client, TransactionDefinition definition) { + super(client); this.definition = definition; - this.client = client; } @Override boolean cancelTasks() { - if (state.isInTransaction()) { + final ConnectionContext context = client.getContext(); + if (context.isInTransaction()) { tasks |= CANCEL; return true; } + final Duration timeout = definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT); if (timeout != null) { - if (client.getContext().isLockWaitTimeoutSupported()) { - long lockWaitTimeout = timeout.getSeconds(); + if (context.isLockWaitTimeoutSupported()) { tasks |= LOCK_WAIT_TIMEOUT; - statements.add("SET innodb_lock_wait_timeout=" + lockWaitTimeout); + statements.add(StringUtils.lockWaitTimeoutStatement(timeout)); } else { QueryFlow.logger.warn( "Lock wait timeout is not supported by server, transaction definition lockWaitTimeout is ignored"); @@ -1267,22 +999,19 @@ protected boolean process(int task, SynchronousSink sink) { case LOCK_WAIT_TIMEOUT: final Duration timeout = definition.getAttribute(TransactionDefinition.LOCK_WAIT_TIMEOUT); if (timeout != null) { - final long lockWaitTimeout = timeout.getSeconds(); - state.setCurrentLockWaitTimeout(lockWaitTimeout); + client.getContext().setCurrentLockWaitTimeout(timeout); } return true; case ISOLATION_LEVEL: - final IsolationLevel isolationLevel = - definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL); + final IsolationLevel isolationLevel = definition.getAttribute(TransactionDefinition.ISOLATION_LEVEL); if (isolationLevel != null) { - state.setIsolationLevel(isolationLevel); + client.getContext().setCurrentIsolationLevel(isolationLevel); } return true; case START_TRANSACTION: case CANCEL: sink.complete(); return false; - } sink.error(new IllegalStateException("Undefined transaction task: " + task + ", remain: " + tasks)); @@ -1352,14 +1081,14 @@ final class CreateSavepointState extends AbstractTransactionState { private final String name; - CreateSavepointState(final ConnectionState state, final String name) { - super(state); + CreateSavepointState(final Client client, final String name) { + super(client); this.name = name; } @Override boolean cancelTasks() { - if (!state.isInTransaction()) { + if (!client.getContext().isInTransaction()) { tasks |= START_TRANSACTION; statements.add("BEGIN"); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java index e5c3596b6..1a96e2d79 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/internal/util/StringUtils.java @@ -16,13 +16,14 @@ package io.asyncer.r2dbc.mysql.internal.util; +import java.time.Duration; import java.time.ZoneId; import java.time.ZoneOffset; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonEmpty; /** - * A utility for processing {@link String} in MySQL/MariaDB. + * A utility for processing {@link String} and simple statements in MySQL/MariaDB. */ public final class StringUtils { @@ -79,16 +80,48 @@ public static String extendReturning(String sql, String returning) { return returning.isEmpty() ? sql : sql + " RETURNING " + returning; } + /** + * Generates a {@link String} indicating the statement timeout variable. e.g. {@code "max_statement_time=1.5"} for + * MariaDB or {@code "max_execution_time=1500"} for MySQL. + * + * @param timeout the statement timeout + * @param isMariaDb whether the current server is MariaDB + * @return the statement timeout variable + */ + public static String statementTimeoutVariable(Duration timeout, boolean isMariaDb) { + // mariadb: https://mariadb.com/kb/en/aborting-statements/ + // mysql: https://dev.mysql.com/blog-archive/server-side-select-statement-timeouts/ + // ref: https://github.com/mariadb-corporation/mariadb-connector-r2dbc + if (isMariaDb) { + // MariaDB supports fractional seconds with microsecond precision + double seconds = (timeout.getSeconds() + timeout.getNano() / 1_000_000_000.0); + return "max_statement_time=" + seconds; + } + + return "max_execution_time=" + timeout.toMillis(); + } + + /** + * Generates a statement to set the lock wait timeout for the current session. It is using InnoDB-specific session + * variable {@code innodb_lock_wait_timeout}. + * + * @param timeout the lock wait timeout + * @return the lock wait timeout statement + */ + public static String lockWaitTimeoutStatement(Duration timeout) { + return "SET innodb_lock_wait_timeout=" + timeout.getSeconds(); + } + /** * Parses a normalized {@link ZoneId} from a time zone string of MySQL. *

- * Note: since java 14.0.2, 11.0.8, 8u261 and 7u271, America/Nuuk is already renamed from America/Godthab. - * See also tzdata2020a + * Note: since java 14.0.2, 11.0.8, 8u261 and 7u271, America/Nuuk is already renamed from America/Godthab. See also + * tzdata2020a * * @param zoneId the time zone string * @return the normalized {@link ZoneId} - * @throws IllegalArgumentException if the time zone string is {@code null} or empty - * @throws java.time.DateTimeException if the time zone string has an invalid format + * @throws IllegalArgumentException if the time zone string is {@code null} or empty + * @throws java.time.DateTimeException if the time zone string has an invalid format * @throws java.time.zone.ZoneRulesException if the time zone string cannot be found */ public static ZoneId parseZoneId(String zoneId) { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java index 5e2be6114..5d0635412 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java @@ -16,10 +16,13 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.cache.Caches; import io.asyncer.r2dbc.mysql.constant.ServerStatuses; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; +import io.r2dbc.spi.IsolationLevel; import org.junit.jupiter.api.Test; +import java.time.Duration; import java.time.ZoneId; import static org.assertj.core.api.Assertions.assertThat; @@ -46,15 +49,36 @@ void getTimeZone() { void setTwiceTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, 8192, true, null); - context.initTimeZone(ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.initTimeZone(ZoneId.systemDefault())); + + context.initSession( + Caches.createPrepareCache(0), + IsolationLevel.REPEATABLE_READ, + false, Duration.ZERO, + null, + ZoneId.systemDefault() + ); + assertThatIllegalStateException().isThrownBy(() -> context.initSession( + Caches.createPrepareCache(0), + IsolationLevel.REPEATABLE_READ, + false, + Duration.ZERO, + null, + ZoneId.systemDefault() + )); } @Test void badSetTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, 8192, true, ZoneId.systemDefault()); - assertThatIllegalStateException().isThrownBy(() -> context.initTimeZone(ZoneId.systemDefault())); + assertThatIllegalStateException().isThrownBy(() -> context.initSession( + Caches.createPrepareCache(0), + IsolationLevel.REPEATABLE_READ, + false, + Duration.ZERO, + null, + ZoneId.systemDefault() + )); } public static ConnectionContext mock() { @@ -69,7 +93,7 @@ public static ConnectionContext mock(boolean isMariaDB, ZoneId zoneId) { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, 8192, true, zoneId); - context.init(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"), + context.initHandshake(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"), Capability.of(~(isMariaDB ? 1 : 0))); context.setServerStatuses(ServerStatuses.AUTO_COMMIT); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index b45d7f91c..8fa06f1f9 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -68,16 +68,16 @@ class ConnectionIntegrationTest extends IntegrationTestSupport { @Test void isInTransaction() { - castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.context().isInTransaction()) .isFalse()) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(connection.commitTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse())); + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse())); } @DisabledIf("envIsLessThanMySql56") @@ -88,16 +88,16 @@ void startTransaction() { TransactionDefinition readWriteConsistent = MySqlTransactionDefinition.mutability(true) .consistent(); - castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.isInTransaction()) + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.context().isInTransaction()) .isFalse()) .then(connection.beginTransaction(readOnlyConsistent)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) .then(connection.beginTransaction(readWriteConsistent)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse())); + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse())); } @Test @@ -115,9 +115,9 @@ void autoRollbackPreRelease() { .flatMap(MySqlResult::getRowsUpdated) .single() .doOnNext(it -> assertThat(it).isEqualTo(1)) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(conn.context().isInTransaction()).isTrue()) .then(conn.preRelease()) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(conn.context().isInTransaction()).isFalse()) .then(conn.postAllocate()) .thenMany(conn.createStatement("SELECT * FROM test") .execute()) @@ -143,7 +143,7 @@ void shouldNotRollbackCommittedPreRelease() { .doOnNext(it -> assertThat(it).isEqualTo(1)) .then(conn.commitTransaction()) .then(conn.preRelease()) - .doOnSuccess(ignored -> assertThat(conn.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(conn.context().isInTransaction()).isFalse()) .then(conn.postAllocate()) .thenMany(conn.createStatement("SELECT * FROM test") .execute()) @@ -158,15 +158,15 @@ void transactionDefinitionLockWaitTimeout() { .beginTransaction(MySqlTransactionDefinition.empty() .lockWaitTimeout(Duration.ofSeconds(345))) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isTrue(); + assertThat(connection.context().isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isTrue(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isTrue(); }) .then(connection.rollbackTransaction()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isFalse(); + assertThat(connection.context().isInTransaction()).isFalse(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); })); } @@ -175,15 +175,15 @@ void transactionDefinitionIsolationLevel() { castedComplete(connection -> connection .beginTransaction(MySqlTransactionDefinition.from(READ_COMMITTED)) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isTrue(); + assertThat(connection.context().isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(READ_COMMITTED); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); }) .then(connection.rollbackTransaction()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isFalse(); + assertThat(connection.context().isInTransaction()).isFalse(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); })); } @@ -194,7 +194,7 @@ void setTransactionLevelNotInTransaction() { Mono.fromSupplier(connection::getTransactionIsolationLevel) .doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ)) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(REPEATABLE_READ)) .then(connection.rollbackTransaction()) @@ -203,7 +203,7 @@ void setTransactionLevelNotInTransaction() { .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED)) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) // ensure transaction isolation level applies to subsequent transactions .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED)) @@ -222,13 +222,13 @@ void setTransactionLevelInTransaction() { .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isNotEqualTo(READ_COMMITTED)) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) // ensure that session isolation level is changed after rollback .then(Mono.fromSupplier(connection::getTransactionIsolationLevel)) .doOnSuccess(it -> assertThat(it).isEqualTo(READ_COMMITTED)) // ensure transaction isolation level applies to subsequent transactions .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) ); } @@ -240,15 +240,15 @@ void transactionDefinition() { .lockWaitTimeout(Duration.ofSeconds(112)) .consistent()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isTrue(); + assertThat(connection.context().isInTransaction()).isTrue(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isTrue(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isTrue(); }) .then(connection.rollbackTransaction()) .doOnSuccess(ignored -> { - assertThat(connection.isInTransaction()).isFalse(); + assertThat(connection.context().isInTransaction()).isFalse(); assertThat(connection.getTransactionIsolationLevel()).isEqualTo(REPEATABLE_READ); - assertThat(connection.isLockWaitTimeoutChanged()).isFalse(); + assertThat(connection.context().isLockWaitTimeoutChanged()).isFalse(); })); } @@ -290,7 +290,7 @@ void createSavepointAndRollbackToSavepoint(String savepoint) { "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) .flatMap(IntegrationTestSupport::extractRowsUpdated) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (1, 'test1')") .execute())) .flatMap(IntegrationTestSupport::extractRowsUpdated) @@ -301,7 +301,7 @@ void createSavepointAndRollbackToSavepoint(String savepoint) { .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(2)) .then(connection.createSavepoint(savepoint)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (3, 'test3')") .execute())) .flatMap(IntegrationTestSupport::extractRowsUpdated) @@ -312,12 +312,12 @@ void createSavepointAndRollbackToSavepoint(String savepoint) { .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(4)) .then(connection.rollbackTransactionToSavepoint(savepoint)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(2)) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(0)) @@ -331,7 +331,7 @@ void createSavepointAndRollbackEntireTransaction(String savepoint) { "CREATE TEMPORARY TABLE test (id INT NOT NULL PRIMARY KEY, name VARCHAR(50))").execute()) .flatMap(IntegrationTestSupport::extractRowsUpdated) .then(connection.beginTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (1, 'test1')") .execute())) .flatMap(IntegrationTestSupport::extractRowsUpdated) @@ -342,7 +342,7 @@ void createSavepointAndRollbackEntireTransaction(String savepoint) { .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(2)) .then(connection.createSavepoint(savepoint)) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.createStatement("INSERT INTO test VALUES (3, 'test3')") .execute())) .flatMap(IntegrationTestSupport::extractRowsUpdated) @@ -353,7 +353,7 @@ void createSavepointAndRollbackEntireTransaction(String savepoint) { .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(4)) .then(connection.rollbackTransaction()) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) .then(Mono.from(connection.createStatement("SELECT COUNT(*) FROM test").execute())) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class)))) .doOnSuccess(count -> assertThat(count).isEqualTo(0)) @@ -374,8 +374,7 @@ void rollbackTransactionWithoutBegin() { void setTransactionIsolationLevel() { complete(connection -> Flux.just(READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE) .concatMap(level -> connection.setTransactionIsolationLevel(level) - .map(ignored -> assertThat(level)) - .doOnNext(a -> a.isEqualTo(connection.getTransactionIsolationLevel())))); + .doOnSuccess(ignored -> assertThat(level).isEqualTo(connection.getTransactionIsolationLevel())))); } @Test @@ -400,7 +399,7 @@ void commitTransactionShouldRespectQueuedMessages() { .execute(), connection.commitTransaction() )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) + .doOnComplete(() -> assertThat(connection.context().isInTransaction()).isFalse()) .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) @@ -421,7 +420,7 @@ void rollbackTransactionShouldRespectQueuedMessages() { .execute(), connection.rollbackTransaction() )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isFalse()) + .doOnComplete(() -> assertThat(connection.context().isInTransaction()).isFalse()) .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) .doOnNext(count -> assertThat(count).isEqualTo(0L))) @@ -435,15 +434,15 @@ void beginTransactionShouldRespectQueuedMessages() { Mono.from(connection.createStatement(tdl).execute()) .flatMap(IntegrationTestSupport::extractRowsUpdated) .then(Mono.from(connection.beginTransaction())) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isTrue()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isTrue()) .thenMany(Flux.merge( connection.createStatement("INSERT INTO test VALUES (1, 'test1')").execute(), connection.commitTransaction(), connection.beginTransaction() )) - .doOnComplete(() -> assertThat(connection.isInTransaction()).isTrue()) + .doOnComplete(() -> assertThat(connection.context().isInTransaction()).isTrue()) .then(Mono.from(connection.rollbackTransaction())) - .doOnSuccess(ignored -> assertThat(connection.isInTransaction()).isFalse()) + .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()) .thenMany(connection.createStatement("SELECT COUNT(*) FROM test").execute()) .flatMap(result -> Mono.from(result.map((row, metadata) -> row.get(0, Long.class))) .doOnNext(count -> assertThat(count).isEqualTo(1L))) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java index c8d50c633..b2847c20d 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnectionTest.java @@ -16,21 +16,36 @@ package io.asyncer.r2dbc.mysql; +import io.asyncer.r2dbc.mysql.api.MySqlTransactionDefinition; import io.asyncer.r2dbc.mysql.cache.Caches; +import io.asyncer.r2dbc.mysql.cache.PrepareCache; import io.asyncer.r2dbc.mysql.client.Client; +import io.asyncer.r2dbc.mysql.client.FluxExchangeable; import io.asyncer.r2dbc.mysql.codec.Codecs; +import io.asyncer.r2dbc.mysql.constant.ServerStatuses; import io.asyncer.r2dbc.mysql.message.client.ClientMessage; import io.asyncer.r2dbc.mysql.message.client.TextQueryMessage; +import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; import io.r2dbc.spi.IsolationLevel; import org.assertj.core.api.ThrowableTypeAssert; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; import reactor.test.StepVerifier; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -39,39 +54,24 @@ */ class MySqlSimpleConnectionTest { - private final Client client; - - private final Codecs codecs = mock(Codecs.class); - - private final IsolationLevel level = IsolationLevel.REPEATABLE_READ; - - private final String product = "MockConnection"; - - private final MySqlSimpleConnection noPrepare; - - MySqlSimpleConnectionTest() { - Client client = mock(Client.class); - - when(client.getContext()).thenReturn(ConnectionContextTest.mock()); - - this.client = client; - this.noPrepare = new MySqlSimpleConnection(client, - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, null); - } + private static final Codecs CODECS = mock(Codecs.class); @Test void createStatement() { String condition = "SELECT * FROM test"; - MySqlSimpleConnection allPrepare = new MySqlSimpleConnection(client, - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, sql -> true); - MySqlSimpleConnection halfPrepare = new MySqlSimpleConnection(client, - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, sql -> false); - MySqlSimpleConnection conditionPrepare = new MySqlSimpleConnection(client, - codecs, level, 50, Caches.createQueryCache(0), - Caches.createPrepareCache(0), product, sql -> sql.equals(condition)); + MySqlSimpleConnection allPrepare = new MySqlSimpleConnection( + mockClient(), + CODECS, + Caches.createQueryCache(0), sql -> true); + MySqlSimpleConnection halfPrepare = new MySqlSimpleConnection( + mockClient(), + CODECS, + Caches.createQueryCache(0), sql -> false); + MySqlSimpleConnection conditionPrepare = new MySqlSimpleConnection( + mockClient(), + CODECS, + Caches.createQueryCache(0), sql -> sql.equals(condition)); + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); assertThat(noPrepare.createStatement("SELECT * FROM test WHERE id=1")) .isExactlyInstanceOf(TextSimpleStatement.class); @@ -105,12 +105,14 @@ void createStatement() { @SuppressWarnings("ConstantConditions") @Test void badCreateStatement() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.createStatement(null)); } @SuppressWarnings("ConstantConditions") @Test void badCreateSavepoint() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); asserted.isThrownBy(() -> noPrepare.createSavepoint("")); @@ -120,6 +122,7 @@ void badCreateSavepoint() { @SuppressWarnings("ConstantConditions") @Test void badReleaseSavepoint() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); asserted.isThrownBy(() -> noPrepare.releaseSavepoint("")); @@ -129,6 +132,7 @@ void badReleaseSavepoint() { @SuppressWarnings("ConstantConditions") @Test void badRollbackTransactionToSavepoint() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); ThrowableTypeAssert asserted = assertThatIllegalArgumentException(); asserted.isThrownBy(() -> noPrepare.rollbackTransactionToSavepoint("")); @@ -138,24 +142,120 @@ void badRollbackTransactionToSavepoint() { @SuppressWarnings("ConstantConditions") @Test void badSetTransactionIsolationLevel() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.setTransactionIsolationLevel(null)); } - @Test - void shouldSetTransactionIsolationLevelSuccessfully() { - ClientMessage message = new TextQueryMessage("SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE"); + @ParameterizedTest + @ValueSource(strings = { "READ UNCOMMITTED", "READ COMMITTED", "REPEATABLE READ", "SERIALIZABLE" }) + void shouldSetTransactionIsolationLevelSuccessfully(String levelSql) { + Client client = mockClient(); + IsolationLevel level = IsolationLevel.valueOf(levelSql); + ClientMessage message = new TextQueryMessage("SET SESSION TRANSACTION ISOLATION LEVEL " + levelSql); + when(client.exchange(eq(message), any())).thenReturn(Flux.empty()); - noPrepare.setTransactionIsolationLevel(IsolationLevel.SERIALIZABLE) + MySqlSimpleConnection noPrepare = newNoPrepare(client); + noPrepare.setTransactionIsolationLevel(level) .as(StepVerifier::create) .verifyComplete(); - assertThat(noPrepare.getSessionTransactionIsolationLevel()).isEqualTo(IsolationLevel.SERIALIZABLE); + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(level); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(level); + } + + @ParameterizedTest + @ValueSource(strings = { + "READ UNCOMMITTED,SERIALIZABLE", + "READ COMMITTED,REPEATABLE READ", + "REPEATABLE READ,READ UNCOMMITTED" + }) + void shouldSetTransactionIsolationLevelInTransaction(String levels) { + String[] levelStatements = levels.split(","); + IsolationLevel currentLevel = IsolationLevel.valueOf(levelStatements[0]); + IsolationLevel sessionLevel = IsolationLevel.valueOf(levelStatements[1]); + Client client = mockClient(); + ClientMessage session = new TextQueryMessage("SET SESSION TRANSACTION ISOLATION LEVEL " + sessionLevel.asSql()); + CompleteMessage mockDone = mock(CompleteMessage.class); + @SuppressWarnings("unchecked") + SynchronousSink sink = (SynchronousSink) mock(SynchronousSink.class); + AtomicBoolean completed = new AtomicBoolean(false); + + doAnswer(it -> { + throw it.getArgument(0, Exception.class); + }).when(sink).error(any()); + doAnswer(it -> { + completed.set(true); + return null; + }).when(sink).complete(); + when(mockDone.isDone()).thenReturn(true); + when(client.exchange(eq(session), any())).thenReturn(Flux.empty()); + when(client.exchange(any())).thenAnswer(it -> { + FluxExchangeable exchangeable = it.getArgument(0); + @SuppressWarnings("unchecked") + CoreSubscriber subscriber = mock(CoreSubscriber.class); + exchangeable.subscribe(subscriber); + + while (!completed.get()) { + exchangeable.accept(mockDone, sink); + } + + // Mock server status to be in transaction + client.getContext().setServerStatuses(ServerStatuses.IN_TRANSACTION); + + return Flux.empty(); + }); + + IsolationLevel mockLevel = IsolationLevel.valueOf("DEFAULT"); + client.getContext().initSession( + mock(PrepareCache.class), + mockLevel, + false, + Duration.ZERO, + null, + null + ); + MySqlSimpleConnection noPrepare = newNoPrepare(client); + + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(mockLevel); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(mockLevel); + + noPrepare.beginTransaction(MySqlTransactionDefinition.from(currentLevel)) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(currentLevel); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(mockLevel); + + noPrepare.setTransactionIsolationLevel(sessionLevel) + .as(StepVerifier::create) + .verifyComplete(); + + assertThat(client.getContext().getCurrentIsolationLevel()).isEqualTo(currentLevel); + assertThat(client.getContext().getSessionIsolationLevel()).isEqualTo(sessionLevel); } @SuppressWarnings("ConstantConditions") @Test void badValidate() { + MySqlSimpleConnection noPrepare = newNoPrepare(mockClient()); assertThatIllegalArgumentException().isThrownBy(() -> noPrepare.validate(null)); } + + private static Client mockClient() { + Client client = mock(Client.class); + + when(client.getContext()).thenReturn(ConnectionContextTest.mock()); + + return client; + } + + private static MySqlSimpleConnection newNoPrepare(Client client) { + return new MySqlSimpleConnection( + client, + CODECS, + Caches.createQueryCache(0), + null + ); + } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java index 345704af5..94e1591f4 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareParameterizedStatementTest.java @@ -52,8 +52,7 @@ public PrepareParameterizedStatement makeInstance(boolean isMariaDB, String sql, return new PrepareParameterizedStatement( client, codecs, - Query.parse(sql), - Caches.createPrepareCache(0) + Query.parse(sql) ); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java index 0e18e7233..56d5ac907 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/PrepareSimpleStatementTest.java @@ -16,7 +16,6 @@ package io.asyncer.r2dbc.mysql; -import io.asyncer.r2dbc.mysql.cache.Caches; import io.asyncer.r2dbc.mysql.client.Client; import io.asyncer.r2dbc.mysql.codec.Codecs; @@ -64,12 +63,7 @@ public PrepareSimpleStatement makeInstance(boolean isMariaDB, String ignored, St when(client.getContext()).thenReturn(ConnectionContextTest.mock(isMariaDB)); - return new PrepareSimpleStatement( - client, - codecs, - sql, - Caches.createPrepareCache(0) - ); + return new PrepareSimpleStatement(client, codecs, sql); } @Override From ea1881bca7716e8728825992fc3049d237903ce1 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 28 Mar 2024 11:45:48 +0900 Subject: [PATCH 43/93] Move ping command to QueryFlow --- .../r2dbc/mysql/MySqlSimpleConnection.java | 27 +++---------------- .../io/asyncer/r2dbc/mysql/PingStatement.java | 2 +- .../io/asyncer/r2dbc/mysql/QueryFlow.java | 23 ++++++++++++++++ 3 files changed, 27 insertions(+), 25 deletions(-) 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 index 829359040..8f8665a60 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlSimpleConnection.java @@ -25,7 +25,6 @@ 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.client.PingMessage; import io.asyncer.r2dbc.mysql.message.server.CompleteMessage; import io.asyncer.r2dbc.mysql.message.server.ErrorMessage; import io.asyncer.r2dbc.mysql.message.server.ServerMessage; @@ -38,12 +37,9 @@ import io.r2dbc.spi.ValidationDepth; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.SynchronousSink; import java.time.Duration; -import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; @@ -66,8 +62,7 @@ final class MySqlSimpleConnection implements MySqlConnection { if (message instanceof ErrorMessage) { ErrorMessage msg = (ErrorMessage) message; - logger.debug("Remote validate failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), - msg.getMessage()); + logger.debug("Remote validate failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), msg.getMessage()); } else { ReferenceCountUtil.safeRelease(message); } @@ -75,18 +70,6 @@ final class MySqlSimpleConnection implements MySqlConnection { return false; }; - private static final BiConsumer> PING = (message, sink) -> { - if (message instanceof ErrorMessage) { - sink.next(message); - sink.complete(); - } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { - sink.next(message); - sink.complete(); - } else { - ReferenceCountUtil.safeRelease(message); - } - }; - private final Client client; private final Codecs codecs; @@ -266,9 +249,9 @@ public Mono validate(ValidationDepth depth) { return Mono.just(false); } - return doPingInternal(client) - .last() + 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. @@ -334,8 +317,4 @@ public Mono setStatementTimeout(Duration timeout) { ConnectionContext context() { return client.getContext(); } - - static Flux doPingInternal(Client client) { - return client.exchange(PingMessage.INSTANCE, PING); - } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java index e64aec930..9cdc43c95 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/PingStatement.java @@ -69,7 +69,7 @@ public Flux execute() { client, codecs, null, - MySqlSimpleConnection.doPingInternal(client) + QueryFlow.ping(client) ))); } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java index 23ce5e806..3bae1dcc3 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/QueryFlow.java @@ -63,6 +63,7 @@ import java.util.List; 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; @@ -86,6 +87,18 @@ 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 @@ -252,6 +265,16 @@ static Mono createSavepoint(Client client, String name, boolean batchSuppo return client.exchange(new TransactionMultiExchangeable(savepointState)).then(); } + /** + * Executes a ping command to the server. + * + * @param client the {@link Client} to exchange messages with. + * @return complete or error messages received in response to this exchange. + */ + static Flux ping(Client client) { + return client.exchange(PingMessage.INSTANCE, PING); + } + /** * Sets a session variable to the server. * From e97dc7d03ce57bd4e05fda8e6918db5917e9057d Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Mon, 1 Apr 2024 10:28:53 +0900 Subject: [PATCH 44/93] Polishing InitFlow --- .../java/io/asyncer/r2dbc/mysql/InitFlow.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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 index 32dcc1c8a..753ff7f8a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java @@ -126,12 +126,19 @@ final class InitFlow { * @param password the password of the {@code user}. * @param compressionAlgorithms the list of compression algorithms. * @param zstdCompressionLevel the zstd compression level. - * @return a {@link Flux} that indicates the initialization is done, or an error if the initialization failed. + * @return a {@link Mono} that indicates the initialization is done, or an error if the initialization failed. */ - static Flux initHandshake(Client client, SslMode sslMode, String database, String user, + 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)); + return client.exchange(new HandshakeExchangeable( + client, + sslMode, + database, + user, + password, + compressionAlgorithms, + zstdCompressionLevel + )).then(); } /** From faa30a70bd5d771bfc1e5627576ef5aa2f85b0d4 Mon Sep 17 00:00:00 2001 From: Mirro Mutth Date: Thu, 28 Mar 2024 15:02:22 +0900 Subject: [PATCH 45/93] Add support for SQL mode NO_BACKSLASH_ESCAPES --- .../r2dbc/mysql/ConnectionContext.java | 4 ++ .../java/io/asyncer/r2dbc/mysql/InitFlow.java | 42 ++++++++-------- .../r2dbc/mysql/constant/ServerStatuses.java | 12 ++++- .../mysql/message/client/ParamWriter.java | 32 ++++++++----- .../client/PreparedTextQueryMessage.java | 2 +- .../mysql/ConnectionIntegrationTest.java | 48 +++++++++++++++++++ .../mysql/QueryIntegrationTestSupport.java | 2 +- .../r2dbc/mysql/codec/CodecTestSupport.java | 2 +- .../r2dbc/mysql/codec/SetCodecTest.java | 2 +- .../mysql/message/client/ParamWriterTest.java | 45 ++++++++++++----- .../message/client/ParameterWriterHelper.java | 4 +- 11 files changed, 140 insertions(+), 55 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index 34011c6e1..cc5aeb2a9 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -216,6 +216,10 @@ public boolean isMariaDb() { return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb(); } + public boolean isNoBackslashEscapes() { + return (serverStatuses & ServerStatuses.NO_BACKSLASH_ESCAPES) != 0; + } + @Override public ZeroDateOption getZeroDateOption() { return zeroDateOption; 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 index 753ff7f8a..a7c13c596 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java @@ -209,31 +209,31 @@ private static Mono loadAndInitInnoDbEngineStatus( 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); + 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))); + 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))); } - })) - .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); - } + 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) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java index 685dae9a7..f90c6c7da 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java @@ -50,12 +50,20 @@ public final class ServerStatuses { public static final short LAST_ROW_SENT = 128; // public static final short DB_DROPPED = 256; -// public static final short NO_BACKSLASH_ESCAPES = 512; + + /** + * Server does not permit backslash escapes. + * + * @since 1.1.3 + */ + public static final short NO_BACKSLASH_ESCAPES = 512; + // public static final short METADATA_CHANGED = 1024; // public static final short QUERY_WAS_SLOW = 2048; // public static final short PS_OUT_PARAMS = 4096; // public static final short IN_TRANS_READONLY = 8192; // public static final short SESSION_STATE_CHANGED = 16384; - private ServerStatuses() { } + private ServerStatuses() { + } } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java index 875e5e18d..a22a64b8b 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java @@ -45,14 +45,17 @@ final class ParamWriter extends ParameterWriter { private final StringBuilder builder; + private final boolean noBackslashEscapes; + private final Query query; private int index; private Mode mode; - private ParamWriter(Query query) { + private ParamWriter(boolean noBackslashEscapes, Query query) { this.builder = newBuilder(query); + this.noBackslashEscapes = noBackslashEscapes; this.query = query; this.index = 1; this.mode = 1 < query.getPartSize() ? Mode.AVAILABLE : Mode.FULL; @@ -318,15 +321,19 @@ private void write0(char[] s, int off, int len) { } private void escape(char c) { + if (c == '\'') { + // MySQL will auto-combine consecutive strings, whatever backslash is used or not, e.g. '1''2' -> '1\'2' + builder.append('\'').append('\''); + return; + } else if (noBackslashEscapes) { + builder.append(c); + return; + } + switch (c) { case '\\': builder.append('\\').append('\\'); break; - case '\'': - // MySQL will auto-combine consecutive strings, like '1''2' -> '12'. - // Sure, there can use '1\'2', but this will be better. (For some logging systems) - builder.append('\'').append('\''); - break; // Maybe useful in the future, keep '"' here. // case '"': buf.append('\\').append('"'); break; // SHIFT-JIS, WINDOWS-932, EUC-JP and eucJP-OPEN will encode '\u00a5' (the sign of Japanese Yen @@ -335,20 +342,19 @@ private void escape(char c) { // case '\u00a5': do something; break; // case '\u20a9': do something; break; case 0: - // MySQL is based on C/C++, must escape '\0' which is an end flag in C style string. + // Should escape '\0' which is an end flag in C style string. builder.append('\\').append('0'); break; case '\032': - // It seems like a problem on Windows 32, maybe check current OS here? + // It gives some problems on Win32. builder.append('\\').append('Z'); break; case '\n': - // Should escape it for some logging such as Relational Database Service (RDS) Logging - // System, etc. Sure, it is not necessary, but this will be better. + // Should be escaped for better logging. builder.append('\\').append('n'); break; case '\r': - // Should escape it for some logging such as RDS Logging System, etc. + // Should be escaped for better logging. builder.append('\\').append('r'); break; default: @@ -357,9 +363,9 @@ private void escape(char c) { } } - static Mono publish(Query query, Flux values) { + static Mono publish(boolean noBackslashEscapes, Query query, Flux values) { return Mono.defer(() -> { - ParamWriter writer = new ParamWriter(query); + ParamWriter writer = new ParamWriter(noBackslashEscapes, query); return OperatorUtils.discardOnCancel(values) .doOnDiscard(MySqlParameter.class, DISPOSE) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java index d6ea6d783..6cd7edf9c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java @@ -87,7 +87,7 @@ public Mono encode(ByteBufAllocator allocator, ConnectionContext contex return Flux.fromArray(values); }); - return ParamWriter.publish(query, parameters).handle((it, sink) -> { + return ParamWriter.publish(context.isNoBackslashEscapes(), query, parameters).handle((it, sink) -> { ByteBuf buf = allocator.buffer(); try { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index 8fa06f1f9..b65b3b447 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -50,6 +50,7 @@ import java.util.Collections; import java.util.Objects; import java.util.function.Function; +import java.util.stream.Stream; import static io.r2dbc.spi.IsolationLevel.READ_COMMITTED; import static io.r2dbc.spi.IsolationLevel.READ_UNCOMMITTED; @@ -80,6 +81,53 @@ void isInTransaction() { .doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse())); } + @ParameterizedTest + @ValueSource(strings = { + "test", + "test`data", + "test\ndata", + "I'm feeling good", + }) + void sqlModeNoBackslashEscapes(String value) { + String tdl = "CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` VARCHAR(50) NOT NULL)"; + + // Add NO_BACKSLASH_ESCAPES instead of replace + castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.context().isNoBackslashEscapes()) + .isFalse()) + .thenMany(connection.createStatement(tdl).execute()) + .flatMap(MySqlResult::getRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO test VALUES (1, ?)") + .bind(0, value) + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .thenMany(connection.createStatement("SELECT COUNT(0) FROM `test` WHERE `value` = ?") + .bind(0, value) + .execute()) + .flatMap(result -> result.map((row, metadata) -> row.get(0, Integer.class))) + .collectList() + .doOnNext(counts -> assertThat(counts).isEqualTo(Collections.singletonList(1))) + .thenMany(connection.createStatement("SELECT @@sql_mode").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get(0, String.class))) + .map(modes -> Stream.concat(Stream.of(modes.split(",")), Stream.of("NO_BACKSLASH_ESCAPES")) + .toArray(String[]::new)) + .last() + .flatMapMany(modes -> connection.createStatement("SET sql_mode = ?") + .bind(0, modes) + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .doOnComplete(() -> assertThat(connection.context().isNoBackslashEscapes()).isTrue()) + .thenMany(connection.createStatement("INSERT INTO test VALUES (2, ?)") + .bind(0, value) + .execute()) + .flatMap(MySqlResult::getRowsUpdated) + .thenMany(connection.createStatement("SELECT COUNT(0) FROM `test` WHERE `value` = ?") + .bind(0, value) + .execute()) + .flatMap(result -> result.map((row, metadata) -> row.get(0, Integer.class))) + .collectList() + .doOnNext(counts -> assertThat(counts).isEqualTo(Collections.singletonList(2)))); + } + @DisabledIf("envIsLessThanMySql56") @Test void startTransaction() { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java index fda842503..1f75d3c7b 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java @@ -564,7 +564,7 @@ void insertOnDuplicate() { .bind(2, 20) .execute()) .flatMap(IntegrationTestSupport::extractRowsUpdated) - .doOnNext(it -> assertThat(it).isOne()) // TODO: check capability flag + .doOnNext(it -> assertThat(it).isOne()) .thenMany(connection.createStatement("SELECT value FROM test WHERE id=?") .bind(0, 1) .execute()) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java index 7a9471793..380d3c6de 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java @@ -91,7 +91,7 @@ default void encodeStringify() { Query query = Query.parse("?"); for (int i = 0; i < origin.length; ++i) { - ParameterWriter writer = ParameterWriterHelper.get(query); + ParameterWriter writer = ParameterWriterHelper.get(false, query); codec.encode(origin[i], context()) .publishText(writer) .as(StepVerifier::create) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java index ac50501a5..a2155a578 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java @@ -129,7 +129,7 @@ void stringifySet() { Query query = Query.parse("?"); for (int i = 0; i < sets.length; ++i) { - ParameterWriter writer = ParameterWriterHelper.get(query); + ParameterWriter writer = ParameterWriterHelper.get(false, query); codec.encode(sets[i], context()) .publishText(writer) .as(StepVerifier::create) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java index 49f2dff65..ed05076fe 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java @@ -18,6 +18,8 @@ import io.asyncer.r2dbc.mysql.Query; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -84,42 +86,42 @@ void badFollowNull() { @Test void appendPart() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.append("define", 2, 5); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'fin'"); } @Test void writePart() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write("define", 2, 3); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'fin'"); } @Test void appendNull() { - assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(parameterOnly(1)).append(null))) + assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(false, parameterOnly(1)).append(null))) .isEqualTo("'null'"); - assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(parameterOnly(1)) + assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(false, parameterOnly(1)) .append(null, 1, 3))) .isEqualTo("'ul'"); } @Test void writeNull() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((String) null); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'null'"); - writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((String) null, 1, 2); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'ul'"); - writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((char[]) null); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'null'"); - writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write((char[]) null, 1, 2); assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'ul'"); } @@ -132,7 +134,7 @@ void publishSuccess() { values[i] = new MockMySqlParameter(true); } - Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values))) + Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values))) .as(StepVerifier::create) .expectNext(new String(new char[SIZE]).replace("\0", "''")) .verifyComplete(); @@ -154,7 +156,7 @@ void publishPartially() { values[i] = new MockMySqlParameter(false); } - Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values))) + Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values))) .as(StepVerifier::create) .verifyError(MockException.class); @@ -169,13 +171,30 @@ void publishNothing() { values[i] = new MockMySqlParameter(false); } - Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values))) + Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values))) .as(StepVerifier::create) .verifyError(MockException.class); assertThat(values).extracting(MockMySqlParameter::refCnt).containsOnly(0); } + @ParameterizedTest + @ValueSource(strings = { + "abc", + "a'b'c", + "a\nb\rc", + "a\"b\"c", + "a\\b\\c", + "a\0b\0c", + "a\u00a5b\u20a9c", + "a\032b\032c", + }) + void noBackslashEscapes(String value) { + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(true, parameterOnly(1)); + writer.write(value); + assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'" + value.replaceAll("'", "''") + "'"); + } + private static Query parameterOnly(int parameters) { char[] chars = new char[parameters]; Arrays.fill(chars, '?'); @@ -184,13 +203,13 @@ private static Query parameterOnly(int parameters) { } private static ParamWriter stringWriter() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.write('0'); return writer; } private static ParamWriter nullWriter() { - ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1)); + ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1)); writer.writeNull(); return writer; } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java index b532546cc..eacaa349a 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParameterWriterHelper.java @@ -45,10 +45,10 @@ public final class ParameterWriterHelper { ReflectionUtils.findMethod(ParamWriter.class, "toSql") .orElseThrow(RuntimeException::new); - public static ParameterWriter get(Query query) { + public static ParameterWriter get(boolean noBackslashEscapes, Query query) { assertThat(query.getPartSize()).isGreaterThan(1); - return ReflectionUtils.newInstance(CONSTRUCTOR, query); + return ReflectionUtils.newInstance(CONSTRUCTOR, noBackslashEscapes, query); } public static String toSql(ParameterWriter writer) { From 52bc71f193949baf305356b18276eecc2603b584 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 4 Apr 2024 10:28:27 +0900 Subject: [PATCH 46/93] Apply TestContainer (#264) Motivation: Enhance test reliability and environment consistency by integrating TestContainers Modification: Added TestContainers dependency and updated tests to run with containerized DBs. Expanded test scope to include MySQL and MariaDB versions to cover conditional test cases. Result: Improved test suite stability and simplified setup for contributors. Resolves #76 Co-authored-by: saurabhyadav1985 <49230235+saurabhyadav1985@users.noreply.github.com> Co-authored-by: Mirro Mutth --- .github/workflows/ci-graalvm-tests.yml | 12 +- .github/workflows/ci-integration-tests.yml | 12 +- .../ci-mariadb-intergration-tests.yml | 14 +- containers/mariadb-compose.yml | 12 -- containers/mysql-compose.yml | 12 -- r2dbc-mysql/pom.xml | 20 ++- .../mysql/message/server/ErrorMessage.java | 1 + .../r2dbc/mysql/IntegrationTestSupport.java | 65 +++---- .../mysql/JacksonIntegrationTestSupport.java | 2 +- .../r2dbc/mysql/MySqlTestKitSupport.java | 3 + .../mysql/QueryIntegrationTestSupport.java | 13 +- .../mysql/SessionStateIntegrationTest.java | 40 +++-- .../r2dbc/mysql/SslTunnelIntegrationTest.java | 34 ++-- .../r2dbc/mysql/TimeZoneIntegrationTest.java | 33 ++-- .../mysql/ZstdCompressionIntegrationTest.java | 13 +- .../internal/util/TestContainerExtension.java | 160 ++++++++++++++++++ .../mysql/internal/util/TestServerUtil.java | 137 +++++++++++++++ .../internal/util/TestServerUtilTest.java | 32 ++++ .../resources/testcontainer/mysql-5.5/my.cnf | 49 ++++++ .../src/main/java/io/asyncer/Main.java | 4 +- 20 files changed, 493 insertions(+), 175 deletions(-) delete mode 100644 containers/mariadb-compose.yml delete mode 100644 containers/mysql-compose.yml create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestContainerExtension.java create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtil.java create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtilTest.java create mode 100644 r2dbc-mysql/src/test/resources/testcontainer/mysql-5.5/my.cnf diff --git a/.github/workflows/ci-graalvm-tests.yml b/.github/workflows/ci-graalvm-tests.yml index 46135627d..ee5f0d03d 100644 --- a/.github/workflows/ci-graalvm-tests.yml +++ b/.github/workflows/ci-graalvm-tests.yml @@ -24,13 +24,6 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up MySQL - env: - MYSQL_DATABASE: r2dbc - MYSQL_ROOT_PASSWORD: r2dbc-password!@ - MYSQL_VERSION: 8.1 - run: docker-compose -f ${{ github.workspace }}/containers/mysql-compose.yml up -d - - uses: graalvm/setup-graalvm@v1 with: java-version: 21 @@ -38,6 +31,9 @@ jobs: native-image-job-reports: true github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Start Bundled MySQL + run: sudo service mysql start + - name: Cache & Load Local Maven Repository uses: actions/cache@v3 with: @@ -50,4 +46,4 @@ jobs: echo "JAVA_HOME=$JAVA_HOME" echo "./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true" ./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true - ./test-native-image/target/test-native-image -Dtest.mysql.password=r2dbc-password!@ + ./test-native-image/target/test-native-image diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index f1ed4ab57..6f0e151ff 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - mysql-version: [ 5.5, 5.6, 5.7, 8.0, 8.1, 8.2 ] + mysql-version: [ 5.5, 5.6.45, 5.6, 5.7.28, 5.7, 8.0, 8.1, 8.2, 8.3] name: Integration test with MySQL ${{ matrix.mysql-version }} steps: - uses: actions/checkout@v3 @@ -21,19 +21,13 @@ jobs: cache: maven - name: Shutdown the Default MySQL run: sudo service mysql stop - - name: Set up MySQL ${{ matrix.mysql-version }} - env: - MYSQL_DATABASE: r2dbc - MYSQL_ROOT_PASSWORD: r2dbc-password!@ - MYSQL_VERSION: ${{ matrix.mysql-version }} - run: docker-compose -f ${{ github.workspace }}/containers/mysql-compose.yml up -d - name: Integration test with MySQL ${{ matrix.mysql-version }} run: | set -o pipefail ./mvnw -B verify -Dmaven.javadoc.skip=true \ -Dmaven.surefire.skip=true \ - -Dtest.mysql.password=r2dbc-password!@ \ - -Dtest.mysql.version=${{ matrix.mysql-version }} \ + -Dtest.db.type=mysql \ + -Dtest.db.version=${{ matrix.mysql-version }} \ -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ -Dio.netty.leakDetectionLevel=paranoid \ -Dio.netty.leakDetection.targetRecords=32 \ diff --git a/.github/workflows/ci-mariadb-intergration-tests.yml b/.github/workflows/ci-mariadb-intergration-tests.yml index 959e8a3cc..d20bba01a 100644 --- a/.github/workflows/ci-mariadb-intergration-tests.yml +++ b/.github/workflows/ci-mariadb-intergration-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - mariadb-version: [ 10.6, 10.11 ] + mariadb-version: [ 10.0, 10.1, 10.2.15, 10.2, 10.3.7, 10.3, 10.5.1, 10.5, 10.6, 10.11] name: Integration test with MariaDB ${{ matrix.mariadb-version }} steps: - uses: actions/checkout@v3 @@ -21,21 +21,13 @@ jobs: cache: maven - name: Shutdown the Default MySQL run: sudo service mysql stop - - name: Set up MariaDB ${{ matrix.mariadb-version }} - env: - MYSQL_DATABASE: r2dbc - MYSQL_ROOT_PASSWORD: r2dbc-password!@ - MARIADB_VERSION: ${{ matrix.mariadb-version }} - run: docker-compose -f ${{ github.workspace }}/containers/mariadb-compose.yml up -d - - name: Integration test with MySQL ${{ matrix.mysql-version }} + - name: Integration test with MariaDB ${{ matrix.mysql-version }} run: | set -o pipefail ./mvnw -B verify -Dmaven.javadoc.skip=true \ -Dmaven.surefire.skip=true \ - -Dtest.mysql.password=r2dbc-password!@ \ - -Dtest.mysql.version=${{ matrix.mariadb-version }} \ -Dtest.db.type=mariadb \ - -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ + -Dtest.db.version=${{ matrix.mariadb-version }} \ -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ -Dio.netty.leakDetectionLevel=paranoid \ -Dio.netty.leakDetection.targetRecords=32 \ diff --git a/containers/mariadb-compose.yml b/containers/mariadb-compose.yml deleted file mode 100644 index a126b551b..000000000 --- a/containers/mariadb-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "3" - -services: - mariadb: - image: mariadb:${MARIADB_VERSION} - container_name: mariadb_${MARIADB_VERSION} - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE: ${MYSQL_DATABASE} - command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - ports: - - "3306:3306" diff --git a/containers/mysql-compose.yml b/containers/mysql-compose.yml deleted file mode 100644 index 2929f250e..000000000 --- a/containers/mysql-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "3" - -services: - mariadb: - image: mysql:${MYSQL_VERSION} - container_name: mysql_${MYSQL_VERSION} - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE: ${MYSQL_DATABASE} - command: mysqld --local-infile=true --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci - ports: - - "3306:3306" diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 6a0b4194b..e126c48d6 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -83,7 +83,8 @@ 1.5.3 4.11.0 8.3.0 - 1.19.6 + 3.3.3 + 1.19.7 4.0.3 5.3.32 2.16.1 @@ -241,6 +242,12 @@ ${mysql.version} test + + org.mariadb.jdbc + mariadb-java-client + ${mariadb.version} + test + com.zaxxer HikariCP @@ -270,6 +277,17 @@ + + org.testcontainers + mariadb + test + + + org.slf4j + slf4j-api + + + com.fasterxml.jackson.core jackson-core diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java index a7ef07fb8..feef32599 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/server/ErrorMessage.java @@ -97,6 +97,7 @@ public R2dbcException toException(@Nullable String sql) { case 1907: // Statement executing timeout case 3024: // Query execution was interrupted, maximum statement execution time exceeded case 1969: // Query execution was interrupted + case 1968: // Query execution was interrupted (max_statement_time exceeded) return new R2dbcTimeoutException(message, sqlState, code); case 1613: // Transaction rollback because of took too long return new R2dbcRollbackException(message, sqlState, code); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java index 5e5d046c0..796ebc5c0 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/IntegrationTestSupport.java @@ -17,9 +17,12 @@ package io.asyncer.r2dbc.mysql; import io.asyncer.r2dbc.mysql.api.MySqlConnection; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; import io.r2dbc.spi.R2dbcBadGrammarException; import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.Result; +import org.junit.jupiter.api.extension.ExtendWith; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,6 +41,7 @@ /** * Base class considers connection factory and general function for integration tests. */ +@ExtendWith(TestContainerExtension.class) abstract class IntegrationTestSupport { private final MySqlConnectionFactory connectionFactory; @@ -82,11 +86,6 @@ static Mono extractRowsUpdated(Result result) { static MySqlConnectionConfiguration configuration( Function customizer ) { - String password = System.getProperty("test.mysql.password"); - - assertThat(password).withFailMessage("Property test.mysql.password must exists and not be empty") - .isNotNull() - .isNotEmpty(); String localInfilePath; @@ -99,73 +98,47 @@ static MySqlConnectionConfiguration configuration( } MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() - .host("127.0.0.1") + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()) .connectTimeout(Duration.ofSeconds(3)) - .user("root") - .password(password) - .database("r2dbc") .allowLoadLocalInfileInPath(localInfilePath); return customizer.apply(builder).build(); } boolean envIsLessThanMySql56() { - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return true; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + if (TestServerUtil.isMariaDb()) { return false; } - + final ServerVersion ver = TestServerUtil.getServerVersion(); return ver.isLessThan(ServerVersion.create(5, 6, 0)); } - boolean envIsLessThanMySql57OrMariaDb102() { - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return true; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + boolean envIsLessThanMySql578OrMariaDb102() { + final ServerVersion ver = TestServerUtil.getServerVersion(); + if (TestServerUtil.isMariaDb()) { return ver.isLessThan(ServerVersion.create(10, 2, 0)); } - return ver.isLessThan(ServerVersion.create(5, 7, 0)); + return ver.isLessThan(ServerVersion.create(5, 7, 8)); } static boolean envIsMariaDb10_5_1() { - String type = System.getProperty("test.db.type"); - - if (!"mariadb".equalsIgnoreCase(type)) { + if (!TestServerUtil.isMariaDb()) { return false; } - ServerVersion ver = ServerVersion.parse(System.getProperty("test.mysql.version")); + final ServerVersion ver = TestServerUtil.getServerVersion(); return ver.isGreaterThanOrEqualTo(ServerVersion.create(10, 5, 1)); } boolean envIsLessThanMySql574OrMariaDb1011() { - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return true; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + final ServerVersion ver = TestServerUtil.getServerVersion(); + if (TestServerUtil.isMariaDb()) { return ver.isLessThan(ServerVersion.create(10, 1, 1)); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java index 10e138607..6fb0bce1a 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/JacksonIntegrationTestSupport.java @@ -67,7 +67,7 @@ void tearDown() { JacksonCodecRegistrar.tearDown(); } - @DisabledIf("envIsLessThanMySql57OrMariaDb102") + @DisabledIf("envIsLessThanMySql578OrMariaDb102") @Test void json() { create().flatMap(connection -> Mono.from(connection.createStatement(TDL).execute()) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java index 7b85d4150..e1760cef6 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlTestKitSupport.java @@ -17,7 +17,9 @@ package io.asyncer.r2dbc.mysql; import com.zaxxer.hikari.HikariDataSource; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; import io.r2dbc.spi.test.TestKit; +import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.jdbc.core.JdbcTemplate; import java.time.Duration; @@ -26,6 +28,7 @@ /** * Base class considers integration tests of {@link TestKit}. */ +@ExtendWith(TestContainerExtension.class) abstract class MySqlTestKitSupport implements TestKit { private final MySqlConnectionFactory connectionFactory; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java index 1f75d3c7b..3afd45a9b 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java @@ -286,7 +286,7 @@ void set() { EnumSet.of(EnumData.ONE, EnumData.THREE)); } - @DisabledIf("envIsLessThanMySql57OrMariaDb102") + @DisabledIf("envIsLessThanMySql578OrMariaDb102") @Test void json() { testType(String.class, false, "JSON", null, "{\"data\": 1}", "[\"data\", 1]", "1", "null", @@ -576,7 +576,7 @@ void insertOnDuplicate() { /** * ref: Issue 91 */ - @DisabledIf("envIsLessThanMySql57OrMariaDb102") + @DisabledIf("envIsLessThanMySql578OrMariaDb102") @Test void testUnionQueryWithJsonColumnDecodedAsString() { complete(connection -> @@ -619,11 +619,18 @@ void testUnionQueryWithJsonColumnDecodedAsString() { @Test @DisabledIf("envIsLessThanMySql574OrMariaDb1011") void setStatementTimeoutTest() { - final String sql = "SELECT 1 WHERE SLEEP(1) > 1"; + final String sql = "SELECT COUNT(*) " + + "FROM information_schema.tables a cross join " + + "information_schema.tables b cross join " + + "information_schema.tables c cross join " + + "information_schema.tables d cross join " + + "information_schema.tables e"; + timeout(connection -> connection.setStatementTimeout(Duration.ofMillis(500)) .then(Mono.from(connection.createStatement(sql).execute())) .flatMapMany(result -> Mono.from(result.map((row, metadata) -> row.get(0, String.class)))) .collectList() + .doOnNext(System.out::println) ); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java index d44608d55..cc61d1181 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SessionStateIntegrationTest.java @@ -17,9 +17,12 @@ package io.asyncer.r2dbc.mysql; import io.asyncer.r2dbc.mysql.internal.util.StringUtils; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; import io.r2dbc.spi.R2dbcTimeoutException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -38,6 +41,7 @@ /** * Integration tests for session states. */ +@ExtendWith(TestContainerExtension.class) class SessionStateIntegrationTest { @Test @@ -146,11 +150,18 @@ void initLockWaitTimeout(String timeout) { @ParameterizedTest @ValueSource(strings = { "PT0.1S", "PT0.5S" }) void initStatementTimeout(String timeout) { + final String sql = "SELECT COUNT(*) " + + "FROM information_schema.tables a cross join " + + "information_schema.tables b cross join " + + "information_schema.tables c cross join " + + "information_schema.tables d cross join " + + "information_schema.tables e"; + Duration statementTimeout = Duration.parse(timeout); connectionFactory(builder -> builder.statementTimeout(statementTimeout)) .create() - .flatMapMany(connection -> connection.createStatement("SELECT 1 WHERE SLEEP(1) > 1").execute() + .flatMapMany(connection -> connection.createStatement(sql).execute() .flatMap(result -> result.map(r -> r.get(0))) .onErrorResume(e -> connection.close().then(Mono.error(e))) .concatWith(connection.close().then(Mono.empty())) @@ -160,16 +171,8 @@ void initStatementTimeout(String timeout) { } static boolean isGreaterThanOrEqualToMariaDB10_1_1MySql5_7_4() { - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return false; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + final ServerVersion ver = TestServerUtil.getServerVersion(); + if (TestServerUtil.isMariaDb()) { return ver.isGreaterThanOrEqualTo(ServerVersion.create(10, 1, 1)); } @@ -187,18 +190,13 @@ static Stream sessionVariables() { private static MySqlConnectionFactory connectionFactory( Function customizer ) { - String password = System.getProperty("test.mysql.password"); - - if (password == null || password.isEmpty()) { - throw new IllegalStateException("Property test.mysql.password must exists and not be empty"); - } MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() - .host("localhost") - .port(3306) - .user("root") - .password(password) - .database("r2dbc"); + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()); return MySqlConnectionFactory.from(customizer.apply(builder).build()); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java index 7313be994..d7723fa15 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/SslTunnelIntegrationTest.java @@ -18,6 +18,8 @@ import io.asyncer.r2dbc.mysql.api.MySqlConnection; import io.asyncer.r2dbc.mysql.constant.SslMode; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.Unpooled; @@ -39,6 +41,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -49,6 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat; +@ExtendWith(TestContainerExtension.class) public class SslTunnelIntegrationTest { private SelfSignedCertificate server; @@ -62,19 +66,14 @@ static void initCachingSha2Password() { // If the server uses caching_sha2_password, the first time a client connects to the server, the // server will require a native SSL connection. So all the SSL tunnel tests should be run after // the caching_sha2_password initialization. - String password = System.getProperty("test.mysql.password"); - - assertThat(password).withFailMessage("Property test.mysql.password must exists and not be empty") - .isNotNull() - .isNotEmpty(); MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() - .host("localhost") - .port(3306) + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) .connectTimeout(Duration.ofSeconds(3)) - .user("root") - .password(password) - .database("r2dbc") + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()) .createDatabaseIfNotExist(true) .build(); @@ -91,7 +90,9 @@ void setUp() throws CertificateException, SSLException, InterruptedException { server = new SelfSignedCertificate(); client = new SelfSignedCertificate(); final SslContext sslContext = SslContextBuilder.forServer(server.key(), server.cert()).build(); - sslTunnelServer = new SslTunnelServer("localhost", 3306, sslContext); + sslTunnelServer = new SslTunnelServer(TestServerUtil.getHost(), + TestServerUtil.getPort(), + sslContext); sslTunnelServer.setUp(); } @@ -104,18 +105,13 @@ void tearDown() throws InterruptedException { @Test void sslTunnelConnectionTest() { - final String password = System.getProperty("test.mysql.password"); - assertThat(password).withFailMessage("Property test.mysql.password must exists and not be empty") - .isNotNull() - .isNotEmpty(); - final MySqlConnectionConfiguration configuration = MySqlConnectionConfiguration.builder() .host("localhost") .port(sslTunnelServer.getLocalPort()) .connectTimeout(Duration.ofSeconds(3)) - .user("root") - .password(password) - .database("r2dbc") + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()) .sslMode(SslMode.TUNNEL) .sslKey(client.privateKey().getAbsolutePath()) .sslCert(client.certificate().getAbsolutePath()) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java index 99da15e3c..bf4a0e1f0 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TimeZoneIntegrationTest.java @@ -2,11 +2,14 @@ import com.zaxxer.hikari.HikariDataSource; import io.asyncer.r2dbc.mysql.api.MySqlResult; +import io.asyncer.r2dbc.mysql.internal.util.TestContainerExtension; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; import org.assertj.core.data.TemporalUnitOffset; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.parallel.Isolated; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -44,6 +47,7 @@ /** * Integration tests for aligning time zone configuration options with jdbc. */ +@ExtendWith(TestContainerExtension.class) @Isolated class TimeZoneIntegrationTest { @@ -291,18 +295,13 @@ static Stream alignSendAndReceiveTimeZoneOption() { private static MySqlConnectionConfiguration configuration( Function customizer ) { - String password = System.getProperty("test.mysql.password"); - - if (password == null || password.isEmpty()) { - throw new IllegalStateException("Property test.mysql.password must exists and not be empty"); - } MySqlConnectionConfiguration.Builder builder = MySqlConnectionConfiguration.builder() - .host("localhost") - .port(3306) - .user("root") - .password(password) - .database("r2dbc"); + .host(TestServerUtil.getHost()) + .port(TestServerUtil.getPort()) + .user(TestServerUtil.getUsername()) + .password(TestServerUtil.getPassword()) + .database(TestServerUtil.getDatabase()); return customizer.apply(builder).build(); } @@ -328,19 +327,13 @@ private static JdbcTemplate jdbc(MySqlConnectionConfiguration config) { } private static String dateTimeSuffix(boolean function) { - String version = System.getProperty("test.mysql.version"); - return version != null && isMicrosecondSupported(version) ? "(6)" : function ? "()" : ""; + return isMicrosecondSupported() ? "(6)" : function ? "()" : ""; } - private static boolean isMicrosecondSupported(String version) { - if (version.isEmpty()) { - return false; - } - - ServerVersion ver = ServerVersion.parse(version); - String type = System.getProperty("test.db.type"); + private static boolean isMicrosecondSupported() { + final ServerVersion ver = TestServerUtil.getServerVersion(); - return "mariadb".equalsIgnoreCase(type) || + return TestServerUtil.isMariaDb() || ver.isGreaterThanOrEqualTo(ServerVersion.create(5, 6, 0)); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java index 8a0c339e7..625fd6768 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ZstdCompressionIntegrationTest.java @@ -17,6 +17,7 @@ package io.asyncer.r2dbc.mysql; import io.asyncer.r2dbc.mysql.constant.CompressionAlgorithm; +import io.asyncer.r2dbc.mysql.internal.util.TestServerUtil; import org.junit.jupiter.api.condition.EnabledIf; /** @@ -30,19 +31,11 @@ class ZstdCompressionIntegrationTest extends CompressionIntegrationTestSupport { } static boolean envIsZstdSupported() { - String type = System.getProperty("test.db.type"); - - if ("mariadb".equalsIgnoreCase(type)) { + if (TestServerUtil.isMariaDb()) { return false; } - String version = System.getProperty("test.mysql.version"); - - if (version == null || version.isEmpty()) { - return true; - } - - ServerVersion ver = ServerVersion.parse(version); + final ServerVersion ver = TestServerUtil.getServerVersion(); return ver.isGreaterThanOrEqualTo(ServerVersion.create(8, 0, 18)); } } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestContainerExtension.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestContainerExtension.java new file mode 100644 index 000000000..e324d3cf9 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestContainerExtension.java @@ -0,0 +1,160 @@ +package io.asyncer.r2dbc.mysql.internal.util; + +import io.netty.util.internal.SystemPropertyUtil; +import org.jetbrains.annotations.VisibleForTesting; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; + +/** + * JUnit 5 extension to start a test container with MySQL or MariaDB. + * The extension starts a test container with MySQL or MariaDB based on the system properties: + *

    + *
  • {@code test.db.testcontainer} - whether to use a test container or not (default: {@code true})
  • + *
  • {@code test.db.type} - the test-container database type (default: {@code mysql})
  • + *
  • {@code test.db.version} - the test-container database version (default: {@code 5.7.44})
  • + *
  • {@code test.db.host} - the self-hosted database server host (default: {@code 127.0.0.1})
  • + *
  • {@code test.db.port} - the self-hosted database server port (default: {@code 3306})
  • + *
  • {@code test.db.database} - the self-hoseted database server database name (default: {@code test})
  • + *
  • {@code test.db.username} - the self-hosted database server username (default: {@code root})
  • + *
  • {@code test.db.password} - the self-hosted database server password (default: {@code root})
  • + *
+ */ +public final class TestContainerExtension implements BeforeAllCallback { + + static final Container server; + + static final boolean useTestContainer; + + static final String dbType; + + static final String dbVersion; + + static { + useTestContainer = SystemPropertyUtil.getBoolean("test.db.testcontainer", true); + dbType = SystemPropertyUtil.get("test.db.type", "mysql"); + dbVersion = SystemPropertyUtil.get("test.db.version", "5.7.44"); + if (useTestContainer) { + server = new TestContainer(dbType, dbVersion); + } else { + server = new SelfHostedContainer(); + } + server.start(); + } + + @Override + public void beforeAll(ExtensionContext extensionContext) throws Exception { + // NOOP - initialized in the static block + } + + private static final class TestContainer implements Container { + + private final JdbcDatabaseContainer container; + + @SuppressWarnings("resource") + private TestContainer(final String dbType, final String dbVersion) { + if ("mariadb".equalsIgnoreCase(dbType)) { + container = new MariaDBContainer<>(dbType + ':' + dbVersion) + .withUsername("root") + .withPassword("") + .withNetwork(Network.newNetwork()) + .withCommand("--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci"); + } else { + container = new MySQLContainer<>(dbType + ':' + dbVersion) + .withUsername("root") + .withNetwork(Network.newNetwork()) + .withCommand("--local-infile=true", + "--character-set-server=utf8mb4", + "--collation-server=utf8mb4_unicode_ci"); + if (dbVersion.startsWith("5.5")) { + // mysql 5.5.x does not support host_cache_size but latest test container utilizes it. + // so we need to remove host_cache_size option when mysql version is 5.5.x and lower + // ref: https://github.com/testcontainers/testcontainers-java/issues/8130 + ((MySQLContainer) container).withConfigurationOverride("testcontainer/mysql-5.5"); + } + } + } + + @Override + public void start() { + container.start(); + } + + @Override + public String getHost() { + return container.getHost(); + } + + @Override + public int getPort() { + return container.getMappedPort(3306); + } + + @Override + public String getDatabase() { + return container.getDatabaseName(); + } + + @Override + public String getUsername() { + return container.getUsername(); + } + + @Override + public String getPassword() { + return container.getPassword(); + } + } + + private static final class SelfHostedContainer implements Container { + + @Override + public void start() { + // NOOP + } + + @Override + public String getHost() { + return SystemPropertyUtil.get("test.db.host", "127.0.0.1"); + } + + @Override + public int getPort() { + return SystemPropertyUtil.getInt("test.db.port", 3306); + } + + @Override + public String getDatabase() { + return SystemPropertyUtil.get("test.db.database", "test"); + } + + @Override + public String getUsername() { + return SystemPropertyUtil.get("test.db.username", "root"); + } + + @Override + public String getPassword() { + return SystemPropertyUtil.get("test.db.password", "root"); + } + } + + interface Container { + + void start(); + + String getHost(); + + int getPort(); + + String getDatabase(); + + String getUsername(); + + String getPassword(); + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtil.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtil.java new file mode 100644 index 000000000..4fbba5683 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtil.java @@ -0,0 +1,137 @@ +package io.asyncer.r2dbc.mysql.internal.util; + +import com.zaxxer.hikari.HikariDataSource; +import io.asyncer.r2dbc.mysql.ServerVersion; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Utility class that provides access to the test server configuration. + */ +public final class TestServerUtil { + + static final AtomicReference dataSource = new AtomicReference<>(); + + static final AtomicReference connection = new AtomicReference<>(); + + /** + * Returns the host of the test server. + * + * @return the host of the test server + */ + public static String getHost() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getHost(); + } + + /** + * Returns the port of the test server. + * @return the port of the test server + */ + public static int getPort() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getPort(); + } + + /** + * Returns the database name of the test server. + * @return the database name of the test server + */ + public static String getDatabase() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getDatabase(); + } + + /** + * Returns the username of the test server. + * @return the username of the test server + */ + public static String getUsername() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getUsername(); + } + + /** + * Returns the password of the test server. + * @return the password of the test server + */ + public static String getPassword() { + ensureTestContainerIsRunning(); + return TestContainerExtension.server.getPassword(); + } + + /** + * Returns whether the test server is a MariaDB server. + * @return whether the test server is a MariaDB server + */ + public static boolean isMariaDb() { + ensureTestContainerIsRunning(); + try { + return !"MySQL".equalsIgnoreCase(getSharedConnection().getMetaData().getDatabaseProductName()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns the server version of the test server. + * @return the server version of the test server + */ + public static ServerVersion getServerVersion() { + ensureTestContainerIsRunning(); + try { + return ServerVersion.parse(getSharedConnection().getMetaData().getDatabaseProductVersion()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns a shared jdbc connection to the test server. + * @return a shared jdbc connection to the test server + */ + public static Connection getSharedConnection() { + ensureTestContainerIsRunning(); + if (connection.get() == null) { + connection.compareAndSet(null, getConnection0()); + } + return connection.get(); + } + + private static Connection getConnection0() { + ensureTestContainerIsRunning(); + DataSource source = dataSource.get(); + if (source == null) { + final String connectionString = String.format( + "jdbc:mariadb://%s:%s/%s?user=%s&password=%s", + // should use mariadb to get correct metadata.getDatabaseProductName() + getHost(), + getPort(), + getDatabase(), + getUsername(), + getPassword()); + HikariDataSource hikariDataSource = new HikariDataSource(); + hikariDataSource.setJdbcUrl(connectionString); + dataSource.compareAndSet(null, hikariDataSource); + } + try { + return dataSource.get().getConnection(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private static void ensureTestContainerIsRunning() { + if (TestContainerExtension.server == null) { + throw new IllegalStateException("Test server is not configured"); + } + TestContainerExtension.server.start(); // ensure running + } + + private TestServerUtil() { + // Utility class + } +} diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtilTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtilTest.java new file mode 100644 index 000000000..ab54aa168 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/internal/util/TestServerUtilTest.java @@ -0,0 +1,32 @@ +package io.asyncer.r2dbc.mysql.internal.util; + +import io.asyncer.r2dbc.mysql.ServerVersion; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(TestContainerExtension.class) +class TestServerUtilTest { + + @Test + void serverVersionTest() { + final boolean useTestContainer = TestContainerExtension.useTestContainer; + final String versionString = TestContainerExtension.dbVersion; + Assumptions.assumeTrue(useTestContainer && containsPatchVersion(versionString)); + Assertions.assertEquals(ServerVersion.parse(versionString), TestServerUtil.getServerVersion()); + } + + @Test + void serverTypeTest() { + final boolean useTestContainer = TestContainerExtension.useTestContainer; + final String dbType = TestContainerExtension.dbType; + Assumptions.assumeTrue(useTestContainer); + Assertions.assertEquals("mariadb".equalsIgnoreCase(dbType), TestServerUtil.isMariaDb()); + } + + private static boolean containsPatchVersion(final String versionString) { + return versionString.indexOf('.') != versionString.lastIndexOf('.'); + } + +} diff --git a/r2dbc-mysql/src/test/resources/testcontainer/mysql-5.5/my.cnf b/r2dbc-mysql/src/test/resources/testcontainer/mysql-5.5/my.cnf new file mode 100644 index 000000000..07369ef5b --- /dev/null +++ b/r2dbc-mysql/src/test/resources/testcontainer/mysql-5.5/my.cnf @@ -0,0 +1,49 @@ +[mysqld] +user = mysql +datadir = /var/lib/mysql +port = 3306 +#socket = /tmp/mysql.sock +skip-external-locking +key_buffer_size = 16K +max_allowed_packet = 1M +table_open_cache = 4 +sort_buffer_size = 64K +read_buffer_size = 256K +read_rnd_buffer_size = 256K +net_buffer_length = 2K +skip-host-cache +skip-name-resolve + +# Don't listen on a TCP/IP port at all. This can be a security enhancement, +# if all processes that need to connect to mysqld run on the same host. +# All interaction with mysqld must be made via Unix sockets or named pipes. +# Note that using this option without enabling named pipes on Windows +# (using the "enable-named-pipe" option) will render mysqld useless! +# +#skip-networking +#server-id = 1 + +# Uncomment the following if you want to log updates +#log-bin=mysql-bin + +# binary logging format - mixed recommended +#binlog_format=mixed + +# Causes updates to non-transactional engines using statement format to be +# written directly to binary log. Before using this option make sure that +# there are no dependencies between transactional and non-transactional +# tables such as in the statement INSERT INTO t_myisam SELECT * FROM +# t_innodb; otherwise, slaves may diverge from the master. +#binlog_direct_non_transactional_updates=TRUE + +# Uncomment the following if you are using InnoDB tables +innodb_data_file_path = ibdata1:10M:autoextend +# You can set .._buffer_pool_size up to 50 - 80 % +# of RAM but beware of setting memory usage too high +innodb_buffer_pool_size = 16M +#innodb_additional_mem_pool_size = 2M +# Set .._log_file_size to 25 % of buffer pool size +innodb_log_file_size = 5M +innodb_log_buffer_size = 8M +innodb_flush_log_at_trx_commit = 1 +innodb_lock_wait_timeout = 50 diff --git a/test-native-image/src/main/java/io/asyncer/Main.java b/test-native-image/src/main/java/io/asyncer/Main.java index 1ad7788b3..1a85dccb2 100644 --- a/test-native-image/src/main/java/io/asyncer/Main.java +++ b/test-native-image/src/main/java/io/asyncer/Main.java @@ -34,8 +34,8 @@ public static void main(String[] args) { .option(ConnectionFactoryOptions.HOST, "127.0.0.1") .option(ConnectionFactoryOptions.PORT, 3306) .option(ConnectionFactoryOptions.USER, "root") - .option(ConnectionFactoryOptions.PASSWORD, System.getProperty("test.mysql.password")) - .option(ConnectionFactoryOptions.DATABASE, "r2dbc") + .option(ConnectionFactoryOptions.PASSWORD, "root") + .option(ConnectionFactoryOptions.DATABASE, "test") .option(MySqlConnectionFactoryProvider.CREATE_DATABASE_IF_NOT_EXIST, true) .build()); From 8641fc45d3a45d6e9ab35a81bf7c8a74b9f92b1e Mon Sep 17 00:00:00 2001 From: jchrys Date: Sat, 6 Apr 2024 20:29:32 +0900 Subject: [PATCH 47/93] Make `upgrade_native_image_version.sh` Executable (#270) Motivation: Fix execution failure of `upgrade_native_image_version.sh` due to missing execute permission. Modifications: Ran `chmod +x ./upgrade_native_image_version.sh` to make the script executable. Result: Script is now executable. --- .github/scripts/upgrade_native_image_version.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/scripts/upgrade_native_image_version.sh diff --git a/.github/scripts/upgrade_native_image_version.sh b/.github/scripts/upgrade_native_image_version.sh old mode 100644 new mode 100755 From 8c699e3702e065aa89f51ccba008095f790646cd Mon Sep 17 00:00:00 2001 From: jchrys Date: Sat, 6 Apr 2024 21:01:04 +0900 Subject: [PATCH 48/93] Skip Checkstyle for Local Staging (#271) Motivation: Local staging phase causes release action to fail due to Checkstyle errors. Modifications: Checkstyle is now skipped. Result: Local staging operates smoothly. --- .github/scripts/release_rollback.sh | 2 +- .github/workflows/cd-release.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/release_rollback.sh b/.github/scripts/release_rollback.sh index 14582ed9a..b67e429c8 100755 --- a/.github/scripts/release_rollback.sh +++ b/.github/scripts/release_rollback.sh @@ -16,7 +16,7 @@ # set -e -TAG=$(grep scm.tag= release.properties | cut -d'=' -f2) +TAG=$(grep scm.tag= r2dbc-mysql/release.properties | cut -d'=' -f2) git remote set-url origin git@github.com:asyncer-io/r2dbc-mysql.git git fetch git checkout "$1" diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 1e9034bc0..d24aa742c 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -128,7 +128,7 @@ jobs: working-directory: ./prepare-workspace/ run: | cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import - ./mvnw -B -ntp -am -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" + ./mvnw -B -ntp -am -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" - name: Prepare Internal Dependencies run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip @@ -141,5 +141,5 @@ jobs: - name: Rollback Release working-directory: ./prepare-workspace/ if: ${{ failure() }} - run: ./.github/scripts/release_rollback.sh + run: ./.github/scripts/release_rollback.sh trunk From bb1405864777b05e2edc71118c8d7591d3b6a9ba Mon Sep 17 00:00:00 2001 From: jchrys Date: Sat, 6 Apr 2024 21:28:36 +0900 Subject: [PATCH 49/93] Remove Unnecessary Stage-Release Step (#272) Motivation: `Prepare Internal Dependencies` is unnecessary Modification: Remove unnecessary step Result: Clean up --- .github/workflows/cd-release.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index d24aa742c..83a436a7d 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -130,9 +130,6 @@ jobs: cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import ./mvnw -B -ntp -am -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" - - name: Prepare Internal Dependencies - run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip - - name: Deploy Local Staged Artifacts working-directory: ./prepare-workspace/ run: ./mvnw -B -ntp -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true -Dcheckstyle.skip From 48b5ed33a067c5652f5f5c38ad70e30660b31d2a Mon Sep 17 00:00:00 2001 From: jchrys Date: Sat, 6 Apr 2024 22:05:46 +0900 Subject: [PATCH 50/93] Skip deploying build-tools (#273) Motivation: Unnecessary build-tools artifact was deployed along with r2dbc-mysql Modification: Skip deploying build-tools Result: Clean up --- .github/workflows/cd-release.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 83a436a7d..a1fb8bde9 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -123,12 +123,16 @@ jobs: - name: Create Local Deploy Directory run: mkdir -p ~/local-staging + + - name: Prepare Internal Dependencies + working-directory: ./prepare-workspace/ + run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip - name: Import GPG & Deploy Local Staging working-directory: ./prepare-workspace/ run: | cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import - ./mvnw -B -ntp -am -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" + ./mvnw -B -ntp -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" - name: Deploy Local Staged Artifacts working-directory: ./prepare-workspace/ From 9a1a74e20bdd8cf36625b94a57f0001963a45bb6 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Sat, 6 Apr 2024 13:11:56 +0000 Subject: [PATCH 51/93] [maven-release-plugin] prepare release r2dbc-mysql-1.1.3 --- r2dbc-mysql/pom.xml | 8 +++----- test-native-image/pom.xml | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index e126c48d6..7c23bd139 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -14,14 +14,12 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + 4.0.0 io.asyncer r2dbc-mysql - 1.1.3-SNAPSHOT + 1.1.3 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -63,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.1.3 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index f74a74a20..e18ad93bb 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.1.3-SNAPSHOT + 1.1.4-SNAPSHOT UTF-8 From b992f58a96b179af27db35ab57e74ec46443be42 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Sat, 6 Apr 2024 13:11:57 +0000 Subject: [PATCH 52/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 7c23bd139..0c26c0a49 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.1.3 + 1.1.4-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.1.3 + HEAD From bcd95f809b9cbee3f5261dfdfeaf641ba692c428 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 25 Jul 2024 10:42:11 +0900 Subject: [PATCH 53/93] Add CodeQL GitHub Action (#280) Motivation: Integrate CodeQL for automated code analysis to enhance security and code quality. Modifications: Add GitHub Action Result: Maintain high standards of code security and quality in the project. --- .github/workflows/ci-codeql.yml | 80 +++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/ci-codeql.yml diff --git a/.github/workflows/ci-codeql.yml b/.github/workflows/ci-codeql.yml new file mode 100644 index 000000000..cc2347469 --- /dev/null +++ b/.github/workflows/ci-codeql.yml @@ -0,0 +1,80 @@ +# Copyright 2024 asyncer.io proejcts +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: "CodeQL" + +on: + push: + branches: [ "trunk", "0.9.x" ] + pull_request: + branches: [ "trunk", "0.9.x" ] + schedule: + - cron: '24 3 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: java-kotlin + build-mode: none # This mode only analyzes Java. Set this to 'autobuild' or 'manual' to analyze Kotlin too. + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From 508d6c38d9354951e3621b963114a1f3d7a75359 Mon Sep 17 00:00:00 2001 From: ZhangJian He Date: Thu, 25 Jul 2024 10:30:37 +0800 Subject: [PATCH 54/93] chore: fix typo in ReactorNettyClient (#278) --- .../java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index 81cb5f21e..961b2806e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -118,7 +118,7 @@ final class ReactorNettyClient implements Client { } sink.next((ServerMessage) it); } else { - // ReferenceCounted will released by Netty. + // ReferenceCounted will be released by Netty. throw ClientExceptions.unsupportedProtocol(it.getClass().getTypeName()); } }) From e37cbddc2fdfb9ad2b12ea27425502a06c03b448 Mon Sep 17 00:00:00 2001 From: ZhangJian He Date: Thu, 25 Jul 2024 19:45:29 +0800 Subject: [PATCH 55/93] feat: support config AddressResolverGroup in r2dbc-mysql (#279) Motivation: Currently,`AddressResolverGroup` can't be configured. The DnsResolver default start address listen to "0.0.0.0", which may have some security risks. also see https://github.com/netty/netty/pull/11061 Modification: Add `AddressResolverGroup` in Client's connect method --------- Signed-off-by: ZhangJian He --- .../mysql/MySqlConnectionConfiguration.java | 83 ++++++++++++------- .../r2dbc/mysql/MySqlConnectionFactory.java | 3 +- .../mysql/MySqlConnectionFactoryProvider.java | 14 ++++ .../io/asyncer/r2dbc/mysql/client/Client.java | 7 +- .../MySqlConnectionConfigurationTest.java | 15 ++++ .../MySqlConnectionFactoryProviderTest.java | 16 ++++ 6 files changed, 104 insertions(+), 34 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 3856b58bd..2f1c75961 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -22,6 +22,7 @@ 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.resources.LoopResources; @@ -127,6 +128,9 @@ public final class MySqlConnectionConfiguration { @Nullable private final Publisher passwordPublisher; + @Nullable + private final AddressResolverGroup resolver; + private MySqlConnectionConfiguration( boolean isHost, String domain, int port, MySqlSslConfiguration ssl, boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout, @@ -141,7 +145,8 @@ private MySqlConnectionConfiguration( int queryCacheSize, int prepareCacheSize, Set compressionAlgorithms, int zstdCompressionLevel, @Nullable LoopResources loopResources, - Extensions extensions, @Nullable Publisher passwordPublisher + Extensions extensions, @Nullable Publisher passwordPublisher, + @Nullable AddressResolverGroup resolver ) { this.isHost = isHost; this.domain = domain; @@ -171,6 +176,7 @@ private MySqlConnectionConfiguration( this.loopResources = loopResources == null ? TcpResources.get() : loopResources; this.extensions = extensions; this.passwordPublisher = passwordPublisher; + this.resolver = resolver; } /** @@ -301,6 +307,11 @@ Publisher getPasswordPublisher() { return passwordPublisher; } + @Nullable + AddressResolverGroup getResolver() { + return resolver; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -337,7 +348,8 @@ public boolean equals(Object o) { zstdCompressionLevel == that.zstdCompressionLevel && Objects.equals(loopResources, that.loopResources) && extensions.equals(that.extensions) && - Objects.equals(passwordPublisher, that.passwordPublisher); + Objects.equals(passwordPublisher, that.passwordPublisher) && + Objects.equals(resolver, that.resolver); } @Override @@ -352,19 +364,26 @@ public int hashCode() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, - loopResources, extensions, passwordPublisher); + loopResources, extensions, passwordPublisher, resolver); } @Override public String toString() { - if (isHost) { - return "MySqlConnectionConfiguration{host='" + domain + "', port=" + port + ", ssl=" + ssl + - ", tcpNoDelay=" + tcpNoDelay + ", tcpKeepAlive=" + tcpKeepAlive + - ", connectTimeout=" + connectTimeout + + return "MySqlConnectionConfiguration{" + + (isHost ? "host='" + domain + "', port=" + port + ", ssl=" + ssl + + ", tcpNoDelay=" + tcpNoDelay + ", tcpKeepAlive=" + tcpKeepAlive : + "unixSocket='" + domain + "'") + + buildCommonToStringPart() + + '}'; + } + + private String buildCommonToStringPart() { + return ", connectTimeout=" + connectTimeout + ", preserveInstants=" + preserveInstants + ", connectionTimeZone=" + connectionTimeZone + ", forceConnectionTimeZoneToSession=" + forceConnectionTimeZoneToSession + - ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + + ", zeroDateOption=" + zeroDateOption + + ", user='" + user + "', password=" + password + ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + ", preferPrepareStatement=" + preferPrepareStatement + ", sessionVariables=" + sessionVariables + @@ -372,32 +391,14 @@ public String toString() { ", statementTimeout=" + statementTimeout + ", loadLocalInfilePath=" + loadLocalInfilePath + ", localInfileBufferSize=" + localInfileBufferSize + - ", queryCacheSize=" + queryCacheSize + ", prepareCacheSize=" + prepareCacheSize + + ", queryCacheSize=" + queryCacheSize + + ", prepareCacheSize=" + prepareCacheSize + ", compressionAlgorithms=" + compressionAlgorithms + ", zstdCompressionLevel=" + zstdCompressionLevel + ", loopResources=" + loopResources + - ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; - } - - return "MySqlConnectionConfiguration{unixSocket='" + domain + - "', connectTimeout=" + connectTimeout + - ", preserveInstants=" + preserveInstants + - ", connectionTimeZone=" + connectionTimeZone + - ", forceConnectionTimeZoneToSession=" + forceConnectionTimeZoneToSession + - ", zeroDateOption=" + zeroDateOption + ", user='" + user + "', password=" + password + - ", database='" + database + "', createDatabaseIfNotExist=" + createDatabaseIfNotExist + - ", preferPrepareStatement=" + preferPrepareStatement + - ", sessionVariables=" + sessionVariables + - ", lockWaitTimeout=" + lockWaitTimeout + - ", statementTimeout=" + statementTimeout + - ", loadLocalInfilePath=" + loadLocalInfilePath + - ", localInfileBufferSize=" + localInfileBufferSize + - ", queryCacheSize=" + queryCacheSize + - ", prepareCacheSize=" + prepareCacheSize + - ", compressionAlgorithms=" + compressionAlgorithms + - ", zstdCompressionLevel=" + zstdCompressionLevel + - ", loopResources=" + loopResources + - ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + '}'; + ", extensions=" + extensions + + ", passwordPublisher=" + passwordPublisher + + ", resolver=" + resolver; } /** @@ -494,6 +495,9 @@ public static final class Builder { @Nullable private Publisher passwordPublisher; + @Nullable + private AddressResolverGroup resolver; + /** * Builds an immutable {@link MySqlConnectionConfiguration} with current options. * @@ -528,7 +532,7 @@ public MySqlConnectionConfiguration build() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, - Extensions.from(extensions, autodetectExtensions), passwordPublisher); + Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver); } /** @@ -1156,6 +1160,21 @@ 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; + } + private SslMode requireSslMode() { SslMode sslMode = this.sslMode; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index d003db2b0..bff85c809 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -147,7 +147,8 @@ private static Mono getMySqlConnection( configuration.isTcpNoDelay(), context, configuration.getConnectTimeout(), - configuration.getLoopResources() + configuration.getLoopResources(), + configuration.getResolver() )).flatMap(client -> { // Lazy init database after handshake/login boolean deferDatabase = configuration.isCreateDatabaseIfNotExist(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index f6dc1a57a..d89005394 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -20,6 +20,7 @@ import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.resolver.AddressResolverGroup; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.ConnectionFactoryProvider; @@ -308,6 +309,17 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr */ public static final Option> PASSWORD_PUBLISHER = Option.valueOf("passwordPublisher"); + /** + * Option to set the {@link AddressResolverGroup} for resolving host addresses. + *

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

+ * + * @since 1.2.0 + */ + public static final Option> RESOLVER = Option.valueOf("resolver"); + @Override public ConnectionFactory create(ConnectionFactoryOptions options) { requireNonNull(options, "connectionFactoryOptions must not be null"); @@ -389,6 +401,8 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { .to(builder::loopResources); mapper.optional(PASSWORD_PUBLISHER).as(Publisher.class) .to(builder::passwordPublisher); + mapper.optional(RESOLVER).as(AddressResolverGroup.class) + .to(builder::resolver); mapper.optional(SESSION_VARIABLES).asArray( String[].class, Function.identity(), diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java index d7c3ac28a..0beaf4c0d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java @@ -22,6 +22,7 @@ import io.asyncer.r2dbc.mysql.message.server.ServerMessage; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelOption; +import io.netty.resolver.AddressResolverGroup; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; import org.jetbrains.annotations.Nullable; @@ -132,7 +133,7 @@ public interface Client { */ static Mono connect(MySqlSslConfiguration ssl, SocketAddress address, boolean tcpKeepAlive, boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout, - LoopResources loopResources) { + LoopResources loopResources, @Nullable AddressResolverGroup resolver) { requireNonNull(ssl, "ssl must not be null"); requireNonNull(address, "address must not be null"); requireNonNull(context, "context must not be null"); @@ -150,6 +151,10 @@ static Mono connect(MySqlSslConfiguration ssl, SocketAddress address, bo tcpClient = tcpClient.option(ChannelOption.TCP_NODELAY, tcpNoDelay); } + if (resolver != null) { + tcpClient = tcpClient.resolver(resolver); + } + return tcpClient.remoteAddress(() -> address).connect() .map(conn -> new ReactorNettyClient(conn, ssl, context)); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java index f050f4e4a..f05defb17 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java @@ -22,6 +22,8 @@ import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; import io.asyncer.r2dbc.mysql.extension.Extension; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.resolver.AddressResolverGroup; +import io.netty.resolver.DefaultAddressResolverGroup; import org.assertj.core.api.ObjectAssert; import org.assertj.core.api.ThrowableTypeAssert; import org.jetbrains.annotations.Nullable; @@ -207,6 +209,19 @@ void validPasswordSupplier() { .verifyComplete(); } + @Test + void validResolver() { + final AddressResolverGroup resolver = DefaultAddressResolverGroup.INSTANCE; + AddressResolverGroup resolverGroup = MySqlConnectionConfiguration.builder() + .host(HOST) + .user(USER) + .resolver(resolver) + .autodetectExtensions(false) + .build() + .getResolver(); + assertThat(resolverGroup).isSameAs(resolver); + } + private static MySqlConnectionConfiguration unixSocketSslMode(SslMode sslMode) { return MySqlConnectionConfiguration.builder() .unixSocket(UNIX_SOCKET) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index ab75161c1..1e71a9f17 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -20,6 +20,8 @@ import io.asyncer.r2dbc.mysql.constant.SslMode; import io.asyncer.r2dbc.mysql.constant.ZeroDateOption; import io.netty.handler.ssl.SslContextBuilder; +import io.netty.resolver.AddressResolverGroup; +import io.netty.resolver.DefaultAddressResolverGroup; import io.r2dbc.spi.ConnectionFactories; import io.r2dbc.spi.ConnectionFactoryOptions; import io.r2dbc.spi.Option; @@ -50,6 +52,7 @@ import java.util.stream.Stream; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.PASSWORD_PUBLISHER; +import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.RESOLVER; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.USE_SERVER_PREPARE_STATEMENT; import static io.r2dbc.spi.ConnectionFactoryOptions.CONNECT_TIMEOUT; import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; @@ -453,6 +456,19 @@ void validPasswordSupplier() { assertThat(ConnectionFactories.get(options)).isExactlyInstanceOf(MySqlConnectionFactory.class); } + @Test + void validResolver() { + final AddressResolverGroup resolver = DefaultAddressResolverGroup.INSTANCE; + ConnectionFactoryOptions options = ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(RESOLVER, resolver) + .build(); + + assertThat(ConnectionFactories.get(options)).isExactlyInstanceOf(MySqlConnectionFactory.class); + } + @Test void allConfigurationOptions() { List exceptConfigs = Arrays.asList( From a5ada25ccf1e16d758606f7cba7a47a4e70a5562 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 25 Jul 2024 21:01:27 +0900 Subject: [PATCH 56/93] Prepare Next Release (#281) Motivation: Will Introduce new feature. Modification: Set Version 1.2.0 Result: Version 1.2.0 --- r2dbc-mysql/pom.xml | 2 +- test-native-image/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 0c26c0a49..7d7785132 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.1.4-SNAPSHOT + 1.2.0-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index e18ad93bb..abb1f56fd 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.1.4-SNAPSHOT + 1.2.0-SNAPSHOT UTF-8 From 17fe67b0471dbc20f34178d48b5493aba2bf9cc8 Mon Sep 17 00:00:00 2001 From: jchrys Date: Sun, 28 Jul 2024 23:19:31 +0900 Subject: [PATCH 57/93] Support MySQL 8.4 and 9.0 (#283) Motivation: Add compatibility for the newer MySQL versions 8.4 and 9.0 to ensure the connector works with the latest MySQL features and improvements. Modifications: - Added tests to verify functionality with MySQL 8.4 and 9.0. Result: Ensures compatibility with MySQL 8.4 and 9.0. --- .github/workflows/ci-integration-tests.yml | 2 +- README.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 6f0e151ff..8cf8991d2 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - mysql-version: [ 5.5, 5.6.45, 5.6, 5.7.28, 5.7, 8.0, 8.1, 8.2, 8.3] + mysql-version: [ 5.5, 5.6.45, 5.6, 5.7.28, 5.7, 8.0, 8.1, 8.2, 8.3, 8.4, 9.0] name: Integration test with MySQL ${{ matrix.mysql-version }} steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 4ddab2f62..ba852f8d0 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ This driver provides the following features: ![MySQL 8.0 status](https://img.shields.io/badge/MySQL%208.0-pass-blue) ![MySQL 8.1 status](https://img.shields.io/badge/MySQL%208.1-pass-blue) ![MySQL 8.2 status](https://img.shields.io/badge/MySQL%208.2-pass-blue) +![MySQL 8.3 status](https://img.shields.io/badge/MySQL%208.3-pass-blue) +![MySQL 8.4 status](https://img.shields.io/badge/MySQL%208.4-pass-blue) +![MySQL 9.0 status](https://img.shields.io/badge/MySQL%209.0-pass-blue) ![MariaDB 10.6 status](https://img.shields.io/badge/MariaDB%2010.6-pass-blue) ![MariaDB 10.11 status](https://img.shields.io/badge/MariaDB%2010.11-pass-blue) From ef9ee1d0e194f45996c138eaa08e1c618da9cdd7 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Mon, 29 Jul 2024 11:18:48 +0000 Subject: [PATCH 58/93] [maven-release-plugin] prepare release r2dbc-mysql-1.2.0 --- r2dbc-mysql/pom.xml | 4 ++-- test-native-image/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 7d7785132..5576bf390 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.2.0-SNAPSHOT + 1.2.0 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.2.0 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index abb1f56fd..0f4eecbcb 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.2.0-SNAPSHOT + 1.2.1-SNAPSHOT UTF-8 From 102c99a33a989f88bb502900ccfeed01d9646f53 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Mon, 29 Jul 2024 11:18:50 +0000 Subject: [PATCH 59/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 5576bf390..f066f06d3 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.2.0 + 1.2.1-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.2.0 + HEAD From bc227f176b868b4ee51c8a2ab2412c6252b1e4ed Mon Sep 17 00:00:00 2001 From: jchrys Date: Sat, 3 Aug 2024 22:07:19 +0900 Subject: [PATCH 60/93] Prepare Next Release (#284) Motivation: Will introduce breaking change. Modification: Set Version 1.3.0 Result: Version 1.3.0 --- README.md | 8 ++++---- r2dbc-mysql/pom.xml | 2 +- test-native-image/pom.xml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ba852f8d0..e581a7159 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Refer to the table below to determine the appropriate version of r2dbc-mysql for | spring-boot-starter-data-r2dbc | spring-data-r2dbc | r2dbc-spi | r2dbc-mysql(recommended) | |--------------------------------|-------------------|---------------|------------------------------| -| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.1.0 | +| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.2.0 | | 2.7.* | 1.5.* | 0.9.1.RELEASE | io.asyncer:r2dbc-mysql:0.9.7 | | 2.6.* and below | 1.4.* and below | 0.8.6.RELEASE | dev.miku:r2dbc-mysql:0.8.2 | @@ -61,7 +61,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so io.asyncer r2dbc-mysql - 1.1.0 + 1.2.0 ``` @@ -71,7 +71,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so ```groovy dependencies { - implementation 'io.asyncer:r2dbc-mysql:1.1.0' + implementation 'io.asyncer:r2dbc-mysql:1.2.0' } ``` @@ -80,7 +80,7 @@ dependencies { ```kotlin dependencies { // Maybe should to use `compile` instead of `implementation` on the lower version of Gradle. - implementation("io.asyncer:r2dbc-mysql:1.1.0") + implementation("io.asyncer:r2dbc-mysql:1.2.0") } ``` diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index f066f06d3..29d0563b3 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 0f4eecbcb..ce5cacece 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.2.1-SNAPSHOT + 1.3.0-SNAPSHOT UTF-8 From f56eaa86918879dd347ffdda6094c9cd3d94b420 Mon Sep 17 00:00:00 2001 From: jchrys Date: Sun, 4 Aug 2024 11:41:56 +0900 Subject: [PATCH 61/93] Handle `BIT(1)` as `Boolean` by Default (#286) Motivation: BIT(1) should be treated as `Boolean`. Modification: set `Boolean.class` as `BIT(1)'s default java type. Result: Ensures `BIT(1)` is handled as `Boolean` by default. like mysql-connector-j's implementation. Resolves #277 --- .../asyncer/r2dbc/mysql/codec/DefaultCodecs.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index d76b398e2..4542a7c9d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -18,6 +18,7 @@ import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; +import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.asyncer.r2dbc.mysql.internal.util.InternalArrays; import io.asyncer.r2dbc.mysql.message.FieldValue; import io.asyncer.r2dbc.mysql.message.LargeFieldValue; @@ -358,11 +359,21 @@ private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat * @param type the {@link Class} specified by the user. * @return the {@link Class} to use for decoding. */ - private static Class chooseClass(MySqlReadableMetadata metadata, Class type) { - Class javaType = metadata.getType().getJavaType(); + private static Class chooseClass(final MySqlReadableMetadata metadata, Class type) { + final Class javaType = getDefaultJavaType(metadata); return type.isAssignableFrom(javaType) ? javaType : type; } + private static Class getDefaultJavaType(final MySqlReadableMetadata metadata) { + final MySqlType type = metadata.getType(); + // ref: https://github.com/asyncer-io/r2dbc-mysql/issues/277 + // BIT(1) should be treated as Boolean by default. + if (type == MySqlType.BIT && Integer.valueOf(1).equals(metadata.getPrecision())) { + return Boolean.class; + } + return type.getJavaType(); + } + static final class Builder implements CodecsBuilder { @GuardedBy("lock") From e3bd49bfd79a430c08833f5b136934f15c9699b0 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Mon, 5 Aug 2024 12:47:27 +0000 Subject: [PATCH 62/93] [maven-release-plugin] prepare release r2dbc-mysql-1.3.0 --- r2dbc-mysql/pom.xml | 4 ++-- test-native-image/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 29d0563b3..4a11d3e89 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.3.0-SNAPSHOT + 1.3.0 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.3.0 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index ce5cacece..36ccf08e3 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.3.0-SNAPSHOT + 1.3.1-SNAPSHOT UTF-8 From 037f869878c2acd782021aec8c5b895503fa1463 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Mon, 5 Aug 2024 12:47:29 +0000 Subject: [PATCH 63/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 4a11d3e89..64d20b48b 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.3.0 + 1.3.1-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.3.0 + HEAD From acff42903b2a54a8d526f4ec2c7fc38c397fc567 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:39:33 +0900 Subject: [PATCH 64/93] Bump actions/download-artifact from 3 to 4.1.7 (#287) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7. --- .github/workflows/cd-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index a1fb8bde9..74846be7a 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -77,7 +77,7 @@ jobs: needs: prepare steps: - name: Download workspace - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4.1.7 with: name: prepare-workspace path: ./prepare-workspace/ From 673aac858c864b043d0da3a5a840887c32f7b4f6 Mon Sep 17 00:00:00 2001 From: svats0001 <104880778+svats0001@users.noreply.github.com> Date: Sat, 30 Nov 2024 03:27:54 +1100 Subject: [PATCH 65/93] Implemented relaxed conversion logic for BooleanCodec (#290) @jchrys #285 Motivation: To allow boolean values stored as strings in MySQL database such as "true", "false", "1" and "0" to be converted to their corresponding boolean values. Modification: BooleanCodec: Changed decode method to check if VARCHAR value is a boolean value. Changed doCanDecode to add VARCHAR. BooleanCodecTest: Added decodeString test to ensure boolean values stored as strings are converted into the correct corresponding boolean values. Result: Boolean values stored as strings can now be converted to their corresponding boolean values. Drawbacks are that there could be a string column containing numeric data with values other than 0 or 1 and the column isn't used for storing boolean values at the same time the codec interprets the 0's and 1's as boolean. Only boolean values "true", "false", "1" and "0" are decoded, other possible types of boolean value strings haven't been included. Also, doCanDecode states that the VARCHAR data type can be decoded but only a small subset of this data type can be decoded and it's not possible to highlight the conditions in the doCanDecode method. --- .../r2dbc/mysql/codec/BooleanCodec.java | 49 +++++++- .../r2dbc/mysql/codec/BooleanCodecTest.java | 115 ++++++++++++++++++ 2 files changed, 161 insertions(+), 3 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index f546ba751..0a59265c3 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java @@ -16,12 +16,15 @@ package io.asyncer.r2dbc.mysql.codec; +import java.math.BigInteger; + import io.asyncer.r2dbc.mysql.MySqlParameter; import io.asyncer.r2dbc.mysql.ParameterWriter; import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.r2dbc.spi.R2dbcNonTransientResourceException; import reactor.core.publisher.Mono; /** @@ -38,7 +41,35 @@ private BooleanCodec() { @Override public Boolean decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, CodecContext context) { - return binary || metadata.getType() == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0'; + MySqlType dataType = metadata.getType(); + + if (dataType == MySqlType.VARCHAR) { + if (!value.isReadable()) { + return createFromLong(0); + } + + String s = value.toString(metadata.getCharCollation(context).getCharset()); + + if (s.equalsIgnoreCase("Y") || s.equalsIgnoreCase("yes") || + s.equalsIgnoreCase("T") || s.equalsIgnoreCase("true")) { + return createFromLong(1); + } else if (s.equalsIgnoreCase("N") || s.equalsIgnoreCase("no") || + s.equalsIgnoreCase("F") || s.equalsIgnoreCase("false")) { + return createFromLong(0); + } else if (s.matches("-?\\d*\\.\\d*") || s.matches("-?\\d*\\.\\d+[eE]-?\\d+") + || s.matches("-?\\d*[eE]-?\\d+")) { + return createFromDouble(Double.parseDouble(s)); + } else if (s.matches("-?\\d+")) { + if (!CodecUtils.isGreaterThanLongMax(s)) { + return createFromLong(CodecUtils.parseLong(value)); + } + return createFromBigInteger(new BigInteger(s)); + } + throw new R2dbcNonTransientResourceException("The value '" + s + "' of type '" + dataType + + "' cannot be encoded into a Boolean.", "22018"); + } + + return binary || dataType == MySqlType.BIT ? value.readBoolean() : value.readByte() != '0'; } @Override @@ -54,8 +85,20 @@ public MySqlParameter encode(Object value, CodecContext context) { @Override public boolean doCanDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); - return (type == MySqlType.BIT || type == MySqlType.TINYINT) && - Integer.valueOf(1).equals(metadata.getPrecision()); + return ((type == MySqlType.BIT || type == MySqlType.TINYINT) && + Integer.valueOf(1).equals(metadata.getPrecision())) || type == MySqlType.VARCHAR; + } + + public Boolean createFromLong(long l) { + return (l == -1 || l > 0); + } + + public Boolean createFromDouble(double d) { + return (d == -1.0d || d > 0); + } + + public Boolean createFromBigInteger(BigInteger b) { + return b.compareTo(BigInteger.valueOf(0)) > 0 || b.compareTo(BigInteger.valueOf(-1)) == 0; } private static final class BooleanMySqlParameter extends AbstractMySqlParameter { diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java index 999111de5..dbfd5c104 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/BooleanCodecTest.java @@ -16,12 +16,22 @@ package io.asyncer.r2dbc.mysql.codec; +import io.asyncer.r2dbc.mysql.ConnectionContextTest; +import io.asyncer.r2dbc.mysql.constant.MySqlType; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import io.r2dbc.spi.R2dbcNonTransientException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.nio.charset.Charset; import java.util.Arrays; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + /** * Unit tests for {@link BooleanCodec}. */ @@ -55,4 +65,109 @@ public ByteBuf[] binaryParameters(Charset charset) { public ByteBuf sized(ByteBuf value) { return value; } + + @Test + void decodeString() { + Codec codec = getCodec(); + Charset c = ConnectionContextTest.mock().getClientCollation().getCharset(); + byte[] bOne = new byte[]{(byte)1}; + byte[] bZero = new byte[]{(byte)0}; + ByteBuffer bitValOne = ByteBuffer.wrap(bOne); + ByteBuffer bitValZero = ByteBuffer.wrap(bZero); + Decoding d1 = new Decoding(Unpooled.copiedBuffer("true", c), "true", MySqlType.VARCHAR); + Decoding d2 = new Decoding(Unpooled.copiedBuffer("false", c), "false", MySqlType.VARCHAR); + Decoding d3 = new Decoding(Unpooled.copiedBuffer("1", c), "1", MySqlType.VARCHAR); + Decoding d4 = new Decoding(Unpooled.copiedBuffer("0", c), "0", MySqlType.VARCHAR); + Decoding d5 = new Decoding(Unpooled.copiedBuffer("Y", c), "Y", MySqlType.VARCHAR); + Decoding d6 = new Decoding(Unpooled.copiedBuffer("no", c), "no", MySqlType.VARCHAR); + Decoding d7 = new Decoding(Unpooled.copiedBuffer("26.57", c), "26.57", MySqlType.VARCHAR); + Decoding d8 = new Decoding(Unpooled.copiedBuffer("-57", c), "=57", MySqlType.VARCHAR); + Decoding d9 = new Decoding(Unpooled.copiedBuffer("100000", c), "100000", MySqlType.VARCHAR); + Decoding d10 = new Decoding(Unpooled.copiedBuffer("-12345678901234567890", c), + "-12345678901234567890", MySqlType.VARCHAR); + Decoding d11 = new Decoding(Unpooled.copiedBuffer("Banana", c), "Banana", MySqlType.VARCHAR); + Decoding d12 = new Decoding(Unpooled.copiedBuffer(bitValOne), bitValOne, MySqlType.BIT); + Decoding d13 = new Decoding(Unpooled.copiedBuffer(bitValZero), bitValZero, MySqlType.BIT); + Decoding d14 = new Decoding(Unpooled.copyDouble(26.57d), 26.57d, MySqlType.DOUBLE); + Decoding d15 = new Decoding(Unpooled.copiedBuffer(bOne), bOne, MySqlType.TINYINT); + Decoding d16 = new Decoding(Unpooled.copiedBuffer(bZero), bZero, MySqlType.TINYINT); + Decoding d17 = new Decoding(Unpooled.copiedBuffer("1e4", c), "1e4", MySqlType.VARCHAR); + Decoding d18 = new Decoding(Unpooled.copiedBuffer("-1.34e10", c), "-1.34e10", MySqlType.VARCHAR); + Decoding d19 = new Decoding(Unpooled.copiedBuffer("-0", c), "-0", MySqlType.VARCHAR); + + assertThat(codec.decode(d1.content(), d1.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d1) + .isEqualTo(true); + + assertThat(codec.decode(d2.content(), d2.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d2) + .isEqualTo(false); + + assertThat(codec.decode(d3.content(), d3.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d3) + .isEqualTo(true); + + assertThat(codec.decode(d4.content(), d4.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d4) + .isEqualTo(false); + + assertThat(codec.decode(d5.content(), d5.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d5) + .isEqualTo(true); + + assertThat(codec.decode(d6.content(), d6.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d6) + .isEqualTo(false); + + assertThat(codec.decode(d7.content(), d7.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d7) + .isEqualTo(true); + + assertThat(codec.decode(d8.content(), d8.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d8) + .isEqualTo(false); + + assertThat(codec.decode(d9.content(), d9.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d9) + .isEqualTo(true); + + assertThat(codec.decode(d10.content(), d10.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d10) + .isEqualTo(false); + + assertThatThrownBy(() -> {codec.decode(d11.content(), d11.metadata(), Boolean.class, false, ConnectionContextTest.mock());}) + .isInstanceOf(R2dbcNonTransientException.class); + + assertThat(codec.decode(d12.content(), d12.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d12) + .isEqualTo(true); + + assertThat(codec.decode(d13.content(), d13.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d13) + .isEqualTo(false); + + assertThat(codec.decode(d14.content(), d14.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d14) + .isEqualTo(true); + + assertThat(codec.decode(d15.content(), d15.metadata(), Boolean.class, true, ConnectionContextTest.mock())) + .as("Decode failed, %s", d15) + .isEqualTo(true); + + assertThat(codec.decode(d16.content(), d16.metadata(), Boolean.class, true, ConnectionContextTest.mock())) + .as("Decode failed, %s", d16) + .isEqualTo(false); + + assertThat(codec.decode(d17.content(), d17.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d17) + .isEqualTo(true); + + assertThat(codec.decode(d18.content(), d18.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d18) + .isEqualTo(false); + + assertThat(codec.decode(d19.content(), d19.metadata(), Boolean.class, false, ConnectionContextTest.mock())) + .as("Decode failed, %s", d19) + .isEqualTo(false); + } } From 07bae6206bc3817c6f2c3aa014248040e5a3a968 Mon Sep 17 00:00:00 2001 From: jchrys Date: Wed, 1 Jan 2025 10:44:35 +0900 Subject: [PATCH 66/93] Update GH Actions (#295) Motivation: Need to update GH Actions before deprecation. Modifications: Updates setup-java, upload-artifact, checkout to v4 Results: Up to date --- .github/workflows/cd-release.yml | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index 74846be7a..afe780792 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -21,13 +21,13 @@ jobs: prepare: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - + - uses: actions/checkout@v4 - name: Set Up Java 8 - uses: actions/setup-java@v3 + + uses: actions/setup-java@v4 with: - distribution: 'temurin' # gh runner local caches lts temurins - java-version: '8' + distribution: "temurin" # gh runner local caches lts temurins + java-version: "8" - name: Setup Git Configs run: | @@ -63,7 +63,7 @@ jobs: run: ./.github/scripts/ensure_prepared.sh - name: Upload workspace - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: prepare-workspace path: ${{ github.workspace }} @@ -77,7 +77,7 @@ jobs: needs: prepare steps: - name: Download workspace - uses: actions/download-artifact@v4.1.7 + uses: actions/download-artifact@v4 with: name: prepare-workspace path: ./prepare-workspace/ @@ -88,10 +88,10 @@ jobs: chmod 755 ./prepare-workspace/.github/scripts/release_rollback.sh - name: Set up Java 8 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: 'temurin' # gh runner local caches lts temurins - java-version: '8' + distribution: "temurin" # gh runner local caches lts temurins + java-version: "8" - name: Setup git configs run: | @@ -123,7 +123,7 @@ jobs: - name: Create Local Deploy Directory run: mkdir -p ~/local-staging - + - name: Prepare Internal Dependencies working-directory: ./prepare-workspace/ run: ./mvnw -B -ntp -pl build-tools clean install -DskipTests -Dcheckstyle.skip @@ -131,16 +131,14 @@ jobs: - name: Import GPG & Deploy Local Staging working-directory: ./prepare-workspace/ run: | - cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import - ./mvnw -B -ntp -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" + cat <(echo -e "${{ secrets.GPG_PRIVATE_KEY }}") | gpg --batch --import + ./mvnw -B -ntp -pl r2dbc-mysql clean javadoc:jar package gpg:sign org.sonatype.plugins:nexus-staging-maven-plugin:deploy -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipRemoteStaging=true -DskipTests=true -Dcheckstyle.skip -Dgpg.passphrase="${{ secrets.GPG_PASSPHRASE }}" -Dgpg.keyname="${{ secrets.GPG_KEY_NAME }}" - name: Deploy Local Staged Artifacts working-directory: ./prepare-workspace/ run: ./mvnw -B -ntp -pl r2dbc-mysql --file pom.xml org.sonatype.plugins:nexus-staging-maven-plugin:deploy-staged -DnexusUrl=https://s01.oss.sonatype.org -DserverId=ossrh-staging -DaltStagingDirectory=/home/runner/local-staging -DskipStagingRepositoryClose=true -Dcheckstyle.skip - - name: Rollback Release working-directory: ./prepare-workspace/ if: ${{ failure() }} run: ./.github/scripts/release_rollback.sh trunk - From 0a8eb03419bf12b487adc3d259b3482ed36fb163 Mon Sep 17 00:00:00 2001 From: jchrys Date: Wed, 1 Jan 2025 11:16:23 +0900 Subject: [PATCH 67/93] Fix Release Action (#296) Motivation: Release action was broken. Modifications: includes hidden files when uploading arfiacts Results: Bug fixed --- .github/workflows/cd-release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index afe780792..a57a2e58e 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -67,6 +67,8 @@ jobs: with: name: prepare-workspace path: ${{ github.workspace }} + include-hidden-files: true + - name: Rollback Release working-directory: ./prepare-workspace/ if: ${{ failure() }} From aa15d0bc6570dc09dc9f20a6cdc012afc8db8309 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Wed, 1 Jan 2025 02:23:09 +0000 Subject: [PATCH 68/93] [maven-release-plugin] prepare release r2dbc-mysql-1.3.1 --- r2dbc-mysql/pom.xml | 4 ++-- test-native-image/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 64d20b48b..13bd87dbf 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.3.1-SNAPSHOT + 1.3.1 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.3.1 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 36ccf08e3..9cb428266 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.3.1-SNAPSHOT + 1.3.2-SNAPSHOT UTF-8 From 3bd23bedc210b8a6f6874b278f724ffb7f59b35a Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Wed, 1 Jan 2025 02:23:11 +0000 Subject: [PATCH 69/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 13bd87dbf..cb6a35607 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.3.1 + 1.3.2-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.3.1 + HEAD From 9d9e763a3b0a4de2f7009ebb4374c3a7ea1fb825 Mon Sep 17 00:00:00 2001 From: jchrys Date: Sun, 12 Jan 2025 04:26:53 +0900 Subject: [PATCH 70/93] Update README (#297) Motivation: Released 1.3.1 Modifications: Released 1.3.1 Result: up-to-date --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index e581a7159..2a9e75b77 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Reactive Relational Database Connectivity MySQL Implementation + ![Maven Central](https://img.shields.io/maven-central/v/io.asyncer/r2dbc-mysql?color=blue) ![LICENSE](https://img.shields.io/github/license/asyncer-io/r2dbc-mysql) @@ -10,15 +11,17 @@ delegate to. See [R2DBC Homepage](https://r2dbc.io). See [R2DBC MySQL wiki](https://github.com/asyncer-io/r2dbc-mysql/wiki) for more information. ## Spring-framework and R2DBC-SPI Compatibility + Refer to the table below to determine the appropriate version of r2dbc-mysql for your project. | spring-boot-starter-data-r2dbc | spring-data-r2dbc | r2dbc-spi | r2dbc-mysql(recommended) | |--------------------------------|-------------------|---------------|------------------------------| -| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.2.0 | +| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.3.1 | | 2.7.* | 1.5.* | 0.9.1.RELEASE | io.asyncer:r2dbc-mysql:0.9.7 | | 2.6.* and below | 1.4.* and below | 0.8.6.RELEASE | dev.miku:r2dbc-mysql:0.8.2 | ## Supported Features + This driver provides the following features: - [x] Unix domain socket. @@ -37,6 +40,7 @@ This driver provides the following features: - [x] MariaDB `RETURNING` clause. ## Version compatibility / Integration tests states + ![MySQL 5.5 status](https://img.shields.io/badge/MySQL%205.5-pass-blue) ![MySQL 5.6 status](https://img.shields.io/badge/MySQL%205.6-pass-blue) ![MySQL 5.7 status](https://img.shields.io/badge/MySQL%205.7-pass-blue) @@ -49,7 +53,6 @@ This driver provides the following features: ![MariaDB 10.6 status](https://img.shields.io/badge/MariaDB%2010.6-pass-blue) ![MariaDB 10.11 status](https://img.shields.io/badge/MariaDB%2010.11-pass-blue) - In fact, it supports lower versions, in the theory, such as 4.1, 4.0, etc. However, Docker-certified images do not have these versions lower than 5.5.0, so tests are not integrated on these versions. @@ -61,7 +64,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so io.asyncer r2dbc-mysql - 1.2.0 + 1.3.1 ``` @@ -71,7 +74,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so ```groovy dependencies { - implementation 'io.asyncer:r2dbc-mysql:1.2.0' + implementation 'io.asyncer:r2dbc-mysql:1.3.1' } ``` @@ -80,7 +83,7 @@ dependencies { ```kotlin dependencies { // Maybe should to use `compile` instead of `implementation` on the lower version of Gradle. - implementation("io.asyncer:r2dbc-mysql:1.2.0") + implementation("io.asyncer:r2dbc-mysql:1.3.1") } ``` @@ -113,7 +116,7 @@ See [Usage](https://github.com/asyncer-io/r2dbc-mysql/wiki/usage) wiki for more ## Reporting Issues -The R2DBC MySQL Implementation uses GitHub as issue tracking system to record bugs and feature requests. +The R2DBC MySQL Implementation uses GitHub as issue tracking system to record bugs and feature requests. If you want to raise an issue, please follow the recommendations below: - Before log a bug, please search the [issue tracker](https://github.com/asyncer-io/r2dbc-mysql/issues) to see if someone has already reported the problem. @@ -152,7 +155,7 @@ Thanks a lot for your support! ## Supports -- [R2DBC Team](https://r2dbc.io) - Thanks for their support by sharing all relevant resources around R2DBC +- [R2DBC Team](https://r2dbc.io) - Thanks for their support by sharing all relevant resources around R2DBC projects. [m]: https://www.mysql.com From d2854390d7c2fd3e75e7c2b884fe4c906126de4e Mon Sep 17 00:00:00 2001 From: rxdcxdrnine Date: Sat, 18 Jan 2025 17:47:01 +0900 Subject: [PATCH 71/93] Add Metrics Configuration Support to enable TcpClient metrics --- .../mysql/MySqlConnectionConfiguration.java | 42 ++++++++++++++++--- .../r2dbc/mysql/MySqlConnectionFactory.java | 3 +- .../mysql/MySqlConnectionFactoryProvider.java | 12 ++++++ .../io/asyncer/r2dbc/mysql/client/Client.java | 6 ++- .../MySqlConnectionConfigurationTest.java | 13 ++++++ .../MySqlConnectionFactoryProviderTest.java | 13 ++++++ 6 files changed, 81 insertions(+), 8 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 2f1c75961..8b4c789de 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -25,6 +25,7 @@ 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; @@ -131,6 +132,8 @@ public final class MySqlConnectionConfiguration { @Nullable private final AddressResolverGroup resolver; + private final boolean metrics; + private MySqlConnectionConfiguration( boolean isHost, String domain, int port, MySqlSslConfiguration ssl, boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout, @@ -146,7 +149,8 @@ private MySqlConnectionConfiguration( Set compressionAlgorithms, int zstdCompressionLevel, @Nullable LoopResources loopResources, Extensions extensions, @Nullable Publisher passwordPublisher, - @Nullable AddressResolverGroup resolver + @Nullable AddressResolverGroup resolver, + boolean metrics ) { this.isHost = isHost; this.domain = domain; @@ -177,6 +181,7 @@ private MySqlConnectionConfiguration( this.extensions = extensions; this.passwordPublisher = passwordPublisher; this.resolver = resolver; + this.metrics = metrics; } /** @@ -312,6 +317,10 @@ AddressResolverGroup getResolver() { return resolver; } + boolean isMetrics() { + return metrics; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -349,7 +358,8 @@ public boolean equals(Object o) { Objects.equals(loopResources, that.loopResources) && extensions.equals(that.extensions) && Objects.equals(passwordPublisher, that.passwordPublisher) && - Objects.equals(resolver, that.resolver); + Objects.equals(resolver, that.resolver) && + metrics == that.metrics; } @Override @@ -364,7 +374,7 @@ public int hashCode() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, - loopResources, extensions, passwordPublisher, resolver); + loopResources, extensions, passwordPublisher, resolver, metrics); } @Override @@ -398,7 +408,8 @@ private String buildCommonToStringPart() { ", loopResources=" + loopResources + ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + - ", resolver=" + resolver; + ", resolver=" + resolver + + ", metrics=" + metrics; } /** @@ -498,6 +509,8 @@ public static final class Builder { @Nullable private AddressResolverGroup resolver; + private boolean metrics; + /** * Builds an immutable {@link MySqlConnectionConfiguration} with current options. * @@ -532,7 +545,7 @@ public MySqlConnectionConfiguration build() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, - Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver); + Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics); } /** @@ -1175,6 +1188,25 @@ public Builder resolver(AddressResolverGroup 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; + } + private SslMode requireSslMode() { SslMode sslMode = this.sslMode; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index bff85c809..e483d2d6d 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -148,7 +148,8 @@ private static Mono getMySqlConnection( context, configuration.getConnectTimeout(), configuration.getLoopResources(), - configuration.getResolver() + configuration.getResolver(), + configuration.isMetrics() )).flatMap(client -> { // Lazy init database after handshake/login boolean deferDatabase = configuration.isCreateDatabaseIfNotExist(); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index d89005394..8f045fcad 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -320,6 +320,16 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr */ 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"); + @Override public ConnectionFactory create(ConnectionFactoryOptions options) { requireNonNull(options, "connectionFactoryOptions must not be null"); @@ -413,6 +423,8 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { .to(builder::lockWaitTimeout); mapper.optional(STATEMENT_TIMEOUT).as(Duration.class, Duration::parse) .to(builder::statementTimeout); + mapper.optional(METRICS).asBoolean() + .to(builder::metrics); return builder.build(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java index 0beaf4c0d..316d90999 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/Client.java @@ -127,19 +127,21 @@ public interface Client { * @param context the connection context * @param connectTimeout connect timeout, or {@code null} if it has no timeout * @param loopResources the loop resources to use + * @param metrics if enable the {@link TcpClient#metrics)} * @return A {@link Mono} that will emit a connected {@link Client}. * @throws IllegalArgumentException if {@code ssl}, {@code address} or {@code context} is {@code null}. * @throws ArithmeticException if {@code connectTimeout} milliseconds overflow as an int */ static Mono connect(MySqlSslConfiguration ssl, SocketAddress address, boolean tcpKeepAlive, boolean tcpNoDelay, ConnectionContext context, @Nullable Duration connectTimeout, - LoopResources loopResources, @Nullable AddressResolverGroup resolver) { + LoopResources loopResources, @Nullable AddressResolverGroup resolver, boolean metrics) { requireNonNull(ssl, "ssl must not be null"); requireNonNull(address, "address must not be null"); requireNonNull(context, "context must not be null"); TcpClient tcpClient = TcpClient.newConnection() - .runOn(loopResources); + .runOn(loopResources) + .metrics(metrics); if (connectTimeout != null) { tcpClient = tcpClient.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java index f05defb17..e62fea190 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfigurationTest.java @@ -222,6 +222,19 @@ void validResolver() { assertThat(resolverGroup).isSameAs(resolver); } + @Test + void invalidMetrics() { + // throw exception when metrics true without micrometer-core dependency + assertThatIllegalArgumentException().isThrownBy(() -> + MySqlConnectionConfiguration + .builder() + .host(HOST) + .user(USER) + .metrics(true) + .build() + ); + } + private static MySqlConnectionConfiguration unixSocketSslMode(SslMode sslMode) { return MySqlConnectionConfiguration.builder() .unixSocket(UNIX_SOCKET) diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java index 1e71a9f17..be48a2255 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProviderTest.java @@ -51,6 +51,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.METRICS; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.PASSWORD_PUBLISHER; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.RESOLVER; import static io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider.USE_SERVER_PREPARE_STATEMENT; @@ -469,6 +470,18 @@ void validResolver() { assertThat(ConnectionFactories.get(options)).isExactlyInstanceOf(MySqlConnectionFactory.class); } + @Test + void invalidMetrics() { + // throw exception when metrics true without micrometer-core dependency + assertThatIllegalArgumentException().isThrownBy(() -> + ConnectionFactories.get(ConnectionFactoryOptions.builder() + .option(DRIVER, "mysql") + .option(HOST, "127.0.0.1") + .option(USER, "root") + .option(METRICS, true) + .build())); + } + @Test void allConfigurationOptions() { List exceptConfigs = Arrays.asList( From 7b28fe37705af953e8ff23b5ade316ad9afeec53 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Thu, 23 Jan 2025 20:23:36 +0000 Subject: [PATCH 72/93] [maven-release-plugin] prepare release r2dbc-mysql-1.3.2 --- r2dbc-mysql/pom.xml | 4 ++-- test-native-image/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index cb6a35607..a4adcd057 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.3.2-SNAPSHOT + 1.3.2 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.3.2 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 9cb428266..412e2a616 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.3.2-SNAPSHOT + 1.3.3-SNAPSHOT UTF-8 From 820bdf47e06c8c4f55eb7b8c792b871a8c461b0e Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Thu, 23 Jan 2025 20:23:39 +0000 Subject: [PATCH 73/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index a4adcd057..2c16d5055 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.3.2 + 1.3.3-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.3.2 + HEAD From abaa2d1a4703ddd754d2c2fc5d1a4ce8f5039e66 Mon Sep 17 00:00:00 2001 From: jchrys Date: Fri, 31 Jan 2025 20:44:36 +0900 Subject: [PATCH 74/93] Support `tinyInt1isBit` Motivation: Aligning with MySQL connector. Modifications: Implemented `tinyInt1isBit` flag. Result: Improved compatibility with MySQL connectors. --- r2dbc-mysql/pom.xml | 2 +- .../r2dbc/mysql/ConnectionContext.java | 9 +++ .../mysql/MySqlConnectionConfiguration.java | 69 +++++++++++++------ .../r2dbc/mysql/MySqlConnectionFactory.java | 1 + .../mysql/MySqlConnectionFactoryProvider.java | 13 +++- .../r2dbc/mysql/codec/CodecContext.java | 6 ++ .../r2dbc/mysql/codec/DefaultCodecs.java | 23 +++++-- .../r2dbc/mysql/ConnectionContextTest.java | 8 +-- .../mysql/ConnectionIntegrationTest.java | 25 +++++++ .../r2dbc/mysql/TinyInt1isBitFalseTest.java | 41 +++++++++++ test-native-image/pom.xml | 2 +- 11 files changed, 165 insertions(+), 34 deletions(-) create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TinyInt1isBitFalseTest.java diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 2c16d5055..43d118836 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.3.3-SNAPSHOT + 1.4.0-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java index cc5aeb2a9..26ec660c4 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java @@ -51,6 +51,8 @@ public final class ConnectionContext implements CodecContext { private final int localInfileBufferSize; + private final boolean tinyInt1isBit; + private final boolean preserveInstants; private int connectionId = -1; @@ -107,12 +109,14 @@ public final class ConnectionContext implements CodecContext { ZeroDateOption zeroDateOption, @Nullable Path localInfilePath, int localInfileBufferSize, + boolean tinyInt1isBit, boolean preserveInstants, @Nullable ZoneId timeZone ) { this.zeroDateOption = requireNonNull(zeroDateOption, "zeroDateOption must not be null"); this.localInfilePath = localInfilePath; this.localInfileBufferSize = localInfileBufferSize; + this.tinyInt1isBit = tinyInt1isBit; this.preserveInstants = preserveInstants; this.timeZone = timeZone; } @@ -216,6 +220,11 @@ public boolean isMariaDb() { return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb(); } + @Override + public boolean isTinyInt1isBit() { + return tinyInt1isBit; + } + public boolean isNoBackslashEscapes() { return (serverStatuses & ServerStatuses.NO_BACKSLASH_ESCAPES) != 0; } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 8b4c789de..93ba9ce78 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -134,24 +134,26 @@ public final class MySqlConnectionConfiguration { private final boolean metrics; + private final boolean tinyInt1isBit; + private MySqlConnectionConfiguration( - boolean isHost, String domain, int port, MySqlSslConfiguration ssl, - boolean tcpKeepAlive, boolean tcpNoDelay, @Nullable Duration connectTimeout, - ZeroDateOption zeroDateOption, - boolean preserveInstants, - String connectionTimeZone, - boolean forceConnectionTimeZoneToSession, - String user, @Nullable CharSequence password, @Nullable String database, - boolean createDatabaseIfNotExist, @Nullable Predicate preferPrepareStatement, - List sessionVariables, @Nullable Duration lockWaitTimeout, @Nullable Duration statementTimeout, - @Nullable Path loadLocalInfilePath, int localInfileBufferSize, - int queryCacheSize, int prepareCacheSize, - Set compressionAlgorithms, int zstdCompressionLevel, - @Nullable LoopResources loopResources, - Extensions extensions, @Nullable Publisher passwordPublisher, - @Nullable AddressResolverGroup resolver, - boolean metrics - ) { + 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; @@ -182,6 +184,7 @@ private MySqlConnectionConfiguration( this.passwordPublisher = passwordPublisher; this.resolver = resolver; this.metrics = metrics; + this.tinyInt1isBit = tinyInt1isBit; } /** @@ -321,6 +324,10 @@ boolean isMetrics() { return metrics; } + boolean isTinyInt1isBit() { + return tinyInt1isBit; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -359,7 +366,8 @@ public boolean equals(Object o) { extensions.equals(that.extensions) && Objects.equals(passwordPublisher, that.passwordPublisher) && Objects.equals(resolver, that.resolver) && - metrics == that.metrics; + metrics == that.metrics && + tinyInt1isBit == that.tinyInt1isBit; } @Override @@ -374,7 +382,7 @@ public int hashCode() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, - loopResources, extensions, passwordPublisher, resolver, metrics); + loopResources, extensions, passwordPublisher, resolver, metrics, tinyInt1isBit); } @Override @@ -409,7 +417,8 @@ private String buildCommonToStringPart() { ", extensions=" + extensions + ", passwordPublisher=" + passwordPublisher + ", resolver=" + resolver + - ", metrics=" + metrics; + ", metrics=" + metrics + + ", tinyint1isBit=" + tinyInt1isBit; } /** @@ -511,6 +520,8 @@ public static final class Builder { private boolean metrics; + private boolean tinyInt1isBit = true; + /** * Builds an immutable {@link MySqlConnectionConfiguration} with current options. * @@ -545,11 +556,11 @@ public MySqlConnectionConfiguration build() { loadLocalInfilePath, localInfileBufferSize, queryCacheSize, prepareCacheSize, compressionAlgorithms, zstdCompressionLevel, loopResources, - Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics); + Extensions.from(extensions, autodetectExtensions), passwordPublisher, resolver, metrics, tinyInt1isBit); } /** - * Configures the database. Default no database. + * Configures the database. Default no database. * * @param database the database, or {@code null} if no database want to be login. * @return this {@link Builder}. @@ -1207,6 +1218,20 @@ public Builder metrics(boolean enabled) { return this; } + /** + * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type. + * When enabled, TINYINT(1) columns (both SIGNED and UNSIGNED) will be treated as + * BIT. default to {@code true}. + * + * @param tinyInt1isBit {@code true} to treat TINYINT(1) as BIT + * @return this {@link Builder} + * @since 1.4.0 + */ + public Builder tinyInt1isBit(boolean tinyInt1isBit) { + this.tinyInt1isBit = tinyInt1isBit; + return this; + } + private SslMode requireSslMode() { SslMode sslMode = this.sslMode; diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index e483d2d6d..a6880cc82 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -137,6 +137,7 @@ private static Mono getMySqlConnection( configuration.getZeroDateOption(), configuration.getLoadLocalInfilePath(), configuration.getLocalInfileBufferSize(), + configuration.isTinyInt1isBit(), configuration.isPreserveInstants(), connectionTimeZone ); diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index 8f045fcad..bff335123 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -330,6 +330,15 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr */ 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 (both SIGNED and UNSIGNED) will be treated as + * BIT. default to {@code true}. + * + * @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"); @@ -424,7 +433,9 @@ static MySqlConnectionConfiguration setup(ConnectionFactoryOptions options) { mapper.optional(STATEMENT_TIMEOUT).as(Duration.class, Duration::parse) .to(builder::statementTimeout); mapper.optional(METRICS).asBoolean() - .to(builder::metrics); + .to(builder::metrics); + mapper.optional(TINY_INT_1_IS_BIT).asBoolean() + .to(builder::tinyInt1isBit); return builder.build(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java index 8eda9c985..5b58fa5f6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/CodecContext.java @@ -69,4 +69,10 @@ public interface CodecContext { * @return if is MariaDB. */ boolean isMariaDb(); + + /** + * + * @return true if tinyInt(1) is treated as bit. + */ + boolean isTinyInt1isBit(); } diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index 4542a7c9d..ba022875a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -45,6 +45,8 @@ */ final class DefaultCodecs implements Codecs { + private static final Integer INTEGER_ONE = Integer.valueOf(1); + private static final List> DEFAULT_CODECS = InternalArrays.asImmutableList( ByteCodec.INSTANCE, ShortCodec.INSTANCE, @@ -137,6 +139,7 @@ private DefaultCodecs(List> codecs) { * Note: this method should NEVER release {@code buf} because of it come from {@code MySqlRow} which will release * this buffer. */ + @Nullable @Override public T decode(FieldValue value, MySqlReadableMetadata metadata, Class type, boolean binary, CodecContext context) { @@ -151,7 +154,7 @@ public T decode(FieldValue value, MySqlReadableMetadata metadata, Class t return null; } - Class target = chooseClass(metadata, type); + Class target = chooseClass(metadata, type, context); if (value instanceof NormalFieldValue) { return decodeNormal((NormalFieldValue) value, metadata, target, binary, context); @@ -162,6 +165,7 @@ public T decode(FieldValue value, MySqlReadableMetadata metadata, Class t throw new IllegalArgumentException("Unknown value " + value.getClass().getSimpleName()); } + @Nullable @Override public T decode(FieldValue value, MySqlReadableMetadata metadata, ParameterizedType type, boolean binary, CodecContext context) { @@ -359,18 +363,27 @@ private T decodeMassive(LargeFieldValue value, MySqlReadableMetadata metadat * @param type the {@link Class} specified by the user. * @return the {@link Class} to use for decoding. */ - private static Class chooseClass(final MySqlReadableMetadata metadata, Class type) { - final Class javaType = getDefaultJavaType(metadata); + private static Class chooseClass(final MySqlReadableMetadata metadata, Class type, + final CodecContext codecContext) { + final Class javaType = getDefaultJavaType(metadata, codecContext); return type.isAssignableFrom(javaType) ? javaType : type; } - private static Class getDefaultJavaType(final MySqlReadableMetadata metadata) { + private static Class getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) { final MySqlType type = metadata.getType(); + final Integer precision = metadata.getPrecision(); + + if (INTEGER_ONE.equals(precision) && (type == MySqlType.TINYINT || type == MySqlType.TINYINT_UNSIGNED) + && codecContext.isTinyInt1isBit()) { + return Boolean.class; + } + // ref: https://github.com/asyncer-io/r2dbc-mysql/issues/277 // BIT(1) should be treated as Boolean by default. - if (type == MySqlType.BIT && Integer.valueOf(1).equals(metadata.getPrecision())) { + if (INTEGER_ONE.equals(precision) && type == MySqlType.BIT) { return Boolean.class; } + return type.getJavaType(); } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java index 5d0635412..eb4a3ea3c 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionContextTest.java @@ -39,7 +39,7 @@ void getTimeZone() { String id = i < 0 ? "UTC" + i : "UTC+" + i; ConnectionContext context = new ConnectionContext( ZeroDateOption.USE_NULL, null, - 8192, true, ZoneId.of(id)); + 8192, true, true, ZoneId.of(id)); assertThat(context.getTimeZone()).isEqualTo(ZoneId.of(id)); } @@ -48,7 +48,7 @@ void getTimeZone() { @Test void setTwiceTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, true, null); + 8192, true, true, null); context.initSession( Caches.createPrepareCache(0), @@ -70,7 +70,7 @@ void setTwiceTimeZone() { @Test void badSetTimeZone() { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, true, ZoneId.systemDefault()); + 8192, true, true, ZoneId.systemDefault()); assertThatIllegalStateException().isThrownBy(() -> context.initSession( Caches.createPrepareCache(0), IsolationLevel.REPEATABLE_READ, @@ -91,7 +91,7 @@ public static ConnectionContext mock(boolean isMariaDB) { public static ConnectionContext mock(boolean isMariaDB, ZoneId zoneId) { ConnectionContext context = new ConnectionContext(ZeroDateOption.USE_NULL, null, - 8192, true, zoneId); + 8192, true, true, zoneId); context.initHandshake(1, ServerVersion.parse(isMariaDB ? "11.2.22.MOCKED" : "8.0.11.MOCKED"), Capability.of(~(isMariaDB ? 1 : 0))); diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index b65b3b447..fccd38530 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -579,6 +579,31 @@ void loadDataLocalInfile(String name) throws URISyntaxException, IOException { .doOnNext(it -> assertThat(it).isEqualTo(json))); } + @Test + public void tinyInt1isBitTrueTestValue1() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Boolean.class)) + .doOnNext(value -> assertThat(value).isEqualTo(true)) + ); + } + + @Test + public void tinyInt1isBitTrueTestValue0() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 0)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Boolean.class)) + .doOnNext(value -> assertThat(value).isEqualTo(false))); + } + @Test void batchCrud() { // TODO: spilt it to multiple test cases and move it to BatchIntegrationTest diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TinyInt1isBitFalseTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TinyInt1isBitFalseTest.java new file mode 100644 index 000000000..9db19e414 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/TinyInt1isBitFalseTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql; + + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +class TinyInt1isBitFalseTest extends IntegrationTestSupport{ + TinyInt1isBitFalseTest() { + super(configuration(builder -> builder.tinyInt1isBit(false))); + } + + @Test + public void tinyInt1isBitFalse() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Byte.class))); + } + +} diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 412e2a616..1640175cb 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.3.3-SNAPSHOT + 1.4.0-SNAPSHOT UTF-8 From 0764d05232122dca255ef215973391533b077e1d Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 10 Feb 2025 11:52:05 +0800 Subject: [PATCH 75/93] Refactor create method of MySQLConnectionFactory --- .../r2dbc/mysql/MySqlConnectionFactory.java | 81 +++++++++---------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java index a6880cc82..094674f2a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactory.java @@ -42,68 +42,65 @@ */ public final class MySqlConnectionFactory implements ConnectionFactory { - private final Mono client; + private final MySqlConnectionConfiguration configuration; + private final LazyQueryCache queryCache; - private MySqlConnectionFactory(Mono client) { - this.client = client; + private MySqlConnectionFactory(MySqlConnectionConfiguration configuration) { + this.configuration = configuration; + this.queryCache = new LazyQueryCache(configuration.getQueryCacheSize()); } @Override public Mono create() { - return client; - } - - @Override - public ConnectionFactoryMetadata getMetadata() { - return MySqlConnectionFactoryMetadata.INSTANCE; - } + MySqlSslConfiguration ssl; + SocketAddress address; - /** - * Creates a {@link MySqlConnectionFactory} with a {@link MySqlConnectionConfiguration}. - * - * @param configuration the {@link MySqlConnectionConfiguration}. - * @return configured {@link MySqlConnectionFactory}. - */ - public static MySqlConnectionFactory from(MySqlConnectionConfiguration configuration) { - requireNonNull(configuration, "configuration must not be null"); - - LazyQueryCache queryCache = new LazyQueryCache(configuration.getQueryCacheSize()); - - return new MySqlConnectionFactory(Mono.defer(() -> { - MySqlSslConfiguration ssl; - SocketAddress address; - - if (configuration.isHost()) { - ssl = configuration.getSsl(); - address = InetSocketAddress.createUnresolved(configuration.getDomain(), + if (configuration.isHost()) { + ssl = configuration.getSsl(); + address = InetSocketAddress.createUnresolved(configuration.getDomain(), configuration.getPort()); - } else { - ssl = MySqlSslConfiguration.disabled(); - address = new DomainSocketAddress(configuration.getDomain()); - } + } else { + ssl = MySqlSslConfiguration.disabled(); + address = new DomainSocketAddress(configuration.getDomain()); + } - String user = configuration.getUser(); - CharSequence password = configuration.getPassword(); - Publisher passwordPublisher = configuration.getPasswordPublisher(); + String user = configuration.getUser(); + CharSequence password = configuration.getPassword(); + Publisher passwordPublisher = configuration.getPasswordPublisher(); - if (Objects.nonNull(passwordPublisher)) { - return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( + if (Objects.nonNull(passwordPublisher)) { + return Mono.from(passwordPublisher).flatMap(token -> getMySqlConnection( configuration, ssl, queryCache, address, user, token - )); - } + )); + } - return getMySqlConnection( + return getMySqlConnection( configuration, ssl, queryCache, address, user, password - ); - })); + ); + } + + @Override + public ConnectionFactoryMetadata getMetadata() { + return MySqlConnectionFactoryMetadata.INSTANCE; + } + + /** + * Creates a {@link MySqlConnectionFactory} with a {@link MySqlConnectionConfiguration}. + * + * @param configuration the {@link MySqlConnectionConfiguration}. + * @return configured {@link MySqlConnectionFactory}. + */ + public static MySqlConnectionFactory from(MySqlConnectionConfiguration configuration) { + requireNonNull(configuration, "configuration must not be null"); + return new MySqlConnectionFactory(configuration); } /** From a1c99ea5ab9585c7bffff7a20bac1eaf2d7d38aa Mon Sep 17 00:00:00 2001 From: jchrys Date: Sun, 16 Feb 2025 06:23:27 +0900 Subject: [PATCH 76/93] use ubuntu-latest Motivation: Ubuntu 20.04 runner will be unsupported by april 1. Modification: Switched to ubuntu-latest Result: Ensure future compatibility. --- .github/workflows/ci-graalvm-tests.yml | 14 +++++++------- .github/workflows/ci-integration-tests.yml | 7 ++++--- .../workflows/ci-mariadb-intergration-tests.yml | 7 ++++--- .github/workflows/ci-unit-tests.yml | 8 ++++---- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci-graalvm-tests.yml b/.github/workflows/ci-graalvm-tests.yml index ee5f0d03d..54db266ff 100644 --- a/.github/workflows/ci-graalvm-tests.yml +++ b/.github/workflows/ci-graalvm-tests.yml @@ -16,18 +16,18 @@ name: Native Image Build Test on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: graalvm-build-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: graalvm/setup-graalvm@v1 with: java-version: 21 - distribution: 'graalvm' + distribution: "graalvm" native-image-job-reports: true github-token: ${{ secrets.GITHUB_TOKEN }} @@ -43,7 +43,7 @@ jobs: - name: Build and run native image run: | - echo "JAVA_HOME=$JAVA_HOME" - echo "./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true" - ./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true - ./test-native-image/target/test-native-image + echo "JAVA_HOME=$JAVA_HOME" + echo "./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true" + ./mvnw -Pgraalvm package -Dmaven.javadoc.skip=true + ./test-native-image/target/test-native-image diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 8cf8991d2..1ef3cdb0e 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -2,14 +2,15 @@ name: Integration Tests on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: integration-tests-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - mysql-version: [ 5.5, 5.6.45, 5.6, 5.7.28, 5.7, 8.0, 8.1, 8.2, 8.3, 8.4, 9.0] + mysql-version: + [5.5, 5.6.45, 5.6, 5.7.28, 5.7, 8.0, 8.1, 8.2, 8.3, 8.4, 9.0] name: Integration test with MySQL ${{ matrix.mysql-version }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/ci-mariadb-intergration-tests.yml b/.github/workflows/ci-mariadb-intergration-tests.yml index d20bba01a..25cf0e498 100644 --- a/.github/workflows/ci-mariadb-intergration-tests.yml +++ b/.github/workflows/ci-mariadb-intergration-tests.yml @@ -2,14 +2,15 @@ name: Integration Tests for MariaDB on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: mariadb-integration-tests-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - mariadb-version: [ 10.0, 10.1, 10.2.15, 10.2, 10.3.7, 10.3, 10.5.1, 10.5, 10.6, 10.11] + mariadb-version: + [10.0, 10.1, 10.2.15, 10.2, 10.3.7, 10.3, 10.5.1, 10.5, 10.6, 10.11] name: Integration test with MariaDB ${{ matrix.mariadb-version }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml index fdf48ce0d..0247ca1d7 100644 --- a/.github/workflows/ci-unit-tests.yml +++ b/.github/workflows/ci-unit-tests.yml @@ -2,14 +2,14 @@ name: Unit tests on: pull_request: - branches: [ "trunk", "0.9.x" ] + branches: ["trunk", "0.9.x"] jobs: unit-tests-pr: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest strategy: matrix: - java-version: [ 8, 11, 17, 21 ] + java-version: [8, 11, 17, 21] name: linux-java-${{ matrix.java-version }} steps: - uses: actions/checkout@v3 @@ -20,7 +20,7 @@ jobs: java-version: ${{ matrix.java-version }} cache: maven - name: Unit test with Maven - run: | + run: | set -o pipefail ./mvnw -B test -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \ -Dio.netty.leakDetectionLevel=paranoid \ From badb3e5cbc72473d60e9ab4f6d445c8789ca09eb Mon Sep 17 00:00:00 2001 From: jchrys Date: Mon, 17 Feb 2025 19:33:24 +0900 Subject: [PATCH 77/93] Fix false positive unexpected connection close Motivation: The connection could be incorrectly detected as unexpectedly closed due to a race condition where the exit message was sent before updating the state to `ST_CLOSING`. Modifications: Ensured state update to `ST_CLOSING` happens before sending the exit message. Result: State update to `ST_CLOSING` happens before sending the exit message, preventing false positive unexpected close detections. resolves #275 --- .../r2dbc/mysql/client/ReactorNettyClient.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index 961b2806e..8e926f21e 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -131,6 +131,14 @@ final class ReactorNettyClient implements Client { logger.debug("Request: {}", message); } + if (message == ExitMessage.INSTANCE) { + if (STATE_UPDATER.compareAndSet(this, ST_CONNECTED, ST_CLOSING)) { + logger.debug("Exit message sent"); + } else { + logger.debug("Exit message sent (duplicated / connection already closed)"); + } + } + if (message.isSequenceReset()) { resetSequence(connection); } @@ -213,15 +221,8 @@ public Mono close() { requestQueue.submit(RequestTask.wrap(sink, Mono.fromRunnable(() -> { Sinks.EmitResult result = requests.tryEmitNext(ExitMessage.INSTANCE); - if (result != Sinks.EmitResult.OK) { logger.error("Exit message sending failed due to {}, force closing", result); - } else { - if (STATE_UPDATER.compareAndSet(this, ST_CONNECTED, ST_CLOSING)) { - logger.debug("Exit message sent"); - } else { - logger.debug("Exit message sent (duplicated / connection already closed)"); - } } }))); }).flatMap(Function.identity()).onErrorResume(e -> { From 7f17f9aa5e06272e21418297a42da49924a5bbec Mon Sep 17 00:00:00 2001 From: jchrys Date: Tue, 18 Feb 2025 20:45:09 +0900 Subject: [PATCH 78/93] Upgrade Project Reactor to 2024.0.3 Motivation: The project was using an outdated Project Reactor (2022.0.16). Modifications: Updated Project Reactor to 2024.0.3. Result: Latest Project Reactor applied --- r2dbc-mysql/pom.xml | 4 ++-- test-native-image/pom.xml | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 43d118836..21e6e9356 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -73,8 +73,8 @@ false 1.0.0.RELEASE - 2022.0.16 - 4.1.106.Final + 2024.0.3 + 4.1.118.Final 3.25.3 1.37 5.10.2 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 1640175cb..3ea1602d0 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -15,9 +15,9 @@ 8 true - 2022.0.16 + 2024.0.3 1.0.0.RELEASE - 20.3.13 + 20.3.17 @@ -67,7 +67,12 @@ ${skipNativeImage} ${project.artifactId} io.asyncer.Main - --report-unsupported-elements-at-runtime --allow-incomplete-classpath --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils + + --report-unsupported-elements-at-runtime + --allow-incomplete-classpath + --initialize-at-run-time=io.netty.handler.ssl.BouncyCastleAlpnSslUtils + --initialize-at-run-time=io.netty.handler.ssl.JdkSslServerContext + From e211cd89ec4bc9ef034eef00d098d627834b67a3 Mon Sep 17 00:00:00 2001 From: jchrys Date: Tue, 18 Feb 2025 23:31:07 +0900 Subject: [PATCH 79/93] Upgrade Maven to the latest version Motivation: Maven version was outdated. Modifications: Updated Maven to the latest version. Result: Maven is now up to date. --- .mvn/wrapper/maven-wrapper.properties | 7 +- mvnw | 437 ++++++++++++-------------- mvnw.cmd | 306 ++++++++---------- 3 files changed, 323 insertions(+), 427 deletions(-) diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 110dad7d8..d58dfb70b 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -6,7 +6,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -14,5 +14,6 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.3/apache-maven-3.9.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar \ No newline at end of file +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip diff --git a/mvnw b/mvnw index 66df28542..19529ddf8 100755 --- a/mvnw +++ b/mvnw @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# https://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an @@ -19,290 +19,241 @@ # ---------------------------------------------------------------------------- # ---------------------------------------------------------------------------- -# Apache Maven Wrapper startup batch script, version 3.2.0 -# -# Required ENV vars: -# ------------------ -# JAVA_HOME - location of a JDK home dir +# Apache Maven Wrapper startup batch script, version 3.3.2 # # Optional ENV vars # ----------------- -# MAVEN_OPTS - parameters passed to the Java VM when running Maven -# e.g. to debug Maven itself, use -# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output # ---------------------------------------------------------------------------- -if [ -z "$MAVEN_SKIP_RC" ] ; then - - if [ -f /usr/local/etc/mavenrc ] ; then - . /usr/local/etc/mavenrc - fi - - if [ -f /etc/mavenrc ] ; then - . /etc/mavenrc - fi - - if [ -f "$HOME/.mavenrc" ] ; then - . "$HOME/.mavenrc" - fi - -fi +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x -# OS specific support. $var _must_ be set to either true or false. -cygwin=false; -darwin=false; -mingw=false +# OS specific support. +native_path() { printf %s\\n "$1"; } case "$(uname)" in - CYGWIN*) cygwin=true ;; - MINGW*) mingw=true;; - Darwin*) darwin=true - # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home - # See https://developer.apple.com/library/mac/qa/qa1170/_index.html - if [ -z "$JAVA_HOME" ]; then - if [ -x "/usr/libexec/java_home" ]; then - JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME - else - JAVA_HOME="/Library/Java/Home"; export JAVA_HOME - fi - fi - ;; +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; esac -if [ -z "$JAVA_HOME" ] ; then - if [ -r /etc/gentoo-release ] ; then - JAVA_HOME=$(java-config --jre-home) - fi -fi - -# For Cygwin, ensure paths are in UNIX format before anything is touched -if $cygwin ; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --unix "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --unix "$CLASSPATH") -fi - -# For Mingw, ensure paths are in UNIX format before anything is touched -if $mingw ; then - [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && - JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" -fi - -if [ -z "$JAVA_HOME" ]; then - javaExecutable="$(which javac)" - if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then - # readlink(1) is not available as standard on Solaris 10. - readLink=$(which readlink) - if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then - if $darwin ; then - javaHome="$(dirname "\"$javaExecutable\"")" - javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" - else - javaExecutable="$(readlink -f "\"$javaExecutable\"")" - fi - javaHome="$(dirname "\"$javaExecutable\"")" - javaHome=$(expr "$javaHome" : '\(.*\)/bin') - JAVA_HOME="$javaHome" - export JAVA_HOME - fi - fi -fi - -if [ -z "$JAVACMD" ] ; then - if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" else JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi fi else - JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" - fi -fi - -if [ ! -x "$JAVACMD" ] ; then - echo "Error: JAVA_HOME is not defined correctly." >&2 - echo " We cannot execute $JAVACMD" >&2 - exit 1 -fi + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : -if [ -z "$JAVA_HOME" ] ; then - echo "Warning: JAVA_HOME environment variable is not set." -fi - -# traverses directory structure from process work directory to filesystem root -# first directory with .mvn subdirectory is considered project base directory -find_maven_basedir() { - if [ -z "$1" ] - then - echo "Path not specified to find_maven_basedir" - return 1 + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi fi +} - basedir="$1" - wdir="$1" - while [ "$wdir" != '/' ] ; do - if [ -d "$wdir"/.mvn ] ; then - basedir=$wdir - break - fi - # workaround for JBEAP-8937 (on Solaris 10/Sparc) - if [ -d "${wdir}" ]; then - wdir=$(cd "$wdir/.." || exit 1; pwd) - fi - # end of workaround +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" done - printf '%s' "$(cd "$basedir" || exit 1; pwd)" + printf %x\\n $h } -# concatenates all lines of a file -concat_lines() { - if [ -f "$1" ]; then - # Remove \r in case we run on Windows within Git Bash - # and check out the repository with auto CRLF management - # enabled. Otherwise, we may read lines that are delimited with - # \r\n and produce $'-Xarg\r' rather than -Xarg due to word - # splitting rules. - tr -s '\r\n' ' ' < "$1" - fi +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 } -log() { - if [ "$MVNW_VERBOSE" = true ]; then - printf '%s\n' "$1" - fi +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" } -BASE_DIR=$(find_maven_basedir "$(dirname "$0")") -if [ -z "$BASE_DIR" ]; then - exit 1; +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" fi -MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR -log "$MAVEN_PROJECTBASEDIR" +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac -########################################################################################## -# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -# This allows using the maven wrapper in projects that prohibit checking in binary data. -########################################################################################## -wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" -if [ -r "$wrapperJarPath" ]; then - log "Found $wrapperJarPath" +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT else - log "Couldn't find $wrapperJarPath, downloading it ..." + die "cannot create temp dir" +fi - if [ -n "$MVNW_REPOURL" ]; then - wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - else - wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - fi - while IFS="=" read -r key value; do - # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) - safeValue=$(echo "$value" | tr -d '\r') - case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; - esac - done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" - log "Downloading from: $wrapperUrl" +mkdir -p -- "${MAVEN_HOME%/*}" - if $cygwin; then - wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") - fi +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" - if command -v wget > /dev/null; then - log "Found wget ... using wget" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - else - wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" - fi - elif command -v curl > /dev/null; then - log "Found curl ... using curl" - [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" - if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then - curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - else - curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" - fi - else - log "Falling back to using Java to download" - javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" - javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" - # For Cygwin, switch paths to Windows format before running javac - if $cygwin; then - javaSource=$(cygpath --path --windows "$javaSource") - javaClass=$(cygpath --path --windows "$javaClass") - fi - if [ -e "$javaSource" ]; then - if [ ! -e "$javaClass" ]; then - log " - Compiling MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/javac" "$javaSource") - fi - if [ -e "$javaClass" ]; then - log " - Running MavenWrapperDownloader.java ..." - ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" - fi - fi - fi +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" fi -########################################################################################## -# End of extension -########################################################################################## -# If specified, validate the SHA-256 sum of the Maven wrapper jar file -wrapperSha256Sum="" -while IFS="=" read -r key value; do - case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; - esac -done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" -if [ -n "$wrapperSha256Sum" ]; then - wrapperSha256Result=false - if command -v sha256sum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then - wrapperSha256Result=true +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true fi - elif command -v shasum > /dev/null; then - if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then - wrapperSha256Result=true + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true fi else - echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." - echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 exit 1 fi - if [ $wrapperSha256Result = false ]; then - echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 - echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 - echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 exit 1 fi fi -MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" - -# For Cygwin, switch paths to Windows format before running java -if $cygwin; then - [ -n "$JAVA_HOME" ] && - JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") - [ -n "$CLASSPATH" ] && - CLASSPATH=$(cygpath --path --windows "$CLASSPATH") - [ -n "$MAVEN_PROJECTBASEDIR" ] && - MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" -# Provide a "standardized" way to retrieve the CLI args that will -# work with both Windows and non-Windows executions. -MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" -export MAVEN_CMD_LINE_ARGS - -WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -# shellcheck disable=SC2086 # safe args -exec "$JAVACMD" \ - $MAVEN_OPTS \ - $MAVEN_DEBUG_OPTS \ - -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ - "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ - ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd index 93880f5d9..b150b91ed 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -1,3 +1,4 @@ +<# : batch portion @REM ---------------------------------------------------------------------------- @REM Licensed to the Apache Software Foundation (ASF) under one @REM or more contributor license agreements. See the NOTICE file @@ -7,7 +8,7 @@ @REM "License"); you may not use this file except in compliance @REM with the License. You may obtain a copy of the License at @REM -@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM http://www.apache.org/licenses/LICENSE-2.0 @REM @REM Unless required by applicable law or agreed to in writing, @REM software distributed under the License is distributed on an @@ -18,188 +19,131 @@ @REM ---------------------------------------------------------------------------- @REM ---------------------------------------------------------------------------- -@REM Apache Maven Wrapper startup batch script, version 3.2.0 -@REM -@REM Required ENV vars: -@REM JAVA_HOME - location of a JDK home dir +@REM Apache Maven Wrapper startup batch script, version 3.3.2 @REM @REM Optional ENV vars -@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands -@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending -@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven -@REM e.g. to debug Maven itself, use -@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 -@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output @REM ---------------------------------------------------------------------------- -@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' -@echo off -@REM set title of command window -title %0 -@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' -@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% - -@REM set %HOME% to equivalent of $HOME -if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") - -@REM Execute a user defined script before this one -if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre -@REM check for pre script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* -if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* -:skipRcPre - -@setlocal - -set ERROR_CODE=0 - -@REM To isolate internal variables from possible post scripts, we use another setlocal -@setlocal - -@REM ==== START VALIDATION ==== -if not "%JAVA_HOME%" == "" goto OkJHome - -echo. -echo Error: JAVA_HOME not found in your environment. >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -:OkJHome -if exist "%JAVA_HOME%\bin\java.exe" goto init - -echo. -echo Error: JAVA_HOME is set to an invalid directory. >&2 -echo JAVA_HOME = "%JAVA_HOME%" >&2 -echo Please set the JAVA_HOME variable in your environment to match the >&2 -echo location of your Java installation. >&2 -echo. -goto error - -@REM ==== END VALIDATION ==== - -:init - -@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". -@REM Fallback to current working directory if not found. - -set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% -IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir - -set EXEC_DIR=%CD% -set WDIR=%EXEC_DIR% -:findBaseDir -IF EXIST "%WDIR%"\.mvn goto baseDirFound -cd .. -IF "%WDIR%"=="%CD%" goto baseDirNotFound -set WDIR=%CD% -goto findBaseDir - -:baseDirFound -set MAVEN_PROJECTBASEDIR=%WDIR% -cd "%EXEC_DIR%" -goto endDetectBaseDir - -:baseDirNotFound -set MAVEN_PROJECTBASEDIR=%EXEC_DIR% -cd "%EXEC_DIR%" - -:endDetectBaseDir - -IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig - -@setlocal EnableExtensions EnableDelayedExpansion -for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a -@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% - -:endReadAdditionalConfig - -SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" -set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" -set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain - -set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B -) - -@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central -@REM This allows using the maven wrapper in projects that prohibit checking in binary data. -if exist %WRAPPER_JAR% ( - if "%MVNW_VERBOSE%" == "true" ( - echo Found %WRAPPER_JAR% - ) -) else ( - if not "%MVNW_REPOURL%" == "" ( - SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" - ) - if "%MVNW_VERBOSE%" == "true" ( - echo Couldn't find %WRAPPER_JAR%, downloading it ... - echo Downloading from: %WRAPPER_URL% - ) - - powershell -Command "&{"^ - "$webclient = new-object System.Net.WebClient;"^ - "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ - "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ - "}"^ - "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ - "}" - if "%MVNW_VERBOSE%" == "true" ( - echo Finished downloading %WRAPPER_JAR% - ) -) -@REM End of extension - -@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file -SET WRAPPER_SHA_256_SUM="" -FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( - IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) ) -IF NOT %WRAPPER_SHA_256_SUM%=="" ( - powershell -Command "&{"^ - "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ - "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ - " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ - " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ - " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ - " exit 1;"^ - "}"^ - "}" - if ERRORLEVEL 1 goto error -) - -@REM Provide a "standardized" way to retrieve the CLI args that will -@REM work with both Windows and non-Windows executions. -set MAVEN_CMD_LINE_ARGS=%* - -%MAVEN_JAVA_EXE% ^ - %JVM_CONFIG_MAVEN_PROPS% ^ - %MAVEN_OPTS% ^ - %MAVEN_DEBUG_OPTS% ^ - -classpath %WRAPPER_JAR% ^ - "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ - %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* -if ERRORLEVEL 1 goto error -goto end - -:error -set ERROR_CODE=1 - -:end -@endlocal & set ERROR_CODE=%ERROR_CODE% - -if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost -@REM check for post script, once with legacy .bat ending and once with .cmd ending -if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" -if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" -:skipRcPost - -@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' -if "%MAVEN_BATCH_PAUSE%"=="on" pause - -if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% - -cmd /C exit /B %ERROR_CODE% +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" From 5425fbb256e6534dc4d156c91cfe4e84024186d9 Mon Sep 17 00:00:00 2001 From: jchrys Date: Wed, 19 Feb 2025 23:22:35 +0900 Subject: [PATCH 80/93] Ensure Reactor-Netty Forward Compatibility Motivation: A change in `r.n.t.SslProvider.ProtocolSslContextSpec` breaks forward compatibility. Modifications: Remove Usage of `ProtocolSslContextSpec`. Result: The SSL context is now correctly initialized under the old reactor-netty version, ensuring forward compatibility. --- .../r2dbc/mysql/client/SslBridgeHandler.java | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java index ce3361fd4..22038bb43 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/SslBridgeHandler.java @@ -33,6 +33,7 @@ import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; +import reactor.core.Exceptions; import reactor.netty.tcp.SslProvider; import javax.net.ssl.HostnameVerifier; @@ -40,7 +41,6 @@ import javax.net.ssl.SSLException; import java.io.File; import java.net.InetSocketAddress; -import java.util.function.Consumer; import static io.asyncer.r2dbc.mysql.internal.util.AssertUtils.requireNonNull; import static io.netty.handler.ssl.SslProvider.JDK; @@ -151,10 +151,16 @@ private void handleSslState(ChannelHandlerContext ctx, SslState state) { switch (state) { case BRIDGING: logger.debug("SSL event triggered, enable SSL handler to pipeline"); - - SslProvider sslProvider = SslProvider.builder() - .sslContext(MySqlSslContextSpec.forClient(ssl, context)) - .build(); + final SslProvider sslProvider; + try { + // Workaround for a forward incompatible change in reactor-netty version 1.2.0 + // See: https://github.com/reactor/reactor-netty/commit/6d0c24d83a7c5b15e403475272293f847415191c + sslProvider = SslProvider.builder() + .sslContext(MySqlSslContextSpec.forClient(ssl, context).sslContext()) + .build(); + } catch (SSLException e) { + throw Exceptions.propagate(e); + } SslHandler sslHandler = sslProvider.getSslContext().newHandler(ctx.alloc()); this.sslEngine = sslHandler.engine(); @@ -195,7 +201,7 @@ private static boolean isTls13Enabled(ConnectionContext context) { || (version.isGreaterThanOrEqualTo(MYSQL_5_6_0) && version.isEnterprise()); } - private static final class MySqlSslContextSpec implements SslProvider.ProtocolSslContextSpec { + private static final class MySqlSslContextSpec { private final SslContextBuilder builder; @@ -203,16 +209,6 @@ private MySqlSslContextSpec(SslContextBuilder builder) { this.builder = builder; } - @Override - public MySqlSslContextSpec configure(Consumer customizer) { - requireNonNull(customizer, "customizer must not be null"); - - customizer.accept(builder); - - return this; - } - - @Override public SslContext sslContext() throws SSLException { return builder.build(); } From 33b258aa4f6938bf32b31f6e5abffc3b51186d1e Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 20 Feb 2025 03:45:51 +0900 Subject: [PATCH 81/93] Fix handling of `TINYINT(1) UNSIGNED` when `tinyInt1isBit` is set Motivation: When the `tinyInt1isBit` flag is set, attempting to convert `TINYINT(1) UNSIGNED` to a boolean results in immediate rejection. Modifications: Prevent conversion of `TINYINT(1) UNSIGNED` to boolean when `tinyInt1isBit` is enabled. Result: `TINYINT(1) UNSIGNED` is handled correctly without unnecessary conversion attempts. --- .../mysql/MySqlConnectionConfiguration.java | 6 +++-- .../mysql/MySqlConnectionFactoryProvider.java | 6 +++-- .../r2dbc/mysql/codec/BooleanCodec.java | 4 +++- .../r2dbc/mysql/codec/DefaultCodecs.java | 22 ++++++++++--------- .../mysql/ConnectionIntegrationTest.java | 13 +++++++++++ 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 93ba9ce78..3857451a1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -1220,8 +1220,10 @@ public Builder metrics(boolean enabled) { /** * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type. - * When enabled, TINYINT(1) columns (both SIGNED and UNSIGNED) will be treated as - * BIT. default to {@code true}. + * 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} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java index bff335123..5905c56ca 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionFactoryProvider.java @@ -332,8 +332,10 @@ public final class MySqlConnectionFactoryProvider implements ConnectionFactoryPr /** * Option to whether the driver should interpret MySQL's TINYINT(1) as a BIT type. - * When enabled, TINYINT(1) columns (both SIGNED and UNSIGNED) will be treated as - * BIT. default to {@code true}. + * 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 */ diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java index 0a59265c3..8fb98c273 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/BooleanCodec.java @@ -32,6 +32,8 @@ */ final class BooleanCodec extends AbstractPrimitiveCodec { + private static final Integer INTEGER_ONE = Integer.valueOf(1); + static final BooleanCodec INSTANCE = new BooleanCodec(); private BooleanCodec() { @@ -86,7 +88,7 @@ public MySqlParameter encode(Object value, CodecContext context) { public boolean doCanDecode(MySqlReadableMetadata metadata) { MySqlType type = metadata.getType(); return ((type == MySqlType.BIT || type == MySqlType.TINYINT) && - Integer.valueOf(1).equals(metadata.getPrecision())) || type == MySqlType.VARCHAR; + INTEGER_ONE.equals(metadata.getPrecision())) || type == MySqlType.VARCHAR; } public Boolean createFromLong(long l) { diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index ba022875a..34f2c67c1 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -45,8 +45,6 @@ */ final class DefaultCodecs implements Codecs { - private static final Integer INTEGER_ONE = Integer.valueOf(1); - private static final List> DEFAULT_CODECS = InternalArrays.asImmutableList( ByteCodec.INSTANCE, ShortCodec.INSTANCE, @@ -369,18 +367,22 @@ private static Class chooseClass(final MySqlReadableMetadata metadata, Class< return type.isAssignableFrom(javaType) ? javaType : type; } - private static Class getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) { - final MySqlType type = metadata.getType(); - final Integer precision = metadata.getPrecision(); - if (INTEGER_ONE.equals(precision) && (type == MySqlType.TINYINT || type == MySqlType.TINYINT_UNSIGNED) - && codecContext.isTinyInt1isBit()) { - return Boolean.class; + private static boolean shouldBeTreatedAsBoolean(final @Nullable Integer precision, final MySqlType type, + final CodecContext context) { + if (precision == null || precision != 1) { + return false; } - // ref: https://github.com/asyncer-io/r2dbc-mysql/issues/277 // BIT(1) should be treated as Boolean by default. - if (INTEGER_ONE.equals(precision) && type == MySqlType.BIT) { + return type == MySqlType.BIT || type == MySqlType.TINYINT && context.isTinyInt1isBit(); + } + + private static Class getDefaultJavaType(final MySqlReadableMetadata metadata, final CodecContext codecContext) { + final MySqlType type = metadata.getType(); + final Integer precision = metadata.getPrecision(); + + if (shouldBeTreatedAsBoolean(precision, type, codecContext)) { return Boolean.class; } diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java index fccd38530..4e4fa34ae 100644 --- a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java @@ -592,6 +592,19 @@ public void tinyInt1isBitTrueTestValue1() { ); } + @Test + public void tinyInt1isBitTrueTestUnsignedTinyInt1isNotBoolean() { + complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1) UNSIGNED)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("INSERT INTO `test` VALUES (1, 1)").execute()) + .flatMap(IntegrationTestSupport::extractRowsUpdated) + .thenMany(connection.createStatement("SELECT `value` FROM `test`").execute()) + .flatMap(result -> result.map((row, metadata) -> row.get("value", Object.class))) + .doOnNext(value -> assertThat(value).isInstanceOf(Short.class)) + .doOnNext(value -> assertThat(value).isEqualTo(Short.valueOf((short)1))) + ); + } + @Test public void tinyInt1isBitTrueTestValue0() { complete(connection -> Mono.from(connection.createStatement("CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` TINYINT(1))").execute()) From cceb8bc3e937842f7958982f23bdb39cdd1cbc48 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Thu, 20 Feb 2025 11:10:28 +0000 Subject: [PATCH 82/93] [maven-release-plugin] prepare release r2dbc-mysql-1.4.0 --- r2dbc-mysql/pom.xml | 4 ++-- test-native-image/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 21e6e9356..90e184254 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.4.0-SNAPSHOT + 1.4.0 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.4.0 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index 3ea1602d0..e260321f2 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.4.0-SNAPSHOT + 1.4.1-SNAPSHOT UTF-8 From 9633b71b7103fd331cf4e62bb87e881f3388ef76 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Thu, 20 Feb 2025 11:10:31 +0000 Subject: [PATCH 83/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 90e184254..904e49fb5 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.4.0 + 1.4.1-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.4.0 + HEAD From d67bf119d03186a9924d82f6252a278325e14600 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 20 Feb 2025 20:39:29 +0900 Subject: [PATCH 84/93] Update README Motivation: Released 1.4.0 Modifications: Released 1.4.0 Result: up-to-date --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2a9e75b77..85039510a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Refer to the table below to determine the appropriate version of r2dbc-mysql for | spring-boot-starter-data-r2dbc | spring-data-r2dbc | r2dbc-spi | r2dbc-mysql(recommended) | |--------------------------------|-------------------|---------------|------------------------------| -| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.3.1 | +| 3.0.* and above | 3.0.* and above | 1.0.0.RELEASE | io.asyncer:r2dbc-mysql:1.4.0 | | 2.7.* | 1.5.* | 0.9.1.RELEASE | io.asyncer:r2dbc-mysql:0.9.7 | | 2.6.* and below | 1.4.* and below | 0.8.6.RELEASE | dev.miku:r2dbc-mysql:0.8.2 | @@ -64,7 +64,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so io.asyncer r2dbc-mysql - 1.3.1 + 1.4.0 ``` @@ -74,7 +74,7 @@ However, Docker-certified images do not have these versions lower than 5.5.0, so ```groovy dependencies { - implementation 'io.asyncer:r2dbc-mysql:1.3.1' + implementation 'io.asyncer:r2dbc-mysql:1.4.0' } ``` @@ -83,7 +83,7 @@ dependencies { ```kotlin dependencies { // Maybe should to use `compile` instead of `implementation` on the lower version of Gradle. - implementation("io.asyncer:r2dbc-mysql:1.3.1") + implementation("io.asyncer:r2dbc-mysql:1.4.0") } ``` From 3e76ca79e7ff67de2f76f7437bb13991fc5c7b01 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 20 Feb 2025 21:04:23 +0900 Subject: [PATCH 85/93] Fix Typo (#310) Motiviation: The `MysqlConnectionConfiguration#toString` has a typo: `tinyint1isBit`. Modifications: Corrected the typo to `tinyInt1isBit`. Result: Fixed. --- .../io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java index 3857451a1..39fb91eb6 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/MySqlConnectionConfiguration.java @@ -418,7 +418,7 @@ private String buildCommonToStringPart() { ", passwordPublisher=" + passwordPublisher + ", resolver=" + resolver + ", metrics=" + metrics + - ", tinyint1isBit=" + tinyInt1isBit; + ", tinyInt1isBit=" + tinyInt1isBit; } /** From b90a453a93831d59ca6c0aaf75b52f0871118506 Mon Sep 17 00:00:00 2001 From: jchrys Date: Mon, 24 Mar 2025 00:50:01 +0900 Subject: [PATCH 86/93] fix(logging): prevent excessive MySQL warning logs (#312) Lower MySQL warning log level from INFO to DEBUG to reduce log spam while maintaining debugging visibility. Closes #311 --- .../r2dbc/mysql/client/ReactorNettyClient.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java index 8e926f21e..5054f3631 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/client/ReactorNettyClient.java @@ -379,17 +379,17 @@ public void error(Throwable e) { @Override public void next(ServerMessage message) { - if (message instanceof WarningMessage) { - int warnings = ((WarningMessage) message).getWarnings(); - if (warnings == 0) { - if (DEBUG_ENABLED) { + if (DEBUG_ENABLED) { + if (message instanceof WarningMessage) { + final int warnings = ((WarningMessage) message).getWarnings(); + if (warnings == 0) { logger.debug("Response: {}", message); + } else { + logger.debug("Response: {}, reports {} warning(s)", message, warnings); } - } else if (INFO_ENABLED) { - logger.info("Response: {}, reports {} warning(s)", message, warnings); + } else { + logger.debug("Response: {}", message); } - } else if (DEBUG_ENABLED) { - logger.debug("Response: {}", message); } responseProcessor.emitNext(message, EmitFailureHandler.FAIL_FAST); From 880226903522dee664ac72f6b0d00a7f72eb424e Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Mon, 14 Apr 2025 13:36:00 +0000 Subject: [PATCH 87/93] [maven-release-plugin] prepare release r2dbc-mysql-1.4.1 --- r2dbc-mysql/pom.xml | 4 ++-- test-native-image/pom.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 904e49fb5..4d4b5f4e1 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.4.1-SNAPSHOT + 1.4.1 Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - HEAD + r2dbc-mysql-1.4.1 diff --git a/test-native-image/pom.xml b/test-native-image/pom.xml index e260321f2..36b13f6b0 100644 --- a/test-native-image/pom.xml +++ b/test-native-image/pom.xml @@ -5,7 +5,7 @@ 4.0.0 io.asyncer test-native-image - 1.4.1-SNAPSHOT + 1.4.2-SNAPSHOT UTF-8 From bae7e03a40d95a4f2d2a5b91808bd649475bd988 Mon Sep 17 00:00:00 2001 From: asyncer-io-bot Date: Mon, 14 Apr 2025 13:36:02 +0000 Subject: [PATCH 88/93] [maven-release-plugin] prepare for next development iteration --- r2dbc-mysql/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index 4d4b5f4e1..f931fddff 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -19,7 +19,7 @@ io.asyncer r2dbc-mysql - 1.4.1 + 1.4.2-SNAPSHOT Reactive Relational Database Connectivity - MySQL https://github.com/asyncer-io/r2dbc-mysql @@ -61,7 +61,7 @@ scm:git:git://github.com/asyncer-io/r2dbc-mysql.git scm:git:ssh://git@github.com/asyncer-io/r2dbc-mysql.git https://github.com/asyncer-io/r2dbc-mysql - r2dbc-mysql-1.4.1 + HEAD From d8e8f8ca464a55460f25245dbc73a7148b17cf06 Mon Sep 17 00:00:00 2001 From: jchrys Date: Sun, 18 May 2025 18:45:16 +0900 Subject: [PATCH 89/93] fix(test): Stringify versions to avoid incorrect Docker tag resolution (#318) motivation: Numeric version like 5.0 resolve to '5', which pulls the latest 5.x version. This caused tests to break due to version mismatches. modifications: Stringify versions results: TestContainer now pulls the correct MySQL image, and tests run reliably. --- .github/workflows/ci-integration-tests.yml | 2 +- .github/workflows/ci-mariadb-intergration-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-integration-tests.yml b/.github/workflows/ci-integration-tests.yml index 1ef3cdb0e..3ac7fb159 100644 --- a/.github/workflows/ci-integration-tests.yml +++ b/.github/workflows/ci-integration-tests.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: mysql-version: - [5.5, 5.6.45, 5.6, 5.7.28, 5.7, 8.0, 8.1, 8.2, 8.3, 8.4, 9.0] + ['5.5', '5.6.45', '5.6', '5.7.28', '5.7', '8.0', '8.1', '8.2', '8.3', '8.4', '9.0', '9.1', '9.2'] name: Integration test with MySQL ${{ matrix.mysql-version }} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/ci-mariadb-intergration-tests.yml b/.github/workflows/ci-mariadb-intergration-tests.yml index 25cf0e498..fcf38e35c 100644 --- a/.github/workflows/ci-mariadb-intergration-tests.yml +++ b/.github/workflows/ci-mariadb-intergration-tests.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: mariadb-version: - [10.0, 10.1, 10.2.15, 10.2, 10.3.7, 10.3, 10.5.1, 10.5, 10.6, 10.11] + ['10.0', '10.1', '10.2.15', '10.2', '10.3.7', '10.3', '10.5.1', '10.5', '10.6', '10.11'] name: Integration test with MariaDB ${{ matrix.mariadb-version }} steps: - uses: actions/checkout@v3 From 64ad31a7c5813986acca17f473f79d4ad84db0e4 Mon Sep 17 00:00:00 2001 From: jchrys Date: Sun, 18 May 2025 18:59:23 +0900 Subject: [PATCH 90/93] chore(deps): update testcontainer ver (#317) --- r2dbc-mysql/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index f931fddff..a1b77998b 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -82,7 +82,7 @@ 4.11.0 8.3.0 3.3.3 - 1.19.7 + 1.21.0 4.0.3 5.3.32 2.16.1 From aa27a08708c76f69f86b2b46c6c6aa2fc5ce0430 Mon Sep 17 00:00:00 2001 From: jchrys Date: Thu, 12 Jun 2025 02:29:49 +0900 Subject: [PATCH 91/93] chore(deploy): Migrate from OSSRH to Central Publisher Portal (#323) Motivation: The OSSRH will reach EOL on June 30th, 2025. To continue publishing to Maven Central, migration to the Central Publisher Portal is required. Modifications: Migrated the publishing configuration from OSSRH to the Central Publisher Portal. Results: Ensured uinterrupted deployments post-OSSRH EOL. Signed-off-by: jchrys --- .github/workflows/cd-release.yml | 6 +++--- .github/workflows/cd-snapshot.yml | 6 +++--- r2dbc-mysql/pom.xml | 23 ++++++++++++----------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/workflows/cd-release.yml b/.github/workflows/cd-release.yml index a57a2e58e..75b75ccde 100644 --- a/.github/workflows/cd-release.yml +++ b/.github/workflows/cd-release.yml @@ -118,9 +118,9 @@ jobs: with: servers: | [{ - "id": "ossrh-staging", - "username": "${{ secrets.OSSRH_USERNAME }}", - "password": "${{ secrets.OSSRH_PASSWORD }}" + "id": "central", + "username": "${{ secrets.CENTRAL_USERNAME }}", + "password": "${{ secrets.CENTRAL_PASSWORD }}" }] - name: Create Local Deploy Directory diff --git a/.github/workflows/cd-snapshot.yml b/.github/workflows/cd-snapshot.yml index a04976b28..8cd96d40c 100644 --- a/.github/workflows/cd-snapshot.yml +++ b/.github/workflows/cd-snapshot.yml @@ -46,9 +46,9 @@ jobs: with: servers: | [{ - "id": "ossrh-snapshots", - "username": "${{ secrets.OSSRH_USERNAME }}", - "password": "${{ secrets.OSSRH_PASSWORD }}" + "id": "central-portal-snapshots", + "username": "${{ secrets.CENTRAL_USERNAME }}", + "password": "${{ secrets.CENTRAL_PASSWORD }}" }] - name: Prepare Internal Dependencies diff --git a/r2dbc-mysql/pom.xml b/r2dbc-mysql/pom.xml index a1b77998b..a903116a8 100644 --- a/r2dbc-mysql/pom.xml +++ b/r2dbc-mysql/pom.xml @@ -448,6 +448,14 @@ + + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 + + central + + @@ -558,16 +566,9 @@ - - - false - - - true - - ossrh-snapshots - Sonatype Nexus Snapshots - https://s01.oss.sonatype.org/content/repositories/snapshots/ - + + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + From 64922945a1aaa1abc58fbeea0fd21c4d5da68ea9 Mon Sep 17 00:00:00 2001 From: dongko Date: Sat, 17 May 2025 16:57:07 +0900 Subject: [PATCH 92/93] Implement ByteArrayInputStreamCodec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fixed canEncode, doCanDecode conditions - Update Copyright Year - Add tests for ByteArrayInputStreamCodec - Implement ByteArrayInputStreamCodec Signed-off-by: dongKos Signed-off-by: 김동환 --- .../codec/ByteArrayInputStreamCodec.java | 153 ++++++++++++++++++ .../r2dbc/mysql/codec/DefaultCodecs.java | 3 +- .../codec/ByteArrayInputStreamCodecTest.java | 68 ++++++++ 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java create mode 100644 r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodecTest.java diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java new file mode 100644 index 000000000..3af23a3bc --- /dev/null +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java @@ -0,0 +1,153 @@ +/* + * Copyright 2025 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.codec; + +import io.asyncer.r2dbc.mysql.MySqlParameter; +import io.asyncer.r2dbc.mysql.ParameterWriter; +import io.asyncer.r2dbc.mysql.api.MySqlReadableMetadata; +import io.asyncer.r2dbc.mysql.constant.MySqlType; +import io.asyncer.r2dbc.mysql.internal.util.VarIntUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import static io.asyncer.r2dbc.mysql.internal.util.InternalArrays.EMPTY_BYTES; + +/** + * Codec for {@link InputStream}. + */ +final class ByteArrayInputStreamCodec extends AbstractClassedCodec { + + static final ByteArrayInputStreamCodec INSTANCE = new ByteArrayInputStreamCodec(); + + private ByteArrayInputStreamCodec() { + super(ByteArrayInputStream.class); + } + + @Override + public ByteArrayInputStream decode(ByteBuf value, MySqlReadableMetadata metadata, Class target, boolean binary, + CodecContext context) { + if (!value.isReadable()) { + return new ByteArrayInputStream(EMPTY_BYTES); + } + return new ByteArrayInputStream(value.array()); + } + + @Override + protected boolean doCanDecode(MySqlReadableMetadata metadata) { + return metadata.getType().isBinary(); + } + + @Override + public boolean canEncode(Object value) { + return value instanceof ByteArrayInputStream; + } + + @Override + public MySqlParameter encode(Object value, CodecContext context) { + return new ByteArrayInputStreamMysqlParameter((ByteArrayInputStream) value); + } + + private static final class ByteArrayInputStreamMysqlParameter extends AbstractMySqlParameter { + + private final ByteArrayInputStream value; + + private ByteArrayInputStreamMysqlParameter(ByteArrayInputStream value) { + this.value = value; + } + + @Override + public Mono publishBinary(ByteBufAllocator allocator) { + return Mono.fromSupplier(() -> { + int size = value.available(); + if (size == 0) { + return allocator.buffer(Byte.BYTES).writeByte(0); + } + + int addedSize = VarIntUtils.varIntBytes(size); + ByteBuf buf = allocator.buffer(addedSize + size); + + try { + VarIntUtils.writeVarInt(buf, size); + + byte[] byteArray = new byte[size]; + int readBytes = value.read(byteArray); + + if (readBytes != size) { + buf.release(); + throw new IllegalStateException("Expected to read " + size + " bytes, but got " + readBytes); + } + + return buf.writeBytes(byteArray); + } catch (Exception e) { + buf.release(); + throw new RuntimeException(e); + } + }); + } + + @Override + public Mono publishText(ParameterWriter writer) { + return Mono.fromRunnable(() -> { + try { + int size = value.available(); + byte[] byteArray = new byte[size]; + int readBytes = value.read(byteArray); + + if (size != 0 && readBytes != size) { + throw new IllegalStateException("Expected to read " + size + " bytes, but got " + readBytes); + } + + writer.writeHex(byteArray); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Override + public String toString() { + return value.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof ByteArrayInputStreamMysqlParameter)) { + return false; + } + + ByteArrayInputStreamMysqlParameter that = (ByteArrayInputStreamMysqlParameter) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + + @Override + public MySqlType getType() { + return MySqlType.VARBINARY; + } + } +} diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java index 34f2c67c1..01e47348c 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/DefaultCodecs.java @@ -81,7 +81,8 @@ final class DefaultCodecs implements Codecs { BlobCodec.INSTANCE, ByteBufferCodec.INSTANCE, - ByteArrayCodec.INSTANCE + ByteArrayCodec.INSTANCE, + ByteArrayInputStreamCodec.INSTANCE ); private final List> codecs; diff --git a/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodecTest.java b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodecTest.java new file mode 100644 index 000000000..8c7e8b847 --- /dev/null +++ b/r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodecTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 asyncer.io projects + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.asyncer.r2dbc.mysql.codec; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import org.testcontainers.shaded.org.bouncycastle.util.encoders.Hex; + +import java.io.ByteArrayInputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Unit tests for {@link ByteArrayInputStreamCodec}. + */ +public class ByteArrayInputStreamCodecTest implements CodecTestSupport { + + private final byte[][] rawData = { + new byte[0], + new byte[] { 0x7F }, + new byte[] { 0x12, 34, 0x56, 78, (byte) 0x9A }, + "Hello world!".getBytes(StandardCharsets.US_ASCII), + new byte[] { (byte) 0xFE, (byte) 0xDC, (byte) 0xBA }, + }; + + private final ByteArrayInputStream[] data = Arrays.stream(rawData) + .map(ByteArrayInputStream::new) + .toArray(ByteArrayInputStream[]::new); + + @Override + public Codec getCodec() { + return ByteArrayInputStreamCodec.INSTANCE; + } + + @Override + public ByteArrayInputStream[] originParameters() { + return data; + } + + @Override + public Object[] stringifyParameters() { + return Arrays.stream(rawData) + .map(bytes -> String.format("x'%s'", Hex.toHexString(bytes))) + .toArray(); + } + + @Override + public ByteBuf[] binaryParameters(Charset charset) { + return Arrays.stream(rawData) + .map(Unpooled::wrappedBuffer) + .toArray(ByteBuf[]::new); + } +} From 8b47ca535fed72c29f9d83ac3feee1221530998d Mon Sep 17 00:00:00 2001 From: jchrys Date: Sat, 28 Jun 2025 22:17:25 +0900 Subject: [PATCH 93/93] =?UTF-8?q?refactor(codec):=20remove=20intermediate?= =?UTF-8?q?=20byte=20array=20in=20ByteArrayInputStre=E2=80=A6=20(#324)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …amCodec Signed-off-by: jchrys --- .../r2dbc/mysql/codec/ByteArrayInputStreamCodec.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java index 3af23a3bc..c31261f5a 100644 --- a/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java +++ b/r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/codec/ByteArrayInputStreamCodec.java @@ -86,16 +86,13 @@ public Mono publishBinary(ByteBufAllocator allocator) { try { VarIntUtils.writeVarInt(buf, size); - - byte[] byteArray = new byte[size]; - int readBytes = value.read(byteArray); - + int readBytes = buf.writeBytes(value, size); if (readBytes != size) { buf.release(); throw new IllegalStateException("Expected to read " + size + " bytes, but got " + readBytes); } - return buf.writeBytes(byteArray); + return buf; } catch (Exception e) { buf.release(); throw new RuntimeException(e);