Skip to content

Commit 182ef8e

Browse files
committed
Add protocol support for query attributes.
This sends an empty set of attributes with each query. Signed-off-by: Bradley Grainger <[email protected]>
1 parent 13235bb commit 182ef8e

11 files changed

+115
-11
lines changed

src/MySqlConnector/Core/ConcatenatedCommandPayloadCreator.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,18 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict
1414
return false;
1515

1616
writer.Write((byte) CommandKind.Query);
17+
18+
if (commandListPosition.Commands[commandListPosition.CommandIndex].Connection!.Session.SupportsQueryAttributes)
19+
{
20+
// attribute count
21+
writer.WriteLengthEncodedInteger(0);
22+
23+
// attribute set count (always 1)
24+
writer.Write((byte) 1);
25+
26+
// TODO: write attributes for all commands
27+
}
28+
1729
bool isComplete;
1830
do
1931
{

src/MySqlConnector/Core/IMySqlCommand.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal interface IMySqlCommand
1010
bool AllowUserVariables { get; }
1111
CommandBehavior CommandBehavior { get; }
1212
MySqlParameterCollection? RawParameters { get; }
13+
MySqlAttributeCollection? RawAttributes { get; }
1314
PreparedStatements? TryGetPreparedStatements();
1415
MySqlConnection? Connection { get; }
1516
long LastInsertedId { get; }

src/MySqlConnector/Core/ServerSession.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public ServerSession(ConnectionPool? pool, int poolGeneration, int id)
6161
public WeakReference<MySqlConnection>? OwningConnection { get; set; }
6262
public bool SupportsComMulti => m_supportsComMulti;
6363
public bool SupportsDeprecateEof => m_supportsDeprecateEof;
64+
public bool SupportsQueryAttributes { get; private set; }
6465
public bool SupportsSessionTrack => m_supportsSessionTrack;
6566
public bool ProcAccessDenied { get; set; }
6667
public ICollection<KeyValuePair<string, object?>> ActivityTags => m_activityTags;
@@ -315,7 +316,7 @@ public void FinishQuerying()
315316
// In order to handle this case, we issue a dummy query that will consume the pending cancellation.
316317
// See https://bugs.mysql.com/bug.php?id=45679
317318
Log.Debug("Session{0} sending 'DO SLEEP(0)' command to clear pending cancellation", m_logArguments);
318-
var payload = QueryPayload.Create("DO SLEEP(0);");
319+
var payload = QueryPayload.Create(SupportsQueryAttributes, "DO SLEEP(0);");
319320
#pragma warning disable CA2012 // Safe because method completes synchronously
320321
SendAsync(payload, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
321322
payload = ReceiveReplyAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
@@ -463,10 +464,13 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
463464
m_supportsComMulti = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.MariaDbComMulti) != 0;
464465
m_supportsConnectionAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.ConnectionAttributes) != 0;
465466
m_supportsDeprecateEof = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.DeprecateEof) != 0;
467+
SupportsQueryAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.QueryAttributes) != 0;
466468
m_supportsSessionTrack = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SessionTrack) != 0;
467469
var serverSupportsSsl = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Ssl) != 0;
468470
m_characterSet = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? CharacterSet.Utf8Mb4GeneralCaseInsensitive : CharacterSet.Utf8GeneralCaseInsensitive;
469-
m_setNamesPayload = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? s_setNamesUtf8mb4Payload : s_setNamesUtf8Payload;
471+
m_setNamesPayload = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ?
472+
(SupportsQueryAttributes ? s_setNamesUtf8mb4WithAttributesPayload : s_setNamesUtf8mb4NoAttributesPayload) :
473+
(SupportsQueryAttributes ? s_setNamesUtf8WithAttributesPayload : s_setNamesUtf8NoAttributesPayload);
470474

471475
// disable pipelining for RDS MySQL 5.7 (assuming Aurora); otherwise take it from the connection string or default to true
472476
if (!cs.Pipelining.HasValue && ServerVersion.Version.Major == 5 && ServerVersion.Version.Minor == 7 && HostName.EndsWith(".rds.amazonaws.com", StringComparison.OrdinalIgnoreCase))
@@ -495,9 +499,9 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
495499
}
496500
}
497501

498-
Log.Debug("Session{0} made connection; ServerVersion={1}; ConnectionId={2}; Compression={3}; Attributes={4}; DeprecateEof={5}; Ssl={6}; SessionTrack={7}; Pipelining={8}",
502+
Log.Debug("Session{0} made connection; ServerVersion={1}; ConnectionId={2}; Compression={3}; Attributes={4}; DeprecateEof={5}; Ssl={6}; SessionTrack={7}; Pipelining={8}; QueryAttributes={9}",
499503
m_logArguments[0], ServerVersion.OriginalString, ConnectionId,
500-
m_useCompression, m_supportsConnectionAttributes, m_supportsDeprecateEof, serverSupportsSsl, m_supportsSessionTrack, m_supportsPipelining);
504+
m_useCompression, m_supportsConnectionAttributes, m_supportsDeprecateEof, serverSupportsSsl, m_supportsSessionTrack, m_supportsPipelining, SupportsQueryAttributes);
501505

502506
if (cs.SslMode != MySqlSslMode.None && (cs.SslMode != MySqlSslMode.Preferred || serverSupportsSsl))
503507
{
@@ -1633,7 +1637,7 @@ private async Task GetRealServerDetailsAsync(IOBehavior ioBehavior, Cancellation
16331637
Log.Debug("Session{0} detected proxy; getting CONNECTION_ID(), VERSION() from server", m_logArguments);
16341638
try
16351639
{
1636-
await SendAsync(QueryPayload.Create("SELECT CONNECTION_ID(), VERSION();"), ioBehavior, cancellationToken).ConfigureAwait(false);
1640+
await SendAsync(QueryPayload.Create(SupportsQueryAttributes, "SELECT CONNECTION_ID(), VERSION();"), ioBehavior, cancellationToken).ConfigureAwait(false);
16371641

16381642
// column count: 2
16391643
await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -1908,8 +1912,10 @@ protected override void OnStatementBegin(int index)
19081912

19091913
static ReadOnlySpan<byte> BeginCertificateBytes => new byte[] { 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45 }; // -----BEGIN CERTIFICATE-----
19101914
static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ServerSession));
1911-
static readonly PayloadData s_setNamesUtf8Payload = QueryPayload.Create("SET NAMES utf8;");
1912-
static readonly PayloadData s_setNamesUtf8mb4Payload = QueryPayload.Create("SET NAMES utf8mb4;");
1915+
static readonly PayloadData s_setNamesUtf8NoAttributesPayload = QueryPayload.Create(false, "SET NAMES utf8;");
1916+
static readonly PayloadData s_setNamesUtf8mb4NoAttributesPayload = QueryPayload.Create(false, "SET NAMES utf8mb4;");
1917+
static readonly PayloadData s_setNamesUtf8WithAttributesPayload = QueryPayload.Create(true, "SET NAMES utf8;");
1918+
static readonly PayloadData s_setNamesUtf8mb4WithAttributesPayload = QueryPayload.Create(true, "SET NAMES utf8mb4;");
19131919
static int s_lastId;
19141920

19151921
readonly object m_lock;

src/MySqlConnector/Core/SingleCommandPayloadCreator.cs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict
2626
Log.Trace("Session{0} Preparing command payload; CommandText: {1}", command.Connection!.Session.Id, command.CommandText);
2727

2828
writer.Write((byte) CommandKind.Query);
29+
var supportsQueryAttributes = command.Connection!.Session.SupportsQueryAttributes;
30+
if (supportsQueryAttributes)
31+
{
32+
// attribute count
33+
writer.WriteLengthEncodedInteger((uint) (command.RawAttributes?.Count ?? 0));
34+
35+
// attribute set count (always 1)
36+
writer.Write((byte) 1);
37+
38+
// TODO: write attributes
39+
}
40+
else if (command.RawAttributes?.Count > 0)
41+
{
42+
Log.Warn("Session{0} has query attributes but server doesn't support them; CommandText: {1}", command.Connection!.Session.Id, command.CommandText);
43+
}
44+
2945
WriteQueryPayload(command, cachedProcedures, writer);
3046

3147
commandListPosition.CommandIndex++;
@@ -62,9 +78,26 @@ private static void WritePreparedStatement(IMySqlCommand command, PreparedStatem
6278
if (Log.IsTraceEnabled())
6379
Log.Trace("Session{0} Preparing command payload; CommandId: {1}; CommandText: {2}", command.Connection!.Session.Id, preparedStatement.StatementId, command.CommandText);
6480

81+
var attributes = command.RawAttributes;
82+
var supportsQueryAttributes = command.Connection!.Session.SupportsQueryAttributes;
83+
6584
writer.Write(preparedStatement.StatementId);
66-
writer.Write((byte) 0);
85+
86+
// NOTE: documentation is not updated yet, but due to bugs in MySQL Server 8.0.23-8.0.25, the PARAMETER_COUNT_AVAILABLE (0x08)
87+
// flag has to be set in the 'flags' block in order for query attributes to be sent with a prepared statement; we do not version-sniff the
88+
// server but assume that it must support this flag
89+
writer.Write((byte) (supportsQueryAttributes ? 8 : 0));
6790
writer.Write(1);
91+
92+
if (supportsQueryAttributes)
93+
{
94+
writer.WriteLengthEncodedInteger((uint) ((attributes?.Count ?? 0) + (preparedStatement.Parameters?.Length ?? 0)));
95+
}
96+
else if (attributes?.Count > 0)
97+
{
98+
Log.Warn("Session{0} has attributes for CommandId {1} but the server does not support them", command.Connection!.Session.Id, preparedStatement.StatementId);
99+
}
100+
68101
if (preparedStatement.Parameters?.Length > 0)
69102
{
70103
// TODO: How to handle incorrect number of parameters?
@@ -114,6 +147,9 @@ private static void WritePreparedStatement(IMySqlCommand command, PreparedStatem
114147
}
115148

116149
writer.Write(TypeMapper.ConvertToColumnTypeAndFlags(mySqlDbType, command.Connection!.GuidFormat));
150+
151+
if (supportsQueryAttributes)
152+
writer.Write((byte) 0); // empty string
117153
}
118154

119155
var options = command.CreateStatementPreparerOptions();

src/MySqlConnector/MySqlAttribute.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace MySqlConnector
2+
{
3+
public sealed class MySqlAttribute
4+
{
5+
public string AttributeName { get; set; } = "";
6+
7+
public object? Value { get; set; }
8+
}
9+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using System.Collections;
2+
3+
namespace MySqlConnector
4+
{
5+
public sealed class MySqlAttributeCollection : ICollection<MySqlAttribute>
6+
{
7+
public int Count => m_attributes.Count;
8+
public bool IsReadOnly => false;
9+
public void Add(MySqlAttribute item) => throw new NotImplementedException();
10+
public void Clear() => throw new NotImplementedException();
11+
public bool Contains(MySqlAttribute item) => throw new NotImplementedException();
12+
public void CopyTo(MySqlAttribute[] array, int arrayIndex) => throw new NotImplementedException();
13+
public IEnumerator<MySqlAttribute> GetEnumerator() => m_attributes.GetEnumerator();
14+
public bool Remove(MySqlAttribute item) => throw new NotImplementedException();
15+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
16+
17+
internal MySqlAttributeCollection() => m_attributes = new();
18+
19+
private readonly List<MySqlAttribute> m_attributes;
20+
}
21+
}

src/MySqlConnector/MySqlBatchCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public MySqlBatchCommand(string? commandText)
5353

5454
MySqlParameterCollection? IMySqlCommand.RawParameters => m_parameterCollection;
5555

56+
MySqlAttributeCollection? IMySqlCommand.RawAttributes => null;
57+
5658
MySqlConnection? IMySqlCommand.Connection => Batch?.Connection;
5759

5860
long IMySqlCommand.LastInsertedId => m_lastInsertedId;

src/MySqlConnector/MySqlCommand.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ private MySqlCommand(MySqlCommand other)
7575
DesignTimeVisible = other.DesignTimeVisible;
7676
UpdatedRowSource = other.UpdatedRowSource;
7777
m_parameterCollection = other.CloneRawParameters();
78+
//// TODO: clone attributes
7879
}
7980

8081
/// <summary>
@@ -84,6 +85,13 @@ private MySqlCommand(MySqlCommand other)
8485

8586
MySqlParameterCollection? IMySqlCommand.RawParameters => m_parameterCollection;
8687

88+
/// <summary>
89+
/// The collection of <see cref="MySqlAttribute"/> objects for this command.
90+
/// </summary>
91+
public MySqlAttributeCollection Attributes => m_attributeCollection ??= new();
92+
93+
MySqlAttributeCollection? IMySqlCommand.RawAttributes => m_attributeCollection;
94+
8795
public new MySqlParameter CreateParameter() => (MySqlParameter) base.CreateParameter();
8896

8997
/// <inheritdoc/>
@@ -421,6 +429,7 @@ private bool IsValid([NotNullWhen(false)] out Exception? exception)
421429
MySqlConnection? m_connection;
422430
string m_commandText;
423431
MySqlParameterCollection? m_parameterCollection;
432+
MySqlAttributeCollection? m_attributeCollection;
424433
int? m_commandTimeout;
425434
CommandType m_commandType;
426435
CommandBehavior m_commandBehavior;

src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s
2626
(serverCapabilities & ProtocolCapabilities.ConnectionAttributes) |
2727
(serverCapabilities & ProtocolCapabilities.SessionTrack) |
2828
(serverCapabilities & ProtocolCapabilities.DeprecateEof) |
29+
(serverCapabilities & ProtocolCapabilities.QueryAttributes) |
2930
additionalCapabilities));
3031
writer.Write(0x4000_0000);
3132
writer.Write((byte) characterSet);

src/MySqlConnector/Protocol/Payloads/QueryPayload.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ namespace MySqlConnector.Protocol.Payloads;
44

55
internal static class QueryPayload
66
{
7-
public static PayloadData Create(string query)
7+
public static PayloadData Create(bool supportsQueryAttributes, string query)
88
{
99
var length = Encoding.UTF8.GetByteCount(query);
10-
var payload = new byte[length + 1];
10+
var payload = new byte[length + 1 + (supportsQueryAttributes ? 2 : 0)];
1111
payload[0] = (byte) CommandKind.Query;
12-
Encoding.UTF8.GetBytes(query, 0, query.Length, payload, 1);
12+
if (supportsQueryAttributes)
13+
payload[2] = 1;
14+
Encoding.UTF8.GetBytes(query, 0, query.Length, payload, supportsQueryAttributes ? 3 : 1);
1315
return new PayloadData(payload);
1416
}
1517
}

0 commit comments

Comments
 (0)