Skip to content

Commit

Permalink
Fix StringBuilder MySqlParameter support for .NET 5.0. Fixes #977
Browse files Browse the repository at this point in the history
- Use StringBuilder.GetChunks for binary parameters
- Fix UTF-8 encoding for surrogate code units split across chunks
  • Loading branch information
bgrainger committed May 26, 2021
1 parent 8ee4b84 commit f1ad497
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 12 deletions.
2 changes: 2 additions & 0 deletions docs/content/tutorials/migrating-from-connector-net.md
Expand Up @@ -282,3 +282,5 @@ The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~
* ~~[#101714](https://bugs.mysql.com/bug.php?id=101714): Extremely slow performance reading result sets~~
* [#102593](https://bugs.mysql.com/bug.php?id=102593): Can't use `MemoryStream` as `MySqlParameter.Value`
* [#103390](https://bugs.mysql.com/bug.php?id=103390): Can't query `CHAR(36)` column if `MySqlCommand` is prepared
* [#103801](https://bugs.mysql.com/bug.php?id=103801): `TimeSpan` parameters lose microseconds with prepared statement
* [#103819](https://bugs.mysql.com/bug.php?id=103819): Can't use `StringBuilder` containing non-BMP characters as `MySqlParameter.Value`
8 changes: 5 additions & 3 deletions src/MySqlConnector/MySqlParameter.cs
Expand Up @@ -413,6 +413,8 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
writer.Write((byte) '\'');
foreach (var chunk in stringBuilder.GetChunks())
WriteString(writer, noBackslashEscapes, writeDelimiters: false, chunk.Span);
if (stringBuilder.Length != 0)
writer.Write("".AsSpan(), flush: true);
writer.Write((byte) '\'');
#elif NET45 || NETSTANDARD1_3
WriteString(writer, noBackslashEscapes, stringBuilder.ToString());
Expand Down Expand Up @@ -483,13 +485,13 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w
if (nextDelimiterIndex == -1)
{
// write the rest of the string
writer.Write(remainingValue);
writer.Write(remainingValue, flush: writeDelimiters);
charsWritten += remainingValue.Length;
}
else
{
// write up to (and including) the delimiter, then double it
writer.Write(remainingValue.Slice(0, nextDelimiterIndex + 1));
writer.Write(remainingValue.Slice(0, nextDelimiterIndex + 1), flush: true);
if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes)
writer.Write((byte) '\\');
else if (remainingValue[nextDelimiterIndex] == '\'')
Expand Down Expand Up @@ -671,7 +673,7 @@ internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions opt
#endif
else if (Value is StringBuilder stringBuilder)
{
writer.WriteLengthEncodedString(stringBuilder.ToString());
writer.WriteLengthEncodedString(stringBuilder);
}
else if (MySqlDbType == MySqlDbType.Int16)
{
Expand Down
60 changes: 55 additions & 5 deletions src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs
Expand Up @@ -130,26 +130,76 @@ public unsafe void Write(string value, int offset, int length)
}
}
#else
public void Write(string value) => Write(value.AsSpan());
public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length));
public void Write(string value) => Write(value.AsSpan(), flush: true);
public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length), flush: true);

public void Write(ReadOnlySpan<char> chars)
public void Write(ReadOnlySpan<char> chars, bool flush)
{
m_encoder ??= Encoding.UTF8.GetEncoder();
while (chars.Length > 0)
{
if (m_output.Length < 4)
Reallocate();
m_encoder.Convert(chars, m_output.Span, true, out var charsUsed, out var bytesUsed, out var completed);
m_encoder.Convert(chars, m_output.Span, flush: false, out var charsUsed, out var bytesUsed, out var completed);
chars = chars.Slice(charsUsed);
m_output = m_output.Slice(bytesUsed);
if (!completed)
Reallocate();
Debug.Assert(completed == (chars.Length == 0));
}

if (flush && m_encoder is not null)
{
if (m_output.Length < 4)
Reallocate();
m_encoder.Convert("".AsSpan(), m_output.Span, flush: true, out _, out var bytesUsed, out _);
m_output = m_output.Slice(bytesUsed);
}
}
#endif

public void WriteLengthEncodedString(StringBuilder stringBuilder)
{
#if !NET45 && !NET461 && !NET471 && !NETSTANDARD1_3 && !NETSTANDARD2_0 && !NETSTANDARD2_1 && !NETCOREAPP2_1
// save where the length will be written
var lengthPosition = Position;
if (m_output.Length < 9)
Reallocate(9);
Advance(9);

// write all the text as UTF-8
m_encoder ??= Encoding.UTF8.GetEncoder();
foreach (var chunk in stringBuilder.GetChunks())
{
var currentSpan = chunk.Span;
while (currentSpan.Length > 0)
{
if (m_output.Length < 4)
Reallocate();
m_encoder.Convert(currentSpan, m_output.Span, false, out var charsUsed, out var bytesUsed, out var completed);
currentSpan = currentSpan.Slice(charsUsed);
m_output = m_output.Slice(bytesUsed);
if (!completed)
Reallocate();
Debug.Assert(completed == (currentSpan.Length == 0));
}
}

// flush the output
if (m_output.Length < 4)
Reallocate();
m_encoder.Convert("".AsSpan(), m_output.Span, true, out _, out var finalBytesUsed, out _);
m_output = m_output.Slice(finalBytesUsed);

// write the length (as a 64-bit integer) in the reserved space
var textLength = Position - (lengthPosition + 9);
m_buffer[lengthPosition] = 0xFE;
BinaryPrimitives.WriteUInt64LittleEndian(m_buffer.AsSpan(lengthPosition + 1), (ulong) textLength);
#else
this.WriteLengthEncodedString(stringBuilder.ToString());
#endif
}

public void WriteString(short value)
{
int bytesWritten;
Expand Down Expand Up @@ -255,7 +305,7 @@ public static void WriteLengthEncodedString(this ByteBufferWriter writer, ReadOn
{
var byteCount = Encoding.UTF8.GetByteCount(value);
writer.WriteLengthEncodedInteger((ulong) byteCount);
writer.Write(value);
writer.Write(value, flush: true);
}
#endif

Expand Down
14 changes: 10 additions & 4 deletions tests/SideBySide/InsertTests.cs
Expand Up @@ -249,7 +249,7 @@ public void InsertMemoryStream(bool prepare)
Assert.Equal(new byte[] { 97, 98, 99, 100 }, reader.GetValue(1));
}

[Theory]
[SkippableTheory(Baseline = "https://bugs.mysql.com/bug.php?id=103819")]
[InlineData(false)]
[InlineData(true)]
public void InsertStringBuilder(bool prepare)
Expand All @@ -259,17 +259,23 @@ public void InsertStringBuilder(bool prepare)
connection.Execute(@"drop table if exists insert_string_builder;
create table insert_string_builder(rowid integer not null primary key auto_increment, str text);");

var value = "\aAB\\12'ab\\'\\'";
var value = new StringBuilder("\aAB\\12'ab\\'\\'");
for (var i = 0; i < 100; i++)
value.Append("\U0001F600\uD800\'\U0001F601\uD800");

using var cmd = connection.CreateCommand();
cmd.CommandText = @"insert into insert_string_builder(str) values(@str);";
cmd.Parameters.AddWithValue("@str", new StringBuilder(value));
cmd.Parameters.AddWithValue("@str", value);
if (prepare)
cmd.Prepare();
cmd.ExecuteNonQuery();

using var reader = connection.ExecuteReader(@"select str from insert_string_builder order by rowid;");
Assert.True(reader.Read());
Assert.Equal(value, reader.GetValue(0));

// all unpaired high-surrogates will be converted to the Unicode Replacement Character when converted to UTF-8 to be transmitted to the server
var expected = value.ToString().Replace('\uD800', '\uFFFD');
Assert.Equal(expected, reader.GetValue(0));
}

[Fact]
Expand Down

0 comments on commit f1ad497

Please sign in to comment.