diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 642d572c..abd303b6 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1,2 +1,2 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/README.md b/README.md index 373fae49..bf015bab 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,16 @@ +# Why doesn't it seem so active + +Due to COVID-19 and my personal reasons, the progress of this project in 2021 is soooooo slow. + +If you ask me "Are u ok? how about u healthy?". Yes, I'm fine. In China, most areas are no longer infected +with COVID-19. It just...my work plan was severely hindered because of COVID-19. + +I trust this will not become a norm. *I'm trying to maintain this project as well as I can*. + +**May the dead rest, and hope the living be healthy** + +*Donation are not accepted because I'm subscribing JetBrains open source license, thanks.* + # Reactive Relational Database Connectivity MySQL Implementation [![Maven Central](https://img.shields.io/maven-central/v/dev.miku/r2dbc-mysql?color=green&label=Maven%20Central)](https://search.maven.org/search?q=g:%22dev.miku%22%20AND%20a:%22r2dbc-mysql%22) diff --git a/mvnw b/mvnw index 41c0f0c2..a16b5431 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 # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 diff --git a/mvnw.cmd b/mvnw.cmd index 86115719..c8d43372 100644 --- a/mvnw.cmd +++ b/mvnw.cmd @@ -7,7 +7,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 http://www.apache.org/licenses/LICENSE-2.0 +@REM https://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 diff --git a/pom.xml b/pom.xml index c9d5988d..c01f0a17 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ dev.miku r2dbc-mysql - 0.8.3.BUILD-SNAPSHOT + 0.9.0.BUILD-SNAPSHOT jar Reactive Relational Database Connectivity - MySQL @@ -56,26 +56,27 @@ - 3.18.1 - 1.8 - 0.2.0.RELEASE - 1.27 - 3.0.2 - 5.7.0 - 1.2.3 - 3.7.7 - 8.0.23 UTF-8 UTF-8 - 0.9.0.BUILD-SNAPSHOT - Dysprosium-SR16 - 1.7.30 - 1.15.1 - 3.4.5 - 5.3.3 - 2.12.1 + 1.8 true false + + 0.9.0.BUILD-SNAPSHOT + Dysprosium-SR22 + 3.20.2 + 1.33 + 5.7.2 + 1.2.5 + 3.12.3 + 8.0.26 + 1.7.32 + 1.16.0 + 4.0.3 + 5.3.9 + 2.12.4 + 0.2.0.RELEASE + 3.0.2 @@ -261,7 +262,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.2.0 + 3.3.0 dev.miku.r2dbc.mysql.authentication,dev.miku.r2dbc.mysql.client,dev.miku.r2dbc.mysql.util,dev.miku.r2dbc.mysql.codec.lob,dev.miku.r2dbc.mysql.message @@ -297,7 +298,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.0.1 verify @@ -472,5 +473,4 @@ - diff --git a/src/main/java/dev/miku/r2dbc/mysql/ColumnNameSet.java b/src/main/java/dev/miku/r2dbc/mysql/ColumnNameSet.java index 541f960d..a784fa8b 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/ColumnNameSet.java +++ b/src/main/java/dev/miku/r2dbc/mysql/ColumnNameSet.java @@ -18,14 +18,7 @@ import dev.miku.r2dbc.mysql.util.InternalArrays; -import java.util.AbstractSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.Objects; -import java.util.Set; -import java.util.Spliterator; -import java.util.Spliterators; +import java.util.*; import java.util.function.Consumer; import java.util.function.Predicate; @@ -40,6 +33,9 @@ */ 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; @@ -58,7 +54,7 @@ private ColumnNameSet(String[] originNames, String[] sortedNames) { @Override public boolean contains(Object o) { if (o instanceof String) { - return MySqlNames.nameSearch(this.sortedNames, (String) o) >= 0; + return findIndex((String) o) >= 0; } return false; @@ -186,6 +182,14 @@ 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"); diff --git a/src/main/java/dev/miku/r2dbc/mysql/ConnectionState.java b/src/main/java/dev/miku/r2dbc/mysql/ConnectionState.java index 7e80306e..a9f78120 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/ConnectionState.java +++ b/src/main/java/dev/miku/r2dbc/mysql/ConnectionState.java @@ -35,10 +35,10 @@ interface ConnectionState { * * @param timeoutSeconds seconds of current lock wait timeout. */ - void setLockWaitTimeout(long timeoutSeconds); + void setCurrentLockWaitTimeout(long timeoutSeconds); /** - * Checks if lock wait timeout has been changed by {@link #setLockWaitTimeout(long)}. + * Checks if lock wait timeout has been changed by {@link #setCurrentLockWaitTimeout(long)}. * * @return if lock wait timeout changed. */ @@ -52,7 +52,7 @@ interface ConnectionState { /** * Resets current isolation level in initial state. */ - void resetLockWaitTimeout(); + void resetCurrentLockWaitTimeout(); /** * Checks if connection is processing a transaction. diff --git a/src/main/java/dev/miku/r2dbc/mysql/ExceptionFactory.java b/src/main/java/dev/miku/r2dbc/mysql/ExceptionFactory.java deleted file mode 100644 index d2b67a98..00000000 --- a/src/main/java/dev/miku/r2dbc/mysql/ExceptionFactory.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2018-2021 the original author or authors. - * - * 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. - */ - -package dev.miku.r2dbc.mysql; - -import dev.miku.r2dbc.mysql.message.server.ErrorMessage; -import io.r2dbc.spi.R2dbcBadGrammarException; -import io.r2dbc.spi.R2dbcDataIntegrityViolationException; -import io.r2dbc.spi.R2dbcException; -import io.r2dbc.spi.R2dbcNonTransientResourceException; -import io.r2dbc.spi.R2dbcPermissionDeniedException; -import io.r2dbc.spi.R2dbcRollbackException; -import io.r2dbc.spi.R2dbcTimeoutException; -import io.r2dbc.spi.R2dbcTransientResourceException; -import reactor.util.annotation.Nullable; - -import static dev.miku.r2dbc.mysql.util.AssertUtils.requireNonNull; - -/** - * A factory for generate {@link R2dbcException}s. - */ -final class ExceptionFactory { - - private static final String CONSTRAINT_VIOLATION_PREFIX = "23"; - - private static final String TRANSACTION_ROLLBACK_PREFIX = "40"; - - private static final String SYNTAX_ERROR_PREFIX = "42"; - - static R2dbcException createException(ErrorMessage message, @Nullable String sql) { - requireNonNull(message, "error message must not be null"); - - int errorCode = message.getErrorCode(); - String sqlState = message.getSqlState(); - String errorMessage = message.getErrorMessage(); - - // Should keep looking more error codes - switch (errorCode) { - case 1044: // Database access denied - case 1045: // Wrong password - case 1095: // Kill thread denied - case 1142: // Table access denied - case 1143: // Column access denied - case 1227: // Operation has no privilege(s) - case 1370: // Routine or process access denied - case 1698: // User need password but has no password - case 1873: // Change user denied - return new R2dbcPermissionDeniedException(errorMessage, sqlState, errorCode); - case 1159: // Read interrupted, reading packet timeout because of network jitter in most cases - case 1161: // Write interrupted, writing packet timeout because of network jitter in most cases - case 1213: // Dead lock :-( no one wants this - case 1317: // Statement execution interrupted - return new R2dbcTransientResourceException(errorMessage, sqlState, errorCode); - case 1205: // Wait lock timeout - case 1907: // Statement executing timeout - return new R2dbcTimeoutException(errorMessage, sqlState, errorCode); - case 1613: // Transaction rollback because of took too long - return new R2dbcRollbackException(errorMessage, sqlState, errorCode); - case 1050: // Table already exists - case 1051: // Unknown table - case 1054: // Unknown column name in existing table - case 1064: // Bad syntax - case 1247: // Unsupported reference - case 1146: // Unknown table name - case 1304: // Something already exists, like savepoint - case 1305: // Something does not exists, like savepoint - case 1630: // Function not exists - return new R2dbcBadGrammarException(errorMessage, sqlState, errorCode, sql); - case 1022: // Duplicate key - case 1048: // Field cannot be null - case 1062: // Duplicate entry for key constraint - case 1169: // Violation of an unique constraint - case 1215: // Add a foreign key has a violation - case 1216: // Child row has a violation of foreign key constraint when inserting or updating - case 1217: // Parent row has a violation of foreign key constraint when deleting or updating - case 1364: // Field has no default value but user try set it to DEFAULT - case 1451: // Parent row has a violation of foreign key constraint when deleting or updating - case 1452: // Child row has a violation of foreign key constraint when inserting or updating - case 1557: // Conflicting foreign key constraints and unique constraints - case 1859: // Duplicate unknown entry for key constraint - return new R2dbcDataIntegrityViolationException(errorMessage, sqlState, errorCode); - } - - if (sqlState == null) { - // Has no SQL state, all exceptions mismatch, fallback. - return new R2dbcNonTransientResourceException(errorMessage, null, errorCode); - } - - return mappingSqlState(errorMessage, sqlState, errorCode, sql); - } - - private static R2dbcException mappingSqlState(String errorMessage, String sqlState, int errorCode, - @Nullable String sql) { - if (sqlState.startsWith(SYNTAX_ERROR_PREFIX)) { - return new R2dbcBadGrammarException(errorMessage, sqlState, errorCode, sql); - } else if (sqlState.startsWith(CONSTRAINT_VIOLATION_PREFIX)) { - return new R2dbcDataIntegrityViolationException(errorMessage, sqlState, errorCode); - } else if (sqlState.startsWith(TRANSACTION_ROLLBACK_PREFIX)) { - return new R2dbcRollbackException(errorMessage, sqlState, errorCode); - } - - // Uncertain SQL state, all exceptions mismatch, fallback. - return new R2dbcNonTransientResourceException(errorMessage, null, errorCode); - } -} diff --git a/src/main/java/dev/miku/r2dbc/mysql/InsertSyntheticRow.java b/src/main/java/dev/miku/r2dbc/mysql/InsertSyntheticRow.java index dd07cc92..2415075b 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/InsertSyntheticRow.java +++ b/src/main/java/dev/miku/r2dbc/mysql/InsertSyntheticRow.java @@ -89,6 +89,18 @@ public Number get(String name) { return get0(getType().getJavaType()); } + @Override + public boolean contains(String name) { + requireNonNull(name, "name must not be null"); + + return contains0(name); + } + + @Override + public RowMetadata getMetadata() { + return this; + } + @Override public ColumnMetadata getColumnMetadata(int index) { assertValidIndex(index); @@ -110,6 +122,7 @@ public List getColumnMetadatas() { } @Override + @SuppressWarnings("deprecation") public Set getColumnNames() { return nameSet; } @@ -135,11 +148,15 @@ public Nullability getNullability() { } private void assertValidName(String name) { - if (!nameSet.contains(name)) { + if (!contains0(name)) { throw new NoSuchElementException("Column name '" + name + "' does not exist in " + this.nameSet); } } + private boolean contains0(String name) { + return nameSet.contains(name); + } + private T get0(Class type) { return codecs.decodeLastInsertId(lastInsertId, type); } diff --git a/src/main/java/dev/miku/r2dbc/mysql/MySqlBatchingBatch.java b/src/main/java/dev/miku/r2dbc/mysql/MySqlBatchingBatch.java index e3c967ba..3611f53e 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/MySqlBatchingBatch.java +++ b/src/main/java/dev/miku/r2dbc/mysql/MySqlBatchingBatch.java @@ -61,7 +61,7 @@ public MySqlBatch add(String sql) { @Override public Flux execute() { return QueryFlow.execute(client, getSql()) - .map(messages -> new MySqlResult(false, codecs, context, null, messages)); + .map(messages -> MySqlResult.toResult(false, codecs, context, null, messages)); } @Override diff --git a/src/main/java/dev/miku/r2dbc/mysql/MySqlConnection.java b/src/main/java/dev/miku/r2dbc/mysql/MySqlConnection.java index 67ce356c..a07c828c 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/MySqlConnection.java +++ b/src/main/java/dev/miku/r2dbc/mysql/MySqlConnection.java @@ -38,6 +38,7 @@ import reactor.util.annotation.Nullable; import java.time.DateTimeException; +import java.time.Duration; import java.time.ZoneId; import java.time.ZoneOffset; import java.util.function.BiConsumer; @@ -108,8 +109,8 @@ public final class MySqlConnection implements Connection, ConnectionState { private static final BiConsumer> PING = (message, sink) -> { if (message instanceof ErrorMessage) { ErrorMessage msg = (ErrorMessage) message; - logger.debug("Remote validate failed: [{}] [{}] {}", msg.getErrorCode(), msg.getSqlState(), - msg.getErrorMessage()); + logger.debug("Remote validate failed: [{}] [{}] {}", msg.getCode(), msg.getSqlState(), + msg.getMessage()); sink.next(false); sink.complete(); } else if (message instanceof CompleteMessage && ((CompleteMessage) message).isDone()) { @@ -132,8 +133,6 @@ public final class MySqlConnection implements Connection, ConnectionState { private final IsolationLevel sessionLevel; - private final long lockWaitTimeout; - private final QueryCache queryCache; private final PrepareCache prepareCache; @@ -153,6 +152,14 @@ public final class MySqlConnection implements Connection, ConnectionState { */ private volatile IsolationLevel currentLevel; + /** + * Session lock wait timeout. + */ + private volatile long lockWaitTimeout; + + /** + * Current transaction lock wait timeout. + */ private volatile long currentLockWaitTimeout; MySqlConnection(Client client, ConnectionContext context, Codecs codecs, IsolationLevel level, @@ -225,11 +232,9 @@ public Mono commitTransaction() { @Override public MySqlBatch createBatch() { - if (batchSupported) { - return new MySqlBatchingBatch(client, codecs, context); - } + return batchSupported ? new MySqlBatchingBatch(client, codecs, context) : + new MySqlSyntheticBatch(client, codecs, context); - return new MySqlSyntheticBatch(client, codecs, context); } @Override @@ -377,7 +382,7 @@ public void setIsolationLevel(IsolationLevel level) { } @Override - public void setLockWaitTimeout(long timeoutSeconds) { + public void setCurrentLockWaitTimeout(long timeoutSeconds) { this.currentLockWaitTimeout = timeoutSeconds; } @@ -392,7 +397,7 @@ public boolean isLockWaitTimeoutChanged() { } @Override - public void resetLockWaitTimeout() { + public void resetCurrentLockWaitTimeout() { this.currentLockWaitTimeout = this.lockWaitTimeout; } @@ -401,6 +406,23 @@ public boolean isInTransaction() { return (context.getServerStatuses() & ServerStatuses.IN_TRANSACTION) != 0; } + @Override + public Mono setLockWaitTimeout(Duration timeout) { + requireNonNull(timeout, "timeout must not be null"); + + long timeoutSeconds = timeout.getSeconds(); + return QueryFlow.executeVoid(client, "SET innodb_lock_wait_timeout=" + timeoutSeconds) + .doOnSuccess(ignored -> this.lockWaitTimeout = this.currentLockWaitTimeout = timeoutSeconds); + } + + @Override + public Publisher setStatementTimeout(Duration timeout) { + requireNonNull(timeout, "timeout must not be null"); + + // TODO: implement me + return Mono.empty(); + } + boolean isSessionAutoCommit() { return (context.getServerStatuses() & ServerStatuses.AUTO_COMMIT) != 0; } diff --git a/src/main/java/dev/miku/r2dbc/mysql/MySqlResult.java b/src/main/java/dev/miku/r2dbc/mysql/MySqlResult.java index 8ada542a..72bc4b1e 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/MySqlResult.java +++ b/src/main/java/dev/miku/r2dbc/mysql/MySqlResult.java @@ -19,168 +19,316 @@ import dev.miku.r2dbc.mysql.codec.Codecs; import dev.miku.r2dbc.mysql.message.FieldValue; import dev.miku.r2dbc.mysql.message.server.DefinitionMetadataMessage; -import dev.miku.r2dbc.mysql.message.server.EofMessage; +import dev.miku.r2dbc.mysql.message.server.ErrorMessage; import dev.miku.r2dbc.mysql.message.server.OkMessage; import dev.miku.r2dbc.mysql.message.server.RowMessage; import dev.miku.r2dbc.mysql.message.server.ServerMessage; import dev.miku.r2dbc.mysql.message.server.SyntheticMetadataMessage; import dev.miku.r2dbc.mysql.util.NettyBufferUtils; import dev.miku.r2dbc.mysql.util.OperatorUtils; +import io.netty.util.AbstractReferenceCounted; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import io.r2dbc.spi.R2dbcException; +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 reactor.core.publisher.MonoProcessor; import reactor.core.publisher.SynchronousSink; import reactor.util.annotation.Nullable; -import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Predicate; import static dev.miku.r2dbc.mysql.util.AssertUtils.requireNonNull; /** * An implementation of {@link Result} 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}. It's based on a {@link OkMessage}, when the {@code generatedKeyName} is + * not {@code null}. */ public final class MySqlResult implements Result { - private static final Function ROWS_UPDATED = message -> - (int) message.getAffectedRows(); - private static final Consumer RELEASE = ReferenceCounted::release; - private final boolean isBinary; - - private final Codecs codecs; - - private final ConnectionContext context; - - @Nullable - private final String generatedKeyName; - - private final AtomicReference> messages; + private static final BiConsumer> ROWS_UPDATED = (segment, sink) -> { + if (segment instanceof UpdateCount) { + sink.next((int) ((UpdateCount) segment).value()); + } else if (segment instanceof Message) { + sink.error(((Message) segment).exception()); + } else if (segment instanceof ReferenceCounted) { + ReferenceCountUtil.safeRelease(segment); + } + }; - private final MonoProcessor okProcessor = MonoProcessor.create(); + private static final BiFunction SUM = Integer::sum; - private MySqlRowMetadata rowMetadata; + private final Flux segments; - MySqlResult(boolean isBinary, Codecs codecs, ConnectionContext context, @Nullable String generatedKeyName, - Flux messages) { - this.isBinary = isBinary; - this.codecs = requireNonNull(codecs, "codecs must not be null"); - this.context = requireNonNull(context, "context must not be null"); - this.generatedKeyName = generatedKeyName; - this.messages = new AtomicReference<>(requireNonNull(messages, "messages must not be null")); + private MySqlResult(Flux segments) { + this.segments = segments; } @Override public Mono getRowsUpdated() { - return affects().map(ROWS_UPDATED); + return segments.handle(ROWS_UPDATED).reduce(SUM); } @Override - public Publisher map(BiFunction f) { + public Flux map(BiFunction f) { requireNonNull(f, "mapping function must not be null"); - if (generatedKeyName == null) { - return results().handle((message, sink) -> handleResult(message, sink, f)); - } + return segments.handle((segment, sink) -> { + if (segment instanceof RowSegment) { + Row row = ((RowSegment) segment).row(); - return affects().map(message -> { - InsertSyntheticRow row = new InsertSyntheticRow(codecs, generatedKeyName, - message.getLastInsertId()); - return f.apply(row, row); + try { + sink.next(f.apply(row, row.getMetadata())); + } finally { + ReferenceCountUtil.safeRelease(segment); + } + } else if (segment instanceof Message) { + sink.error(((Message) segment).exception()); + } else if (segment instanceof ReferenceCounted) { + ReferenceCountUtil.safeRelease(segment); + } }); } - private Mono affects() { - return this.okProcessor.doOnSubscribe(s -> { - Flux messages = this.messages.getAndSet(null); - - if (messages == null) { - // Has subscribed, `okProcessor` will be set or cancel. - return; - } + @Override + public Flux map(Function f) { + requireNonNull(f, "mapping function must not be null"); - messages.subscribe(message -> { - if (message instanceof OkMessage) { - // No need check terminal because of OkMessage no need release. - this.okProcessor.onNext(((OkMessage) message)); - } else if (message instanceof EofMessage) { - // Metadata EOF message will be not receive in here. - // EOF message, means it is SELECT statement. - this.okProcessor.onComplete(); - } else { - ReferenceCountUtil.safeRelease(message); + return segments.handle((segment, sink) -> { + if (segment instanceof RowSegment) { + try { + sink.next(f.apply(((RowSegment) segment).row())); + } finally { + ReferenceCountUtil.safeRelease(segment); } - }, this.okProcessor::onError, this.okProcessor::onComplete); + } else if (segment instanceof Message) { + sink.error(((Message) segment).exception()); + } else if (segment instanceof ReferenceCounted) { + ReferenceCountUtil.safeRelease(segment); + } }); } - private Flux results() { - return Flux.defer(() -> { - Flux messages = this.messages.getAndSet(null); + @Override + public MySqlResult filter(Predicate filter) { + requireNonNull(filter, "filter must not be null"); - if (messages == null) { - return Flux.error(new IllegalStateException("Source has been released")); + return new MySqlResult(segments.filter(segment -> { + if (filter.test(segment)) { + return true; } - // Result mode, no need ok message. - this.okProcessor.onComplete(); + if (segment instanceof ReferenceCounted) { + ReferenceCountUtil.safeRelease(segment); + } - return OperatorUtils.discardOnCancel(messages).doOnDiscard(ReferenceCounted.class, RELEASE); - }); + return false; + })); } - private void handleResult(ServerMessage message, SynchronousSink sink, - BiFunction f) { - if (message instanceof SyntheticMetadataMessage) { - DefinitionMetadataMessage[] metadataMessages = ((SyntheticMetadataMessage) message).unwrap(); - if (metadataMessages.length == 0) { - return; + @Override + public Flux flatMap(Function> f) { + requireNonNull(f, "mapping function must not be null"); + + return segments.flatMap(segment -> { + Publisher ret = f.apply(segment); + + if (ret == null) { + return Mono.error(new IllegalStateException("The mapper returned a null Publisher")); } - this.rowMetadata = MySqlRowMetadata.create(metadataMessages); - } else if (message instanceof RowMessage) { - processRow((RowMessage) message, sink, f); - } else { - ReferenceCountUtil.safeRelease(message); + + // doAfterTerminate to not release resources before they had a chance to get emitted. + if (ret instanceof Mono) { + @SuppressWarnings("unchecked") + Mono mono = (Mono) ret; + return mono.doAfterTerminate(() -> ReferenceCountUtil.release(segment)); + } + + return Flux.from(ret).doAfterTerminate(() -> ReferenceCountUtil.release(segment)); + }); + } + + static MySqlResult toResult(boolean binary, Codecs codecs, ConnectionContext context, + @Nullable String generatedKeyName, Flux messages) { + requireNonNull(codecs, "codecs must not be null"); + requireNonNull(context, "context must not be null"); + requireNonNull(messages, "messages must not be null"); + + return new MySqlResult(OperatorUtils.discardOnCancel(messages) + .doOnDiscard(ReferenceCounted.class, RELEASE) + .handle(new MySqlSegments(binary, codecs, context, generatedKeyName))); + } + + private static final class MySqlMessage implements Message { + + private final ErrorMessage message; + + private MySqlMessage(ErrorMessage message) { + this.message = message; + } + + @Override + public R2dbcException exception() { + return message.toException(); + } + + @Override + public int errorCode() { + return message.getCode(); + } + + @Override + public String sqlState() { + return message.getSqlState(); + } + + @Override + public String message() { + return message.getMessage(); } } - private void processRow(RowMessage message, SynchronousSink sink, - BiFunction f) { - MySqlRowMetadata rowMetadata = this.rowMetadata; + private static final class MySqlRowSegment extends AbstractReferenceCounted implements RowSegment { + + private final MySqlRow row; - if (rowMetadata == null) { - ReferenceCountUtil.safeRelease(message); - sink.error(new IllegalStateException("No MySqlRowMetadata available")); - return; + private final FieldValue[] fields; + + private MySqlRowSegment(FieldValue[] fields, MySqlRowMetadata metadata, Codecs codecs, boolean binary, + ConnectionContext context) { + this.row = new MySqlRow(fields, metadata, codecs, binary, context); + this.fields = fields; } - FieldValue[] fields; - T t; + @Override + public Row row() { + return row; + } - try { - fields = message.decode(isBinary, rowMetadata.unwrap()); - } finally { - // Release row messages' reader. - ReferenceCountUtil.safeRelease(message); + @Override + public ReferenceCounted touch(Object hint) { + if (this.fields.length == 0) { + return this; + } + + for (FieldValue field : this.fields) { + field.touch(hint); + } + + return this; } - try { - // Can NOT just sink.next(f.apply(...)) because of finally release - t = f.apply(new MySqlRow(fields, rowMetadata, codecs, isBinary, context), rowMetadata); - } finally { - // Release decoded field values. + @Override + protected void deallocate() { NettyBufferUtils.releaseAll(fields); } + } + + private static class MySqlUpdateCount implements UpdateCount { + + protected final OkMessage message; + + private MySqlUpdateCount(OkMessage message) { + this.message = message; + } + + @Override + public long value() { + return message.getAffectedRows(); + } + } + + private static final class MySqlOkSegment extends MySqlUpdateCount implements RowSegment { + + private final Codecs codecs; + + private final String keyName; + + private MySqlOkSegment(OkMessage message, Codecs codecs, String keyName) { + super(message); + + this.codecs = codecs; + this.keyName = keyName; + } + + @Override + public Row row() { + return new InsertSyntheticRow(codecs, keyName, message.getLastInsertId()); + } + } + + private static final class MySqlSegments implements BiConsumer> { - sink.next(t); + private final boolean binary; + + private final Codecs codecs; + + private final ConnectionContext context; + + @Nullable + private final String generatedKeyName; + + private MySqlRowMetadata rowMetadata; + + private MySqlSegments(boolean binary, Codecs codecs, ConnectionContext context, + @Nullable String generatedKeyName) { + this.binary = binary; + this.codecs = codecs; + this.context = context; + this.generatedKeyName = generatedKeyName; + } + + @Override + public void accept(ServerMessage message, SynchronousSink sink) { + if (message instanceof RowMessage) { + MySqlRowMetadata metadata = this.rowMetadata; + + if (metadata == null) { + ReferenceCountUtil.safeRelease(message); + sink.error(new IllegalStateException("No MySqlRowMetadata available")); + return; + } + + FieldValue[] fields; + + try { + fields = ((RowMessage) message).decode(binary, metadata.unwrap()); + } finally { + ReferenceCountUtil.safeRelease(message); + } + + sink.next(new MySqlRowSegment(fields, metadata, codecs, binary, context)); + } else if (message instanceof SyntheticMetadataMessage) { + DefinitionMetadataMessage[] metadataMessages = ((SyntheticMetadataMessage) message).unwrap(); + + if (metadataMessages.length == 0) { + return; + } + + this.rowMetadata = MySqlRowMetadata.create(metadataMessages); + } else if (message instanceof OkMessage) { + Segment segment = generatedKeyName == null ? new MySqlUpdateCount((OkMessage) message) : + new MySqlOkSegment((OkMessage) message, codecs, generatedKeyName); + + sink.next(segment); + } else if (message instanceof ErrorMessage) { + sink.next(new MySqlMessage((ErrorMessage) message)); + } else { + ReferenceCountUtil.safeRelease(message); + } + } } } diff --git a/src/main/java/dev/miku/r2dbc/mysql/MySqlRow.java b/src/main/java/dev/miku/r2dbc/mysql/MySqlRow.java index 4468b8ac..70e7f3a0 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/MySqlRow.java +++ b/src/main/java/dev/miku/r2dbc/mysql/MySqlRow.java @@ -19,6 +19,7 @@ import dev.miku.r2dbc.mysql.codec.Codecs; import dev.miku.r2dbc.mysql.message.FieldValue; import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; import reactor.util.annotation.Nullable; import java.lang.reflect.ParameterizedType; @@ -101,4 +102,12 @@ public T get(String name, ParameterizedType type) { MySqlColumnDescriptor info = rowMetadata.getColumnMetadata(name); return codecs.decode(fields[info.getIndex()], info, type, binary, context); } + + /** + * {@inheritDoc} + */ + @Override + public RowMetadata getMetadata() { + return rowMetadata; + } } diff --git a/src/main/java/dev/miku/r2dbc/mysql/MySqlRowMetadata.java b/src/main/java/dev/miku/r2dbc/mysql/MySqlRowMetadata.java index e445ce00..72d96f5f 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/MySqlRowMetadata.java +++ b/src/main/java/dev/miku/r2dbc/mysql/MySqlRowMetadata.java @@ -21,7 +21,6 @@ import io.r2dbc.spi.RowMetadata; import java.util.Arrays; -import java.util.Comparator; import java.util.List; import java.util.NoSuchElementException; import java.util.Set; @@ -35,18 +34,10 @@ */ final class MySqlRowMetadata implements RowMetadata { - private static final Comparator NAME_COMPARATOR = (left, right) -> - MySqlNames.compare(left.getName(), right.getName()); - private final MySqlColumnDescriptor[] originMetadata; private final MySqlColumnDescriptor[] sortedMetadata; - /** - * Copied column names from {@link #sortedMetadata}. - */ - private final String[] sortedNames; - private final ColumnNameSet nameSet; private MySqlRowMetadata(MySqlColumnDescriptor[] metadata) { @@ -60,21 +51,19 @@ private MySqlRowMetadata(MySqlColumnDescriptor[] metadata) { this.originMetadata = metadata; this.sortedMetadata = metadata; - this.sortedNames = new String[] { name }; this.nameSet = ColumnNameSet.of(name); break; default: MySqlColumnDescriptor[] sortedMetadata = new MySqlColumnDescriptor[size]; System.arraycopy(metadata, 0, sortedMetadata, 0, size); - Arrays.sort(sortedMetadata, NAME_COMPARATOR); + Arrays.sort(sortedMetadata, ColumnNameSet.NAME_COMPARATOR); String[] originNames = getNames(metadata); String[] sortedNames = getNames(sortedMetadata); this.originMetadata = metadata; this.sortedMetadata = sortedMetadata; - this.sortedNames = sortedNames; this.nameSet = ColumnNameSet.of(originNames, sortedNames); break; @@ -94,7 +83,7 @@ public MySqlColumnDescriptor getColumnMetadata(int index) { public MySqlColumnDescriptor getColumnMetadata(String name) { requireNonNull(name, "name must not be null"); - int index = MySqlNames.nameSearch(this.sortedNames, name); + int index = nameSet.findIndex(name); if (index < 0) { throw new NoSuchElementException("Column name '" + name + "' does not exist"); @@ -103,11 +92,19 @@ public MySqlColumnDescriptor getColumnMetadata(String name) { return sortedMetadata[index]; } + @Override + public boolean contains(String name) { + requireNonNull(name, "name must not be null"); + + return nameSet.contains(name); + } + @Override public List getColumnMetadatas() { return InternalArrays.asImmutableList(originMetadata); } + @SuppressWarnings("deprecation") @Override public Set getColumnNames() { return nameSet; @@ -116,7 +113,7 @@ public Set getColumnNames() { @Override public String toString() { return "MySqlRowMetadata{metadata=" + Arrays.toString(originMetadata) + ", sortedNames=" + - Arrays.toString(sortedNames) + '}'; + Arrays.toString(nameSet.getSortedNames()) + '}'; } MySqlColumnDescriptor[] unwrap() { diff --git a/src/main/java/dev/miku/r2dbc/mysql/MySqlSyntheticBatch.java b/src/main/java/dev/miku/r2dbc/mysql/MySqlSyntheticBatch.java index b8e2a36a..36812a23 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/MySqlSyntheticBatch.java +++ b/src/main/java/dev/miku/r2dbc/mysql/MySqlSyntheticBatch.java @@ -54,7 +54,7 @@ public MySqlBatch add(String sql) { @Override public Flux execute() { return QueryFlow.execute(client, statements) - .map(messages -> new MySqlResult(false, codecs, context, null, messages)); + .map(messages -> MySqlResult.toResult(false, codecs, context, null, messages)); } @Override diff --git a/src/main/java/dev/miku/r2dbc/mysql/OptionMapper.java b/src/main/java/dev/miku/r2dbc/mysql/OptionMapper.java index e28d557e..9d35f9d5 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/OptionMapper.java +++ b/src/main/java/dev/miku/r2dbc/mysql/OptionMapper.java @@ -42,16 +42,18 @@ SourceSpec from(Option option) { return new SourceSpec(options, option); } + @SuppressWarnings("unchecked") void consume(Option option, Consumer consumer) { - T t = options.getValue(option); + Object t = options.getValue(option); if (t != null) { - consumer.accept(t); + consumer.accept((T) t); } } + @SuppressWarnings("unchecked") void requiredConsume(Option option, Consumer consumer) { - consumer.accept(options.getRequiredValue(option)); + consumer.accept((T) options.getRequiredValue(option)); } } diff --git a/src/main/java/dev/miku/r2dbc/mysql/ParametrizedStatementSupport.java b/src/main/java/dev/miku/r2dbc/mysql/ParametrizedStatementSupport.java index 729c8e87..f8c3c0e1 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/ParametrizedStatementSupport.java +++ b/src/main/java/dev/miku/r2dbc/mysql/ParametrizedStatementSupport.java @@ -23,6 +23,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.NoSuchElementException; import java.util.Spliterator; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -134,7 +135,7 @@ private ParameterIndex getIndexes(String name) { ParameterIndex index = query.getNamedIndexes().get(name); if (index == null) { - throw new IllegalArgumentException(String.format("No such parameter with name '%s'", name)); + throw new NoSuchElementException("No such parameter with name: " + name); } return index; diff --git a/src/main/java/dev/miku/r2dbc/mysql/PrepareParametrizedStatement.java b/src/main/java/dev/miku/r2dbc/mysql/PrepareParametrizedStatement.java index 08b73270..1e32d3df 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/PrepareParametrizedStatement.java +++ b/src/main/java/dev/miku/r2dbc/mysql/PrepareParametrizedStatement.java @@ -43,7 +43,7 @@ final class PrepareParametrizedStatement extends ParametrizedStatementSupport { @Override public Flux execute(List bindings) { return QueryFlow.execute(client, query.getFormattedSql(), bindings, fetchSize, prepareCache) - .map(messages -> new MySqlResult(true, codecs, context, generatedKeyName, messages)); + .map(messages -> MySqlResult.toResult(true, codecs, context, generatedKeyName, messages)); } @Override diff --git a/src/main/java/dev/miku/r2dbc/mysql/PrepareSimpleStatement.java b/src/main/java/dev/miku/r2dbc/mysql/PrepareSimpleStatement.java index b91722c8..b5dfc6a0 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/PrepareSimpleStatement.java +++ b/src/main/java/dev/miku/r2dbc/mysql/PrepareSimpleStatement.java @@ -46,7 +46,7 @@ final class PrepareSimpleStatement extends SimpleStatementSupport { @Override public Flux execute() { return QueryFlow.execute(client, sql, BINDINGS, fetchSize, prepareCache) - .map(messages -> new MySqlResult(true, codecs, context, generatedKeyName, messages)); + .map(messages -> MySqlResult.toResult(true, codecs, context, generatedKeyName, messages)); } @Override diff --git a/src/main/java/dev/miku/r2dbc/mysql/QueryFlow.java b/src/main/java/dev/miku/r2dbc/mysql/QueryFlow.java index 146303c1..c4f332d7 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/QueryFlow.java +++ b/src/main/java/dev/miku/r2dbc/mysql/QueryFlow.java @@ -48,8 +48,8 @@ import dev.miku.r2dbc.mysql.message.server.SyntheticSslResponseMessage; import dev.miku.r2dbc.mysql.util.InternalArrays; import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; import io.r2dbc.spi.IsolationLevel; -import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.R2dbcPermissionDeniedException; import io.r2dbc.spi.TransactionDefinition; import org.slf4j.Logger; @@ -82,32 +82,19 @@ final class QueryFlow { // Metadata EOF message will be not receive in here. private static final Predicate RESULT_DONE = message -> message instanceof CompleteMessage; - private static final Consumer OBJ_RELEASE = ReferenceCountUtil::release; - - /** - * Login a {@link Client} and receive the {@code client} after logon. - * - * @param client the {@link Client} to exchange messages with. - * @param sslMode the {@link SslMode} defines SSL capability and behavior. - * @param database the database that will be connected. - * @param user the user that will be login. - * @param password the password of the {@code user}. - * @param context the {@link ConnectionContext} for initialization. - * @return the {@link Client}, or an error/exception received by login failed. - */ - static Mono login(Client client, SslMode sslMode, String database, String user, - @Nullable CharSequence password, ConnectionContext context) { - return client.exchange(new LoginExchangeable(client, sslMode, database, user, password, context)) - .onErrorResume(e -> client.forceClose().then(Mono.error(e))) - .then(Mono.just(client)); - } + private static final Consumer EXECUTE_VOID = message -> { + if (message instanceof ErrorMessage) { + throw ((ErrorMessage) message).toException(); + } else if (message instanceof ReferenceCounted) { + ReferenceCountUtil.safeRelease(message); + } + }; /** * Execute multiple bindings of a server-preparing statement with one-by-one binary execution. The * execution terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. If client - * receives a {@link ErrorMessage} will emit an exception and cancel subsequent {@link Binding}s. The - * exchange will be completed by {@link CompleteMessage} after receive the last result for the last - * binding. + * 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 original statement for exception tracing. @@ -150,36 +137,6 @@ static Flux> execute(Client client, Query query, List executeVoid(Client client, String sql) { - return Mono.defer(() -> execute0(client, sql).doOnNext(OBJ_RELEASE).then()); - } - - /** - * Execute multiple simple queries with one-by-one 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 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 the queries to execute, each element can be contains multi-statements. - * @return receives complete signal. - */ - static Mono executeVoid(Client client, String... statements) { - return client.exchange(new MultiQueryExchangeable(InternalArrays.asIterator(statements))) - .doOnNext(OBJ_RELEASE) - .then(); - } - /** * 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 @@ -217,6 +174,56 @@ static Flux> execute(Client client, List statements) }); } + /** + * Login a {@link Client} and receive the {@code client} after logon. It will emit an exception when + * client receives a {@link ErrorMessage}. + * + * + * @param client the {@link Client} to exchange messages with. + * @param sslMode the {@link SslMode} defines SSL capability and behavior. + * @param database the database that will be connected. + * @param user the user that will be login. + * @param password the password of the {@code user}. + * @param context the {@link ConnectionContext} for initialization. + * @return the messages received in response to the login exchange. + */ + static Mono login(Client client, SslMode sslMode, String database, String user, + @Nullable CharSequence password, ConnectionContext context) { + return client.exchange(new LoginExchangeable(client, sslMode, database, user, password, context)) + .flatMap(message -> client.forceClose().then(Mono.error(message::toException))) + .then(Mono.just(client)); + } + + /** + * Execute a simple query and return a {@link Mono} for the complete signal or error. Query execution + * terminates with the last {@link CompleteMessage} or a {@link ErrorMessage}. The {@link ErrorMessage} + * will emit an exception. The exchange will be completed by {@link CompleteMessage} after receive the + * last result for the last binding. + * + * @param client the {@link Client} to exchange messages with. + * @param sql the query to execute, can be contains multi-statements. + * @return receives complete signal. + */ + static Mono executeVoid(Client client, String sql) { + return Mono.defer(() -> execute0(client, sql).doOnNext(EXECUTE_VOID).then()); + } + + /** + * Execute multiple simple queries with one-by-one 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 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 the queries to execute, each element can be contains multi-statements. + * @return receives complete signal. + */ + static Mono executeVoid(Client client, String... statements) { + return client.exchange(new MultiQueryExchangeable(InternalArrays.asIterator(statements))) + .doOnNext(EXECUTE_VOID) + .then(); + } + /** * Begins a new transaction with a {@link TransactionDefinition}. It will change current transaction * statuses of the {@link ConnectionState}. @@ -273,7 +280,8 @@ static Mono doneTransaction(Client client, ConnectionState state, boolean private static Flux execute0(Client client, String sql) { return client.exchange(TextQueryMessage.of(sql), (message, sink) -> { if (message instanceof ErrorMessage) { - sink.error(ExceptionFactory.createException((ErrorMessage) message, sql)); + sink.next(((ErrorMessage) message).offendedBy(sql)); + sink.complete(); } else { sink.next(message); @@ -303,7 +311,8 @@ public final void subscribe(CoreSubscriber actual) { @Override public final void accept(ServerMessage message, SynchronousSink sink) { if (message instanceof ErrorMessage) { - sink.error(transform((ErrorMessage) message)); + sink.next(((ErrorMessage) message).offendedBy(offendingSql())); + sink.complete(); } else { sink.next(message); @@ -323,7 +332,7 @@ public final void accept(ServerMessage message, SynchronousSink s abstract protected ClientMessage nextMessage(); - abstract protected R2dbcException transform(ErrorMessage message); + abstract protected String offendingSql(); } /** @@ -375,8 +384,8 @@ protected ClientMessage nextMessage() { } @Override - protected R2dbcException transform(ErrorMessage message) { - return ExceptionFactory.createException(message, query.getFormattedSql()); + protected String offendingSql() { + return query.getFormattedSql(); } } @@ -406,8 +415,8 @@ public boolean isDisposed() { @Override protected void afterSubscribe() { String current = this.statements.next(); - this.requests.onNext(TextQueryMessage.of(current)); this.current = current; + this.requests.onNext(TextQueryMessage.of(current)); } @Override @@ -423,8 +432,8 @@ protected ClientMessage nextMessage() { } @Override - protected R2dbcException transform(ErrorMessage message) { - return ExceptionFactory.createException(message, current); + protected String offendingSql() { + return current; } } @@ -498,7 +507,8 @@ public void subscribe(CoreSubscriber actual) { @Override public void accept(ServerMessage message, SynchronousSink sink) { if (message instanceof ErrorMessage) { - sink.error(ExceptionFactory.createException((ErrorMessage) message, sql)); + sink.next(((ErrorMessage) message).offendedBy(sql)); + sink.complete(); return; } @@ -701,7 +711,7 @@ private void onCompleteMessage(CompleteMessage message, SynchronousSink { +final class LoginExchangeable extends FluxExchangeable { private static final Logger logger = LoggerFactory.getLogger(LoginExchangeable.class); @@ -753,9 +763,10 @@ public void subscribe(CoreSubscriber actual) { } @Override - public void accept(ServerMessage message, SynchronousSink sink) { + public void accept(ServerMessage message, SynchronousSink sink) { if (message instanceof ErrorMessage) { - sink.error(ExceptionFactory.createException((ErrorMessage) message, null)); + sink.next((ErrorMessage) message); + sink.complete(); return; } @@ -963,11 +974,9 @@ final Iterator statements() { final boolean accept(ServerMessage message, SynchronousSink sink) { if (message instanceof ErrorMessage) { - sink.error(ExceptionFactory.createException((ErrorMessage) message, sql)); + sink.error(((ErrorMessage) message).toException(sql)); return false; - } - - if (message instanceof CompleteMessage) { + } else if (message instanceof CompleteMessage) { // Note: if CompleteMessage.isDone() is true, it is the last complete message of the entire // operation in batch mode and the last complete message of the current query in multi-query mode. // That means each complete message should be processed whatever it is done or not IN BATCH MODE. @@ -980,10 +989,10 @@ final boolean accept(ServerMessage message, SynchronousSink sink) { this.tasks -= task; return process(task, sink); + } else if (message instanceof ReferenceCounted) { + ReferenceCountUtil.safeRelease(message); } - ReferenceCountUtil.safeRelease(message); - return false; } @@ -1017,7 +1026,7 @@ boolean cancelTasks() { protected boolean process(int task, SynchronousSink sink) { switch (task) { case LOCK_WAIT_TIMEOUT: - state.resetLockWaitTimeout(); + state.resetCurrentLockWaitTimeout(); return true; case COMMIT_OR_ROLLBACK: state.resetIsolationLevel(); @@ -1081,7 +1090,7 @@ boolean cancelTasks() { protected boolean process(int task, SynchronousSink sink) { switch (task) { case LOCK_WAIT_TIMEOUT: - state.setLockWaitTimeout(lockWaitTimeout); + state.setCurrentLockWaitTimeout(lockWaitTimeout); return true; case ISOLATION_LEVEL: if (isolationLevel != null) { diff --git a/src/main/java/dev/miku/r2dbc/mysql/TextParametrizedStatement.java b/src/main/java/dev/miku/r2dbc/mysql/TextParametrizedStatement.java index 0d1db83d..0a04c602 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/TextParametrizedStatement.java +++ b/src/main/java/dev/miku/r2dbc/mysql/TextParametrizedStatement.java @@ -34,6 +34,6 @@ final class TextParametrizedStatement extends ParametrizedStatementSupport { @Override protected Flux execute(List bindings) { return QueryFlow.execute(client, query, bindings) - .map(messages -> new MySqlResult(false, codecs, context, generatedKeyName, messages)); + .map(messages -> MySqlResult.toResult(false, codecs, context, generatedKeyName, messages)); } } diff --git a/src/main/java/dev/miku/r2dbc/mysql/TextSimpleStatement.java b/src/main/java/dev/miku/r2dbc/mysql/TextSimpleStatement.java index 17c04382..513d0e58 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/TextSimpleStatement.java +++ b/src/main/java/dev/miku/r2dbc/mysql/TextSimpleStatement.java @@ -32,6 +32,6 @@ final class TextSimpleStatement extends SimpleStatementSupport { @Override public Flux execute() { return QueryFlow.execute(client, sql) - .map(messages -> new MySqlResult(false, codecs, context, generatedKeyName, messages)); + .map(messages -> MySqlResult.toResult(false, codecs, context, generatedKeyName, messages)); } } diff --git a/src/main/java/dev/miku/r2dbc/mysql/client/WriteSubscriber.java b/src/main/java/dev/miku/r2dbc/mysql/client/WriteSubscriber.java index 12265fb6..b3ea07f8 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/client/WriteSubscriber.java +++ b/src/main/java/dev/miku/r2dbc/mysql/client/WriteSubscriber.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; +import io.netty.util.ReferenceCountUtil; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; diff --git a/src/main/java/dev/miku/r2dbc/mysql/message/server/ErrorMessage.java b/src/main/java/dev/miku/r2dbc/mysql/message/server/ErrorMessage.java index a562dd00..12d2bb42 100644 --- a/src/main/java/dev/miku/r2dbc/mysql/message/server/ErrorMessage.java +++ b/src/main/java/dev/miku/r2dbc/mysql/message/server/ErrorMessage.java @@ -17,6 +17,14 @@ package dev.miku.r2dbc.mysql.message.server; import io.netty.buffer.ByteBuf; +import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.R2dbcRollbackException; +import io.r2dbc.spi.R2dbcTimeoutException; +import io.r2dbc.spi.R2dbcTransientResourceException; import reactor.util.annotation.Nullable; import java.nio.charset.StandardCharsets; @@ -25,27 +33,111 @@ import static dev.miku.r2dbc.mysql.util.AssertUtils.requireNonNull; /** - * MySQL error message, sql state will be a property independently. + * A message considers an error that's reported by server-side. Not like JDBC MySQL, the SQL state is an + * independent property. + *

+ * The {@link #offendingSql} will be bound by statement flow, protocol layer will always get {@code null}. */ public final class ErrorMessage implements ServerMessage { + private static final String CONSTRAINT_VIOLATION_PREFIX = "23"; + + private static final String TRANSACTION_ROLLBACK_PREFIX = "40"; + + private static final String SYNTAX_ERROR_PREFIX = "42"; + private static final int SQL_STATE_SIZE = 5; - private final int errorCode; + private final int code; @Nullable private final String sqlState; - private final String errorMessage; + private final String message; + + @Nullable + private final String offendingSql; + + private ErrorMessage(int code, @Nullable String sqlState, String message) { + this(code, sqlState, message, null); + } - private ErrorMessage(int errorCode, @Nullable String sqlState, String errorMessage) { - this.errorCode = errorCode; + private ErrorMessage(int code, @Nullable String sqlState, String message, @Nullable String offendingSql) { + this.code = code; this.sqlState = sqlState; - this.errorMessage = requireNonNull(errorMessage, "error message must not be null"); + this.message = requireNonNull(message, "message must not be null"); + this.offendingSql = offendingSql; + } + + public R2dbcException toException() { + return toException(offendingSql); + } + + public R2dbcException toException(@Nullable String sql) { + // Should keep looking more error codes + switch (code) { + case 1044: // Database access denied + case 1045: // Wrong password + case 1095: // Kill thread denied + case 1142: // Table access denied + case 1143: // Column access denied + case 1227: // Operation has no privilege(s) + case 1370: // Routine or process access denied + case 1698: // User need password but has no password + case 1873: // Change user denied + return new R2dbcPermissionDeniedException(message, sqlState, code); + case 1159: // Read interrupted, reading packet timeout because of network jitter in most cases + case 1161: // Write interrupted, writing packet timeout because of network jitter in most cases + case 1213: // Dead-lock :-( no one wants this + case 1317: // Statement execution interrupted + return new R2dbcTransientResourceException(message, sqlState, code); + case 1205: // Wait lock timeout + case 1907: // Statement executing timeout + return new R2dbcTimeoutException(message, sqlState, code); + case 1613: // Transaction rollback because of took too long + return new R2dbcRollbackException(message, sqlState, code); + case 1050: // Table already exists + case 1051: // Unknown table + case 1054: // Unknown column name in existing table + case 1064: // Bad syntax + case 1247: // Unsupported reference + case 1146: // Unknown table name + case 1304: // Something already exists, like savepoint + case 1305: // Something does not exists, like savepoint + case 1630: // Function not exists + return new R2dbcBadGrammarException(message, sqlState, code, sql); + case 1022: // Duplicate key + case 1048: // Field cannot be null + case 1062: // Duplicate entry for key constraint + case 1169: // Violation of an unique constraint + case 1215: // Add a foreign key has a violation + case 1216: // Child row has a violation of foreign key constraint when inserting or updating + case 1217: // Parent row has a violation of foreign key constraint when deleting or updating + case 1364: // Field has no default value but user try set it to DEFAULT + case 1451: // Parent row has a violation of foreign key constraint when deleting or updating + case 1452: // Child row has a violation of foreign key constraint when inserting or updating + case 1557: // Conflicting foreign key constraints and unique constraints + case 1859: // Duplicate unknown entry for key constraint + return new R2dbcDataIntegrityViolationException(message, sqlState, code); + } + + if (sqlState == null) { + // Has no SQL state, all exceptions mismatch, fallback. + return new R2dbcNonTransientResourceException(message, null, code); + } else if (sqlState.startsWith(SYNTAX_ERROR_PREFIX)) { + return new R2dbcBadGrammarException(message, sqlState, code, sql); + } else if (sqlState.startsWith(CONSTRAINT_VIOLATION_PREFIX)) { + return new R2dbcDataIntegrityViolationException(message, sqlState, code); + } else if (sqlState.startsWith(TRANSACTION_ROLLBACK_PREFIX)) { + return new R2dbcRollbackException(message, sqlState, code); + } + + // Uncertain SQL state, all exceptions mismatch, fallback. + return new R2dbcNonTransientResourceException(message, null, code); } - public int getErrorCode() { - return errorCode; + public int getCode() { + return code; } @Nullable @@ -53,8 +145,18 @@ public String getSqlState() { return sqlState; } - public String getErrorMessage() { - return errorMessage; + public String getMessage() { + return message; + } + + /** + * Creates a new {@link ErrorMessage} with specific offending statement. + * + * @param sql offending statement. + * @return {@code this} if {@code sql} is {@code null}, otherwise a new {@link ErrorMessage}. + */ + public ErrorMessage offendedBy(@Nullable String sql) { + return sql == null ? this : new ErrorMessage(code, sqlState, message, sql); } /** @@ -92,19 +194,18 @@ public boolean equals(Object o) { ErrorMessage that = (ErrorMessage) o; - return errorCode == that.errorCode && Objects.equals(sqlState, that.sqlState) && - errorMessage.equals(that.errorMessage); + return code == that.code && Objects.equals(sqlState, that.sqlState) && + message.equals(that.message) && Objects.equals(offendingSql, that.offendingSql); } @Override public int hashCode() { - int hash = 31 * errorCode + (sqlState != null ? sqlState.hashCode() : 0); - return 31 * hash + errorMessage.hashCode(); + int hash = 31 * code + Objects.hashCode(sqlState); + return 31 * (31 * hash + message.hashCode()) + Objects.hashCode(offendingSql); } @Override public String toString() { - return "ErrorMessage{errorCode=" + errorCode + ", sqlState='" + sqlState + "', errorMessage='" + - errorMessage + "'}"; + return "ErrorMessage{code=" + code + ", sqlState='" + sqlState + "', message='" + message + "'}"; } } diff --git a/src/test/java/dev/miku/r2dbc/mysql/QueryIntegrationTestSupport.java b/src/test/java/dev/miku/r2dbc/mysql/QueryIntegrationTestSupport.java index a2b930ec..bf6387ce 100644 --- a/src/test/java/dev/miku/r2dbc/mysql/QueryIntegrationTestSupport.java +++ b/src/test/java/dev/miku/r2dbc/mysql/QueryIntegrationTestSupport.java @@ -17,6 +17,8 @@ package dev.miku.r2dbc.mysql; import com.fasterxml.jackson.core.type.TypeReference; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; import io.r2dbc.spi.Connection; import io.r2dbc.spi.Result; import org.junit.jupiter.api.Test; @@ -44,6 +46,7 @@ import java.util.EnumSet; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -554,6 +557,22 @@ private static Flux extractFirstInteger(Result result) { return Flux.from(result.map((row, metadata) -> row.get(0, Integer.class))); } + private static Flux> extractOk(Result result, Class type) { + return Flux.from(result.flatMap(segment -> { + try { + if (segment instanceof Result.UpdateCount && segment instanceof Result.RowSegment) { + long affected = ((Result.UpdateCount) segment).value(); + T t = Objects.requireNonNull(((Result.RowSegment) segment).row().get(0, type)); + return Mono.just(Tuples.of(affected, t)); + } else { + return Mono.empty(); + } + } finally { + ReferenceCountUtil.release(segment); + } + })); + } + @SuppressWarnings("unchecked") private static Flux> extractOptionalField(Result result, Type type) { if (type instanceof Class) { @@ -605,13 +624,11 @@ private static Mono testOne(MySqlConnection connection, Type type, boo return Mono.from(insert.returnGeneratedValues("id") .execute()) - .flatMap(result -> extractRowsUpdated(result) - .doOnNext(u -> assertThat(u).isEqualTo(1)) - .thenMany(extractFirstInteger(result)) + .flatMap(result -> extractOk(result, Integer.class) .collectList() .map(ids -> { - assertThat(ids).hasSize(1); - return ids.get(0); + assertThat(ids).hasSize(1).first().extracting(Tuple2::getT1).isEqualTo(1L); + return ids.get(0).getT2(); })) .flatMap(id -> Mono.from(connection.createStatement("SELECT value FROM test WHERE id=?") .bind(0, id) diff --git a/src/test/java/dev/miku/r2dbc/mysql/StatementTestSupport.java b/src/test/java/dev/miku/r2dbc/mysql/StatementTestSupport.java index 0c97db9f..6fb5ed60 100644 --- a/src/test/java/dev/miku/r2dbc/mysql/StatementTestSupport.java +++ b/src/test/java/dev/miku/r2dbc/mysql/StatementTestSupport.java @@ -18,6 +18,8 @@ import org.junit.jupiter.api.Test; +import java.util.NoSuchElementException; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -62,7 +64,7 @@ default void badBind() { assertThrows(IndexOutOfBoundsException.class, () -> statement.bind(-1, 1)); assertThrows(IndexOutOfBoundsException.class, () -> statement.bind(2, 1)); assertThrows(IllegalArgumentException.class, () -> statement.bind(1, null)); - assertThrows(IllegalArgumentException.class, () -> statement.bind("", 1)); + assertThrows(NoSuchElementException.class, () -> statement.bind("", 1)); assertThrows(IllegalArgumentException.class, () -> statement.bind("", null)); } else { assertThrows(UnsupportedOperationException.class, () -> statement.bind(0, 1)); @@ -103,7 +105,7 @@ default void badBindNull() { assertThrows(IndexOutOfBoundsException.class, () -> statement.bindNull(-1, Integer.class)); assertThrows(IndexOutOfBoundsException.class, () -> statement.bindNull(2, Integer.class)); assertThrows(IllegalArgumentException.class, () -> statement.bindNull(1, null)); - assertThrows(IllegalArgumentException.class, () -> statement.bindNull("", Integer.class)); + assertThrows(NoSuchElementException.class, () -> statement.bindNull("", Integer.class)); assertThrows(IllegalArgumentException.class, () -> statement.bindNull("", null)); } else { assertThrows(UnsupportedOperationException.class, () -> statement.bindNull(0, Integer.class)); diff --git a/src/test/java/dev/miku/r2dbc/mysql/util/FluxCumulateEnvelopeTest.java b/src/test/java/dev/miku/r2dbc/mysql/util/FluxCumulateEnvelopeTest.java index c1970b87..b34da431 100644 --- a/src/test/java/dev/miku/r2dbc/mysql/util/FluxCumulateEnvelopeTest.java +++ b/src/test/java/dev/miku/r2dbc/mysql/util/FluxCumulateEnvelopeTest.java @@ -19,8 +19,8 @@ import dev.miku.r2dbc.mysql.constant.Envelopes; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.PooledByteBufAllocator; -import io.netty.buffer.Unpooled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -43,23 +43,33 @@ class FluxCumulateEnvelopeTest { private static final byte[] RD_PATTERN = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz" .getBytes(StandardCharsets.US_ASCII); + private static final Consumer EMPTY_HEADER = buf -> { + try { + assertThat(ByteBufUtil.getBytes(buf, buf.readerIndex(), buf.readableBytes(), false)) + .hasSize(4) + .containsOnly(0); + } finally { + buf.release(); + } + }; + private final ByteBufAllocator allocator = PooledByteBufAllocator.DEFAULT; @Test void empty() { envelopes(Flux.empty(), randomEnvelopeSize()) .as(StepVerifier::create) - .expectNext(Unpooled.wrappedBuffer(new byte[4])) + .assertNext(EMPTY_HEADER) .verifyComplete(); envelopes(Flux.just(allocator.buffer(0, 0)), randomEnvelopeSize()) .as(StepVerifier::create) - .expectNext(Unpooled.wrappedBuffer(new byte[4])) + .assertNext(EMPTY_HEADER) .verifyComplete(); envelopes(Flux.empty(), Integer.MAX_VALUE) .as(StepVerifier::create) - .expectNext(Unpooled.wrappedBuffer(new byte[4])) + .assertNext(EMPTY_HEADER) .verifyComplete(); } @@ -272,7 +282,7 @@ private Consumer> assertBuffers(String origin, int envelopeSize, i buffers.add(envelope); } else { assertThat(n - 1).isEqualTo(i); - buffers.add(Unpooled.buffer(0, 0)); + buffers.add(header.alloc().buffer(0, 0)); } }