Skip to content

Commit faa30a7

Browse files
committed
Add support for SQL mode NO_BACKSLASH_ESCAPES
1 parent e97dc7d commit faa30a7

File tree

11 files changed

+140
-55
lines changed

11 files changed

+140
-55
lines changed

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/ConnectionContext.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,10 @@ public boolean isMariaDb() {
216216
return (capability != null && capability.isMariaDb()) || serverVersion.isMariaDb();
217217
}
218218

219+
public boolean isNoBackslashEscapes() {
220+
return (serverStatuses & ServerStatuses.NO_BACKSLASH_ESCAPES) != 0;
221+
}
222+
219223
@Override
220224
public ZeroDateOption getZeroDateOption() {
221225
return zeroDateOption;

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/InitFlow.java

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -209,31 +209,31 @@ private static Mono<SessionState> loadAndInitInnoDbEngineStatus(
209209
Codecs codecs,
210210
@Nullable Duration lockWaitTimeout
211211
) {
212-
return new TextSimpleStatement(client, codecs, "SHOW VARIABLES LIKE 'innodb\\\\_lock\\\\_wait\\\\_timeout'")
213-
.execute()
214-
.flatMap(r -> r.map(readable -> {
215-
String value = readable.get(1, String.class);
212+
return new TextSimpleStatement(
213+
client,
214+
codecs,
215+
"SHOW VARIABLES LIKE 'innodb_lock_wait_timeout'"
216+
).execute().flatMap(r -> r.map(readable -> {
217+
String value = readable.get(1, String.class);
216218

217-
if (value == null || value.isEmpty()) {
218-
return data;
219-
} else {
220-
return data.lockWaitTimeout(Duration.ofSeconds(Long.parseLong(value)));
219+
if (value == null || value.isEmpty()) {
220+
return data;
221+
} else {
222+
return data.lockWaitTimeout(Duration.ofSeconds(Long.parseLong(value)));
223+
}
224+
})).single(data).flatMap(d -> {
225+
if (lockWaitTimeout != null) {
226+
// Do not use context.isLockWaitTimeoutSupported() here, because its session variable is not set
227+
if (d.lockWaitTimeoutSupported) {
228+
return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(lockWaitTimeout))
229+
.then(Mono.fromSupplier(() -> d.lockWaitTimeout(lockWaitTimeout)));
221230
}
222-
}))
223-
.single(data)
224-
.flatMap(d -> {
225-
if (lockWaitTimeout != null) {
226-
// Do not use context.isLockWaitTimeoutSupported() here, because its session variable is not set
227-
if (d.lockWaitTimeoutSupported) {
228-
return QueryFlow.executeVoid(client, StringUtils.lockWaitTimeoutStatement(lockWaitTimeout))
229-
.then(Mono.fromSupplier(() -> d.lockWaitTimeout(lockWaitTimeout)));
230-
}
231231

232-
logger.warn("Lock wait timeout is not supported by server, ignore initial setting");
233-
return Mono.just(d);
234-
}
232+
logger.warn("Lock wait timeout is not supported by server, ignore initial setting");
235233
return Mono.just(d);
236-
});
234+
}
235+
return Mono.just(d);
236+
});
237237
}
238238

239239
private static Mono<SessionState> loadSessionVariables(Client client, Codecs codecs) {

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/constant/ServerStatuses.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,20 @@ public final class ServerStatuses {
5050
public static final short LAST_ROW_SENT = 128;
5151

5252
// public static final short DB_DROPPED = 256;
53-
// public static final short NO_BACKSLASH_ESCAPES = 512;
53+
54+
/**
55+
* Server does not permit backslash escapes.
56+
*
57+
* @since 1.1.3
58+
*/
59+
public static final short NO_BACKSLASH_ESCAPES = 512;
60+
5461
// public static final short METADATA_CHANGED = 1024;
5562
// public static final short QUERY_WAS_SLOW = 2048;
5663
// public static final short PS_OUT_PARAMS = 4096;
5764
// public static final short IN_TRANS_READONLY = 8192;
5865
// public static final short SESSION_STATE_CHANGED = 16384;
5966

60-
private ServerStatuses() { }
67+
private ServerStatuses() {
68+
}
6169
}

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/ParamWriter.java

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,17 @@ final class ParamWriter extends ParameterWriter {
4545

4646
private final StringBuilder builder;
4747

48+
private final boolean noBackslashEscapes;
49+
4850
private final Query query;
4951

5052
private int index;
5153

5254
private Mode mode;
5355

54-
private ParamWriter(Query query) {
56+
private ParamWriter(boolean noBackslashEscapes, Query query) {
5557
this.builder = newBuilder(query);
58+
this.noBackslashEscapes = noBackslashEscapes;
5659
this.query = query;
5760
this.index = 1;
5861
this.mode = 1 < query.getPartSize() ? Mode.AVAILABLE : Mode.FULL;
@@ -318,15 +321,19 @@ private void write0(char[] s, int off, int len) {
318321
}
319322

320323
private void escape(char c) {
324+
if (c == '\'') {
325+
// MySQL will auto-combine consecutive strings, whatever backslash is used or not, e.g. '1''2' -> '1\'2'
326+
builder.append('\'').append('\'');
327+
return;
328+
} else if (noBackslashEscapes) {
329+
builder.append(c);
330+
return;
331+
}
332+
321333
switch (c) {
322334
case '\\':
323335
builder.append('\\').append('\\');
324336
break;
325-
case '\'':
326-
// MySQL will auto-combine consecutive strings, like '1''2' -> '12'.
327-
// Sure, there can use '1\'2', but this will be better. (For some logging systems)
328-
builder.append('\'').append('\'');
329-
break;
330337
// Maybe useful in the future, keep '"' here.
331338
// case '"': buf.append('\\').append('"'); break;
332339
// 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) {
335342
// case '\u00a5': do something; break;
336343
// case '\u20a9': do something; break;
337344
case 0:
338-
// MySQL is based on C/C++, must escape '\0' which is an end flag in C style string.
345+
// Should escape '\0' which is an end flag in C style string.
339346
builder.append('\\').append('0');
340347
break;
341348
case '\032':
342-
// It seems like a problem on Windows 32, maybe check current OS here?
349+
// It gives some problems on Win32.
343350
builder.append('\\').append('Z');
344351
break;
345352
case '\n':
346-
// Should escape it for some logging such as Relational Database Service (RDS) Logging
347-
// System, etc. Sure, it is not necessary, but this will be better.
353+
// Should be escaped for better logging.
348354
builder.append('\\').append('n');
349355
break;
350356
case '\r':
351-
// Should escape it for some logging such as RDS Logging System, etc.
357+
// Should be escaped for better logging.
352358
builder.append('\\').append('r');
353359
break;
354360
default:
@@ -357,9 +363,9 @@ private void escape(char c) {
357363
}
358364
}
359365

360-
static Mono<String> publish(Query query, Flux<MySqlParameter> values) {
366+
static Mono<String> publish(boolean noBackslashEscapes, Query query, Flux<MySqlParameter> values) {
361367
return Mono.defer(() -> {
362-
ParamWriter writer = new ParamWriter(query);
368+
ParamWriter writer = new ParamWriter(noBackslashEscapes, query);
363369

364370
return OperatorUtils.discardOnCancel(values)
365371
.doOnDiscard(MySqlParameter.class, DISPOSE)

r2dbc-mysql/src/main/java/io/asyncer/r2dbc/mysql/message/client/PreparedTextQueryMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public Mono<ByteBuf> encode(ByteBufAllocator allocator, ConnectionContext contex
8787
return Flux.fromArray(values);
8888
});
8989

90-
return ParamWriter.publish(query, parameters).handle((it, sink) -> {
90+
return ParamWriter.publish(context.isNoBackslashEscapes(), query, parameters).handle((it, sink) -> {
9191
ByteBuf buf = allocator.buffer();
9292

9393
try {

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/ConnectionIntegrationTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import java.util.Collections;
5151
import java.util.Objects;
5252
import java.util.function.Function;
53+
import java.util.stream.Stream;
5354

5455
import static io.r2dbc.spi.IsolationLevel.READ_COMMITTED;
5556
import static io.r2dbc.spi.IsolationLevel.READ_UNCOMMITTED;
@@ -80,6 +81,53 @@ void isInTransaction() {
8081
.doOnSuccess(ignored -> assertThat(connection.context().isInTransaction()).isFalse()));
8182
}
8283

84+
@ParameterizedTest
85+
@ValueSource(strings = {
86+
"test",
87+
"test`data",
88+
"test\ndata",
89+
"I'm feeling good",
90+
})
91+
void sqlModeNoBackslashEscapes(String value) {
92+
String tdl = "CREATE TEMPORARY TABLE `test` (`id` INT NOT NULL PRIMARY KEY, `value` VARCHAR(50) NOT NULL)";
93+
94+
// Add NO_BACKSLASH_ESCAPES instead of replace
95+
castedComplete(connection -> Mono.fromRunnable(() -> assertThat(connection.context().isNoBackslashEscapes())
96+
.isFalse())
97+
.thenMany(connection.createStatement(tdl).execute())
98+
.flatMap(MySqlResult::getRowsUpdated)
99+
.thenMany(connection.createStatement("INSERT INTO test VALUES (1, ?)")
100+
.bind(0, value)
101+
.execute())
102+
.flatMap(MySqlResult::getRowsUpdated)
103+
.thenMany(connection.createStatement("SELECT COUNT(0) FROM `test` WHERE `value` = ?")
104+
.bind(0, value)
105+
.execute())
106+
.flatMap(result -> result.map((row, metadata) -> row.get(0, Integer.class)))
107+
.collectList()
108+
.doOnNext(counts -> assertThat(counts).isEqualTo(Collections.singletonList(1)))
109+
.thenMany(connection.createStatement("SELECT @@sql_mode").execute())
110+
.flatMap(result -> result.map((row, metadata) -> row.get(0, String.class)))
111+
.map(modes -> Stream.concat(Stream.of(modes.split(",")), Stream.of("NO_BACKSLASH_ESCAPES"))
112+
.toArray(String[]::new))
113+
.last()
114+
.flatMapMany(modes -> connection.createStatement("SET sql_mode = ?")
115+
.bind(0, modes)
116+
.execute())
117+
.flatMap(MySqlResult::getRowsUpdated)
118+
.doOnComplete(() -> assertThat(connection.context().isNoBackslashEscapes()).isTrue())
119+
.thenMany(connection.createStatement("INSERT INTO test VALUES (2, ?)")
120+
.bind(0, value)
121+
.execute())
122+
.flatMap(MySqlResult::getRowsUpdated)
123+
.thenMany(connection.createStatement("SELECT COUNT(0) FROM `test` WHERE `value` = ?")
124+
.bind(0, value)
125+
.execute())
126+
.flatMap(result -> result.map((row, metadata) -> row.get(0, Integer.class)))
127+
.collectList()
128+
.doOnNext(counts -> assertThat(counts).isEqualTo(Collections.singletonList(2))));
129+
}
130+
83131
@DisabledIf("envIsLessThanMySql56")
84132
@Test
85133
void startTransaction() {

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/QueryIntegrationTestSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -564,7 +564,7 @@ void insertOnDuplicate() {
564564
.bind(2, 20)
565565
.execute())
566566
.flatMap(IntegrationTestSupport::extractRowsUpdated)
567-
.doOnNext(it -> assertThat(it).isOne()) // TODO: check capability flag
567+
.doOnNext(it -> assertThat(it).isOne())
568568
.thenMany(connection.createStatement("SELECT value FROM test WHERE id=?")
569569
.bind(0, 1)
570570
.execute())

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/CodecTestSupport.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ default void encodeStringify() {
9191
Query query = Query.parse("?");
9292

9393
for (int i = 0; i < origin.length; ++i) {
94-
ParameterWriter writer = ParameterWriterHelper.get(query);
94+
ParameterWriter writer = ParameterWriterHelper.get(false, query);
9595
codec.encode(origin[i], context())
9696
.publishText(writer)
9797
.as(StepVerifier::create)

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/codec/SetCodecTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ void stringifySet() {
129129
Query query = Query.parse("?");
130130

131131
for (int i = 0; i < sets.length; ++i) {
132-
ParameterWriter writer = ParameterWriterHelper.get(query);
132+
ParameterWriter writer = ParameterWriterHelper.get(false, query);
133133
codec.encode(sets[i], context())
134134
.publishText(writer)
135135
.as(StepVerifier::create)

r2dbc-mysql/src/test/java/io/asyncer/r2dbc/mysql/message/client/ParamWriterTest.java

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import io.asyncer.r2dbc.mysql.Query;
2020
import org.junit.jupiter.api.Test;
21+
import org.junit.jupiter.params.ParameterizedTest;
22+
import org.junit.jupiter.params.provider.ValueSource;
2123
import reactor.core.publisher.Flux;
2224
import reactor.test.StepVerifier;
2325

@@ -84,42 +86,42 @@ void badFollowNull() {
8486

8587
@Test
8688
void appendPart() {
87-
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
89+
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
8890
writer.append("define", 2, 5);
8991
assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'fin'");
9092
}
9193

9294
@Test
9395
void writePart() {
94-
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
96+
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
9597
writer.write("define", 2, 3);
9698
assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'fin'");
9799
}
98100

99101
@Test
100102
void appendNull() {
101-
assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(parameterOnly(1)).append(null)))
103+
assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(false, parameterOnly(1)).append(null)))
102104
.isEqualTo("'null'");
103-
assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(parameterOnly(1))
105+
assertThat(ParameterWriterHelper.toSql(ParameterWriterHelper.get(false, parameterOnly(1))
104106
.append(null, 1, 3)))
105107
.isEqualTo("'ul'");
106108
}
107109

108110
@Test
109111
void writeNull() {
110-
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
112+
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
111113
writer.write((String) null);
112114
assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'null'");
113115

114-
writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
116+
writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
115117
writer.write((String) null, 1, 2);
116118
assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'ul'");
117119

118-
writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
120+
writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
119121
writer.write((char[]) null);
120122
assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'null'");
121123

122-
writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
124+
writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
123125
writer.write((char[]) null, 1, 2);
124126
assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'ul'");
125127
}
@@ -132,7 +134,7 @@ void publishSuccess() {
132134
values[i] = new MockMySqlParameter(true);
133135
}
134136

135-
Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values)))
137+
Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values)))
136138
.as(StepVerifier::create)
137139
.expectNext(new String(new char[SIZE]).replace("\0", "''"))
138140
.verifyComplete();
@@ -154,7 +156,7 @@ void publishPartially() {
154156
values[i] = new MockMySqlParameter(false);
155157
}
156158

157-
Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values)))
159+
Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values)))
158160
.as(StepVerifier::create)
159161
.verifyError(MockException.class);
160162

@@ -169,13 +171,30 @@ void publishNothing() {
169171
values[i] = new MockMySqlParameter(false);
170172
}
171173

172-
Flux.from(ParamWriter.publish(parameterOnly(SIZE), Flux.fromArray(values)))
174+
Flux.from(ParamWriter.publish(false, parameterOnly(SIZE), Flux.fromArray(values)))
173175
.as(StepVerifier::create)
174176
.verifyError(MockException.class);
175177

176178
assertThat(values).extracting(MockMySqlParameter::refCnt).containsOnly(0);
177179
}
178180

181+
@ParameterizedTest
182+
@ValueSource(strings = {
183+
"abc",
184+
"a'b'c",
185+
"a\nb\rc",
186+
"a\"b\"c",
187+
"a\\b\\c",
188+
"a\0b\0c",
189+
"a\u00a5b\u20a9c",
190+
"a\032b\032c",
191+
})
192+
void noBackslashEscapes(String value) {
193+
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(true, parameterOnly(1));
194+
writer.write(value);
195+
assertThat(ParameterWriterHelper.toSql(writer)).isEqualTo("'" + value.replaceAll("'", "''") + "'");
196+
}
197+
179198
private static Query parameterOnly(int parameters) {
180199
char[] chars = new char[parameters];
181200
Arrays.fill(chars, '?');
@@ -184,13 +203,13 @@ private static Query parameterOnly(int parameters) {
184203
}
185204

186205
private static ParamWriter stringWriter() {
187-
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
206+
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
188207
writer.write('0');
189208
return writer;
190209
}
191210

192211
private static ParamWriter nullWriter() {
193-
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(parameterOnly(1));
212+
ParamWriter writer = (ParamWriter) ParameterWriterHelper.get(false, parameterOnly(1));
194213
writer.writeNull();
195214
return writer;
196215
}

0 commit comments

Comments
 (0)