Skip to content

Commit ddc733b

Browse files
committed
Fix StringBuilder MySqlParameter support for .NET 5.0. Fixes #977
- Use StringBuilder.GetChunks for binary parameters - Fix UTF-8 encoding for surrogate code units split across chunks
1 parent 8ee4b84 commit ddc733b

File tree

4 files changed

+78
-13
lines changed

4 files changed

+78
-13
lines changed

docs/content/tutorials/migrating-from-connector-net.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,3 +282,5 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~
282282
* ~~[#101714](https://bugs.mysql.com/bug.php?id=101714): Extremely slow performance reading result sets~~
283283
* [#102593](https://bugs.mysql.com/bug.php?id=102593): Can't use `MemoryStream` as `MySqlParameter.Value`
284284
* [#103390](https://bugs.mysql.com/bug.php?id=103390): Can't query `CHAR(36)` column if `MySqlCommand` is prepared
285+
* [#103801](https://bugs.mysql.com/bug.php?id=103801): `TimeSpan` parameters lose microseconds with prepared statement
286+
* [#103819](https://bugs.mysql.com/bug.php?id=103819): Can't use `StringBuilder` containing non-BMP characters as `MySqlParameter.Value`

src/MySqlConnector/MySqlParameter.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,8 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
413413
writer.Write((byte) '\'');
414414
foreach (var chunk in stringBuilder.GetChunks())
415415
WriteString(writer, noBackslashEscapes, writeDelimiters: false, chunk.Span);
416+
if (stringBuilder.Length != 0)
417+
writer.Write("".AsSpan(), flush: true);
416418
writer.Write((byte) '\'');
417419
#elif NET45 || NETSTANDARD1_3
418420
WriteString(writer, noBackslashEscapes, stringBuilder.ToString());
@@ -483,13 +485,13 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w
483485
if (nextDelimiterIndex == -1)
484486
{
485487
// write the rest of the string
486-
writer.Write(remainingValue);
488+
writer.Write(remainingValue, flush: writeDelimiters);
487489
charsWritten += remainingValue.Length;
488490
}
489491
else
490492
{
491493
// write up to (and including) the delimiter, then double it
492-
writer.Write(remainingValue.Slice(0, nextDelimiterIndex + 1));
494+
writer.Write(remainingValue.Slice(0, nextDelimiterIndex + 1), flush: true);
493495
if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes)
494496
writer.Write((byte) '\\');
495497
else if (remainingValue[nextDelimiterIndex] == '\'')
@@ -671,7 +673,7 @@ internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions opt
671673
#endif
672674
else if (Value is StringBuilder stringBuilder)
673675
{
674-
writer.WriteLengthEncodedString(stringBuilder.ToString());
676+
writer.WriteLengthEncodedString(stringBuilder);
675677
}
676678
else if (MySqlDbType == MySqlDbType.Int16)
677679
{

src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -130,26 +130,76 @@ public unsafe void Write(string value, int offset, int length)
130130
}
131131
}
132132
#else
133-
public void Write(string value) => Write(value.AsSpan());
134-
public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length));
133+
public void Write(string value) => Write(value.AsSpan(), flush: true);
134+
public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length), flush: true);
135135

136-
public void Write(ReadOnlySpan<char> chars)
136+
public void Write(ReadOnlySpan<char> chars, bool flush)
137137
{
138138
m_encoder ??= Encoding.UTF8.GetEncoder();
139139
while (chars.Length > 0)
140140
{
141141
if (m_output.Length < 4)
142142
Reallocate();
143-
m_encoder.Convert(chars, m_output.Span, true, out var charsUsed, out var bytesUsed, out var completed);
143+
m_encoder.Convert(chars, m_output.Span, flush: false, out var charsUsed, out var bytesUsed, out var completed);
144144
chars = chars.Slice(charsUsed);
145145
m_output = m_output.Slice(bytesUsed);
146146
if (!completed)
147147
Reallocate();
148148
Debug.Assert(completed == (chars.Length == 0));
149149
}
150+
151+
if (flush && m_encoder is not null)
152+
{
153+
if (m_output.Length < 4)
154+
Reallocate();
155+
m_encoder.Convert("".AsSpan(), m_output.Span, flush: true, out _, out var bytesUsed, out _);
156+
m_output = m_output.Slice(bytesUsed);
157+
}
150158
}
151159
#endif
152160

161+
public void WriteLengthEncodedString(StringBuilder stringBuilder)
162+
{
163+
#if !NET45 && !NET461 && !NET471 && !NETSTANDARD1_3 && !NETSTANDARD2_0 && !NETSTANDARD2_1 && !NETCOREAPP2_1
164+
// save where the length will be written
165+
var lengthPosition = Position;
166+
if (m_output.Length < 9)
167+
Reallocate(9);
168+
Advance(9);
169+
170+
// write all the text as UTF-8
171+
m_encoder ??= Encoding.UTF8.GetEncoder();
172+
foreach (var chunk in stringBuilder.GetChunks())
173+
{
174+
var currentSpan = chunk.Span;
175+
while (currentSpan.Length > 0)
176+
{
177+
if (m_output.Length < 4)
178+
Reallocate();
179+
m_encoder.Convert(currentSpan, m_output.Span, false, out var charsUsed, out var bytesUsed, out var completed);
180+
currentSpan = currentSpan.Slice(charsUsed);
181+
m_output = m_output.Slice(bytesUsed);
182+
if (!completed)
183+
Reallocate();
184+
Debug.Assert(completed == (currentSpan.Length == 0));
185+
}
186+
}
187+
188+
// flush the output
189+
if (m_output.Length < 4)
190+
Reallocate();
191+
m_encoder.Convert("".AsSpan(), m_output.Span, true, out _, out var finalBytesUsed, out _);
192+
m_output = m_output.Slice(finalBytesUsed);
193+
194+
// write the length (as a 64-bit integer) in the reserved space
195+
var textLength = Position - (lengthPosition + 9);
196+
m_buffer[lengthPosition] = 0xFE;
197+
BinaryPrimitives.WriteUInt64LittleEndian(m_buffer.AsSpan(lengthPosition + 1), (ulong) textLength);
198+
#else
199+
this.WriteLengthEncodedString(stringBuilder.ToString());
200+
#endif
201+
}
202+
153203
public void WriteString(short value)
154204
{
155205
int bytesWritten;
@@ -255,7 +305,7 @@ public static void WriteLengthEncodedString(this ByteBufferWriter writer, ReadOn
255305
{
256306
var byteCount = Encoding.UTF8.GetByteCount(value);
257307
writer.WriteLengthEncodedInteger((ulong) byteCount);
258-
writer.Write(value);
308+
writer.Write(value, flush: true);
259309
}
260310
#endif
261311

tests/SideBySide/InsertTests.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,27 +249,38 @@ public void InsertMemoryStream(bool prepare)
249249
Assert.Equal(new byte[] { 97, 98, 99, 100 }, reader.GetValue(1));
250250
}
251251

252-
[Theory]
252+
[SkippableTheory(Baseline = "https://bugs.mysql.com/bug.php?id=103819")]
253253
[InlineData(false)]
254254
[InlineData(true)]
255255
public void InsertStringBuilder(bool prepare)
256256
{
257257
using var connection = new MySqlConnection(AppConfig.ConnectionString);
258258
connection.Open();
259259
connection.Execute(@"drop table if exists insert_string_builder;
260-
create table insert_string_builder(rowid integer not null primary key auto_increment, str text);");
260+
create table insert_string_builder(rowid integer not null primary key auto_increment, str text collate utf8mb4_bin);");
261+
262+
var value = new StringBuilder("\aAB\\12'ab\\'\\'");
263+
for (var i = 0; i < 100; i++)
264+
#if !NETCOREAPP3_1
265+
value.Append("\U0001F600\uD800\'\U0001F601\uD800");
266+
#else
267+
// the netcoreapp3.1 implementation is broken when handling an unpaired surrogate; probably https://github.com/dotnet/runtime/issues/33817
268+
value.Append("\U0001F600\U0001F601");
269+
#endif
261270

262-
var value = "\aAB\\12'ab\\'\\'";
263271
using var cmd = connection.CreateCommand();
264272
cmd.CommandText = @"insert into insert_string_builder(str) values(@str);";
265-
cmd.Parameters.AddWithValue("@str", new StringBuilder(value));
273+
cmd.Parameters.AddWithValue("@str", value);
266274
if (prepare)
267275
cmd.Prepare();
268276
cmd.ExecuteNonQuery();
269277

270278
using var reader = connection.ExecuteReader(@"select str from insert_string_builder order by rowid;");
271279
Assert.True(reader.Read());
272-
Assert.Equal(value, reader.GetValue(0));
280+
281+
// all unpaired high-surrogates will be converted to the Unicode Replacement Character when converted to UTF-8 to be transmitted to the server
282+
var expected = value.ToString().Replace('\uD800', '\uFFFD');
283+
Assert.Equal(expected, reader.GetValue(0));
273284
}
274285

275286
[Fact]

0 commit comments

Comments
 (0)