diff --git a/src/MySqlConnector/Core/CompressionMethod.cs b/src/MySqlConnector/Core/CompressionMethod.cs new file mode 100644 index 000000000..bd16884dd --- /dev/null +++ b/src/MySqlConnector/Core/CompressionMethod.cs @@ -0,0 +1,8 @@ +namespace MySqlConnector.Core; + +internal enum CompressionMethod +{ + None, + Zlib, + Zstandard, +} diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index 69a288345..25e36f5a9 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -451,7 +451,9 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella ServerVersion = new(initialHandshake.ServerVersion); ConnectionId = initialHandshake.ConnectionId; AuthPluginData = initialHandshake.AuthPluginData; - m_useCompression = cs.UseCompression && (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Compress) != 0; + m_compressionMethod = !cs.UseCompression ? CompressionMethod.None : + ((initialHandshake.ProtocolCapabilities & ProtocolCapabilities.ZstandardCompressionAlgorithm) != 0 && connection.ZstandardPlugin is not null) ? CompressionMethod.Zstandard : + ((initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Compress) != 0) ? CompressionMethod.Zlib : CompressionMethod.None; CancellationTimeout = cs.CancellationTimeout; UserID = cs.UserID; @@ -483,7 +485,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella else { // pipelining is not currently compatible with compression - m_supportsPipelining = !cs.UseCompression && cs.Pipelining is not false; + m_supportsPipelining = m_compressionMethod == CompressionMethod.None && cs.Pipelining is not false; // for pipelining, concatenate reset connection and SET NAMES query into one buffer if (m_supportsPipelining) @@ -500,7 +502,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } } - Log.SessionMadeConnection(m_logger, Id, ServerVersion.OriginalString, ConnectionId, m_useCompression, m_supportsConnectionAttributes, SupportsDeprecateEof, SupportsCachedPreparedMetadata, serverSupportsSsl, SupportsSessionTrack, m_supportsPipelining, SupportsQueryAttributes); + Log.SessionMadeConnection(m_logger, Id, ServerVersion.OriginalString, ConnectionId, m_compressionMethod != CompressionMethod.None, m_supportsConnectionAttributes, SupportsDeprecateEof, SupportsCachedPreparedMetadata, serverSupportsSsl, SupportsSessionTrack, m_supportsPipelining, SupportsQueryAttributes); if (cs.SslMode != MySqlSslMode.None && (cs.SslMode != MySqlSslMode.Preferred || serverSupportsSsl)) { @@ -517,7 +519,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella cs.ConnectionAttributes = CreateConnectionAttributes(cs.ApplicationName); var password = GetPassword(cs, connection); - using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, m_useCompression, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) + using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null)) await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false); payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false); @@ -570,8 +572,10 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella var redirectionUrl = ok.RedirectionUrl; - if (m_useCompression) + if (m_compressionMethod == CompressionMethod.Zlib) m_payloadHandler = new CompressedPayloadHandler(m_payloadHandler.ByteHandler); + else if (m_compressionMethod == CompressionMethod.Zstandard) + m_payloadHandler = connection.ZstandardPlugin!.CreatePayloadHandler(m_payloadHandler.ByteHandler); // send 'SET NAMES' to set the character set and collation unless the server reports that it's already using the desired character set (e.g., MariaDB >= 11.5) if (ok.NewCharacterSet != (ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? CharacterSet.Utf8Mb4Binary : CharacterSet.Utf8Mb3Binary)) @@ -1632,7 +1636,7 @@ caCertificateChain is not null && var checkCertificateRevocation = cs.SslMode == MySqlSslMode.VerifyFull; - using (var initSsl = HandshakeResponse41Payload.CreateWithSsl(serverCapabilities, cs, m_useCompression, m_characterSet)) + using (var initSsl = HandshakeResponse41Payload.CreateWithSsl(serverCapabilities, cs, m_compressionMethod, m_characterSet)) await SendReplyAsync(initSsl, ioBehavior, cancellationToken).ConfigureAwait(false); var clientAuthenticationOptions = new SslClientAuthenticationOptions @@ -2129,7 +2133,7 @@ protected override void OnStatementBegin(int index) private SslStream? m_sslStream; private X509Certificate2? m_clientCertificate; private IPayloadHandler? m_payloadHandler; - private bool m_useCompression; + private CompressionMethod m_compressionMethod; private bool m_isSecureConnection; private bool m_supportsConnectionAttributes; private bool m_supportsPipelining; diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs index 2d1b30531..bb6cbe795 100644 --- a/src/MySqlConnector/MySqlConnection.cs +++ b/src/MySqlConnector/MySqlConnection.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; using MySqlConnector.Core; using MySqlConnector.Logging; +using MySqlConnector.Plugins; using MySqlConnector.Protocol; using MySqlConnector.Protocol.Payloads; using MySqlConnector.Protocol.Serialization; @@ -979,6 +980,7 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel) internal MySqlTransaction? CurrentTransaction { get; set; } internal MySqlConnectorLoggingConfiguration LoggingConfiguration { get; } + internal ZstandardPlugin? ZstandardPlugin { get; set; } internal bool AllowLoadLocalInfile => GetInitializedConnectionSettings().AllowLoadLocalInfile; internal bool AllowUserVariables => GetInitializedConnectionSettings().AllowUserVariables; internal bool AllowZeroDateTime => GetInitializedConnectionSettings().AllowZeroDateTime; diff --git a/src/MySqlConnector/MySqlConnector.csproj b/src/MySqlConnector/MySqlConnector.csproj index 4cb3425f5..db3ec187d 100644 --- a/src/MySqlConnector/MySqlConnector.csproj +++ b/src/MySqlConnector/MySqlConnector.csproj @@ -35,6 +35,8 @@ + + diff --git a/src/MySqlConnector/MySqlDataSource.cs b/src/MySqlConnector/MySqlDataSource.cs index a975b60c2..0c4372fd4 100644 --- a/src/MySqlConnector/MySqlDataSource.cs +++ b/src/MySqlConnector/MySqlDataSource.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using MySqlConnector.Core; using MySqlConnector.Logging; +using MySqlConnector.Plugins; using MySqlConnector.Protocol.Serialization; namespace MySqlConnector; @@ -18,7 +19,7 @@ public sealed class MySqlDataSource : DbDataSource /// The connection string for the MySQL Server. This parameter is required. /// Thrown if is null. public MySqlDataSource(string connectionString) - : this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default) + : this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, default, default, default) { } @@ -29,7 +30,8 @@ internal MySqlDataSource(string connectionString, RemoteCertificateValidationCallback? remoteCertificateValidationCallback, Func>? periodicPasswordProvider, TimeSpan periodicPasswordProviderSuccessRefreshInterval, - TimeSpan periodicPasswordProviderFailureRefreshInterval) + TimeSpan periodicPasswordProviderFailureRefreshInterval, + ZstandardPlugin? zstandardPlugin) { m_connectionString = connectionString; LoggingConfiguration = loggingConfiguration; @@ -37,6 +39,7 @@ internal MySqlDataSource(string connectionString, m_clientCertificatesCallback = clientCertificatesCallback; m_remoteCertificateValidationCallback = remoteCertificateValidationCallback; m_logger = loggingConfiguration.DataSourceLogger; + m_zstandardPlugin = zstandardPlugin; Pool = ConnectionPool.CreatePool(m_connectionString, LoggingConfiguration, name); m_id = Interlocked.Increment(ref s_lastId); @@ -221,6 +224,7 @@ private string ProvidePasswordFromInitialRefreshTask(MySqlProvidePasswordContext private readonly Func>? m_periodicPasswordProvider; private readonly TimeSpan m_periodicPasswordProviderSuccessRefreshInterval; private readonly TimeSpan m_periodicPasswordProviderFailureRefreshInterval; + private readonly ZstandardPlugin? m_zstandardPlugin; private readonly MySqlProvidePasswordContext? m_providePasswordContext; private readonly CancellationTokenSource? m_passwordProviderTimerCancellationTokenSource; private readonly Timer? m_passwordProviderTimer; diff --git a/src/MySqlConnector/MySqlDataSourceBuilder.cs b/src/MySqlConnector/MySqlDataSourceBuilder.cs index fbe902b40..e1d15d183 100644 --- a/src/MySqlConnector/MySqlDataSourceBuilder.cs +++ b/src/MySqlConnector/MySqlDataSourceBuilder.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Logging; using MySqlConnector.Logging; +using MySqlConnector.Plugins; namespace MySqlConnector; @@ -102,7 +103,8 @@ public MySqlDataSource Build() m_remoteCertificateValidationCallback, m_periodicPasswordProvider, m_periodicPasswordProviderSuccessRefreshInterval, - m_periodicPasswordProviderFailureRefreshInterval + m_periodicPasswordProviderFailureRefreshInterval, + ZstandardPlugin ); } @@ -111,6 +113,8 @@ public MySqlDataSource Build() /// public MySqlConnectionStringBuilder ConnectionStringBuilder { get; } + internal ZstandardPlugin? ZstandardPlugin { get; set; } + private ILoggerFactory? m_loggerFactory; private string? m_name; private Func? m_clientCertificatesCallback; diff --git a/src/MySqlConnector/Plugins/ZstandardPlugin.cs b/src/MySqlConnector/Plugins/ZstandardPlugin.cs new file mode 100644 index 000000000..3696067c3 --- /dev/null +++ b/src/MySqlConnector/Plugins/ZstandardPlugin.cs @@ -0,0 +1,9 @@ +using MySqlConnector.Protocol.Serialization; + +namespace MySqlConnector.Plugins; + +internal abstract class ZstandardPlugin +{ + public abstract IPayloadHandler CreatePayloadHandler(IByteHandler byteHandler); + public abstract int CompressionLevel { get; } +} diff --git a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs index 4628d1507..609fcfd45 100644 --- a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs +++ b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs @@ -5,7 +5,7 @@ namespace MySqlConnector.Protocol.Payloads; internal static class HandshakeResponse41Payload { - private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, bool useCompression, CharacterSet characterSet, ProtocolCapabilities additionalCapabilities = 0) + private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet, ProtocolCapabilities additionalCapabilities = 0) { var writer = new ByteBufferWriter(); @@ -22,10 +22,11 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s (cs.AllowLoadLocalInfile ? ProtocolCapabilities.LocalFiles : 0) | (string.IsNullOrWhiteSpace(cs.Database) ? 0 : ProtocolCapabilities.ConnectWithDatabase) | (cs.UseAffectedRows ? 0 : ProtocolCapabilities.FoundRows) | - (useCompression ? ProtocolCapabilities.Compress : ProtocolCapabilities.None) | + (compressionMethod == CompressionMethod.Zlib ? ProtocolCapabilities.Compress : ProtocolCapabilities.None) | (serverCapabilities & ProtocolCapabilities.ConnectionAttributes) | (serverCapabilities & ProtocolCapabilities.SessionTrack) | (serverCapabilities & ProtocolCapabilities.DeprecateEof) | + (compressionMethod == CompressionMethod.Zstandard ? ProtocolCapabilities.ZstandardCompressionAlgorithm : 0) | (serverCapabilities & ProtocolCapabilities.QueryAttributes) | (serverCapabilities & ProtocolCapabilities.MariaDbCacheMetadata) | additionalCapabilities; @@ -51,13 +52,13 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s return writer; } - public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, bool useCompression, CharacterSet characterSet) => - CreateCapabilitiesPayload(serverCapabilities, cs, useCompression, characterSet, ProtocolCapabilities.Ssl).ToPayloadData(); + public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet) => + CreateCapabilitiesPayload(serverCapabilities, cs, compressionMethod, characterSet, ProtocolCapabilities.Ssl).ToPayloadData(); - public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, bool useCompression, CharacterSet characterSet, byte[]? connectionAttributes) + public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes) { // TODO: verify server capabilities - var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, useCompression, characterSet); + var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, compressionMethod, characterSet); writer.WriteNullTerminatedString(cs.UserID); var authenticationResponse = AuthenticationUtility.CreateAuthenticationResponse(handshake.AuthPluginData, password); writer.Write((byte) authenticationResponse.Length); @@ -72,6 +73,10 @@ public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSe if (connectionAttributes is not null) writer.Write(connectionAttributes); + // Zstandard compression level + if (compressionMethod == CompressionMethod.Zstandard) + writer.Write((byte) (compressionLevel ?? 10)); + return writer.ToPayloadData(); } } diff --git a/src/MySqlConnector/Protocol/ProtocolCapabilities.cs b/src/MySqlConnector/Protocol/ProtocolCapabilities.cs index aca015562..9351dcc85 100644 --- a/src/MySqlConnector/Protocol/ProtocolCapabilities.cs +++ b/src/MySqlConnector/Protocol/ProtocolCapabilities.cs @@ -128,6 +128,18 @@ internal enum ProtocolCapabilities : ulong /// DeprecateEof = 0x100_0000, + /// + /// The client can handle optional metadata information in the resultset. + /// + /// Corresponds to CLIENT_OPTIONAL_RESULTSET_METADATA. + ClientOptionalResultsetMetadata = 0x200_0000, + + /// + /// The client supports the Zstandard compression algorithm. + /// + /// Corresponds to CLIENT_ZSTD_COMPRESSION_ALGORITHM. + ZstandardCompressionAlgorithm = 0x400_0000, + /// /// Supports query attributes (CLIENT_QUERY_ATTRIBUTES). ///