diff --git a/docs/content/connection-options.md b/docs/content/connection-options.md index 3f822ed57..2faae739f 100644 --- a/docs/content/connection-options.md +++ b/docs/content/connection-options.md @@ -451,6 +451,11 @@ These are the other options that MySqlConnector supports. They are set to sensib distributed transactions, but may not be compatible with server replication; there are other limitations. When set to false, regular MySQL transactions are used, just like Connector/NET. + + Use Procedure Cache, UseProcedureCache + true + When false disables the procedure cache + ## Unsupported Options diff --git a/src/MySqlConnector/Core/CommandExecutor.cs b/src/MySqlConnector/Core/CommandExecutor.cs index a2581334a..f21d0b3fb 100644 --- a/src/MySqlConnector/Core/CommandExecutor.cs +++ b/src/MySqlConnector/Core/CommandExecutor.cs @@ -20,17 +20,21 @@ public static async ValueTask ExecuteReaderAsync(CommandListPos Log.CommandExecutorExecuteReader(command.Logger, connection.Session.Id, ioBehavior, commandListPosition.CommandCount); - Dictionary? cachedProcedures = null; + var cachedProcedures = new Dictionary(); for (var commandIndex = 0; commandIndex < commandListPosition.CommandCount; commandIndex++) { var command2 = commandListPosition.CommandAt(commandIndex); - if (command2.CommandType == CommandType.StoredProcedure) + if (command2.CommandType == CommandType.StoredProcedure && connection.Session.UseProcedureCache) { - cachedProcedures ??= []; var commandText = command2.CommandText!; if (!cachedProcedures.ContainsKey(commandText)) { - cachedProcedures.Add(commandText, await connection.GetCachedProcedure(commandText, revalidateMissing: false, ioBehavior, cancellationToken).ConfigureAwait(false)); + var cachedProcedure = await connection.GetCachedProcedure(commandText, revalidateMissing: false, ioBehavior, cancellationToken).ConfigureAwait(false); + + if (cachedProcedure != null) + { + cachedProcedures.Add(commandText, cachedProcedure); + } // because the connection was used to execute a MySqlDataReader with the connection's DefaultCommandTimeout, // we need to reapply the command's CommandTimeout (even if some of the time has elapsed) @@ -41,7 +45,7 @@ public static async ValueTask ExecuteReaderAsync(CommandListPos var writer = new ByteBufferWriter(); //// cachedProcedures will be non-null if there is a stored procedure, which is also the only time it will be read - if (!payloadCreator.WriteQueryCommand(ref commandListPosition, cachedProcedures!, writer, false)) + if (!payloadCreator.WriteQueryCommand(ref commandListPosition, cachedProcedures, writer, false)) throw new InvalidOperationException("ICommandPayloadCreator failed to write query payload"); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/MySqlConnector/Core/ConnectionSettings.cs b/src/MySqlConnector/Core/ConnectionSettings.cs index f43162fbe..1f962affd 100644 --- a/src/MySqlConnector/Core/ConnectionSettings.cs +++ b/src/MySqlConnector/Core/ConnectionSettings.cs @@ -147,6 +147,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb) UseAffectedRows = csb.UseAffectedRows; UseCompression = csb.UseCompression; UseXaTransactions = csb.UseXaTransactions; + UseProcedureCache = csb.UseProcedureCache; static int ToSigned(uint value) => value >= int.MaxValue ? int.MaxValue : (int) value; } @@ -245,6 +246,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat public bool UseAffectedRows { get; } public bool UseCompression { get; } public bool UseXaTransactions { get; } + public bool UseProcedureCache { get; } public byte[]? ConnectionAttributes { get; set; } @@ -335,6 +337,7 @@ private ConnectionSettings(ConnectionSettings other, string host, int port, stri UseAffectedRows = other.UseAffectedRows; UseCompression = other.UseCompression; UseXaTransactions = other.UseXaTransactions; + UseProcedureCache = other.UseProcedureCache; } private static readonly string[] s_localhostPipeServer = ["."]; diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs index fdead8006..e30272439 100644 --- a/src/MySqlConnector/Core/ServerSession.cs +++ b/src/MySqlConnector/Core/ServerSession.cs @@ -63,6 +63,7 @@ public ServerSession(ILogger logger, IConnectionPoolMetadata pool) public bool SupportsQueryAttributes { get; private set; } public bool SupportsSessionTrack { get; private set; } public bool ProcAccessDenied { get; set; } + public bool UseProcedureCache { get; private set; } public ICollection> ActivityTags => m_activityTags; public MySqlDataReader DataReader { get; set; } public MySqlConnectionOpenedConditions Conditions { get; private set; } @@ -148,14 +149,20 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can string commandToPrepare; if (command.CommandType == CommandType.StoredProcedure) { - var cachedProcedure = await command.Connection!.GetCachedProcedure(commandText, revalidateMissing: false, ioBehavior, cancellationToken).ConfigureAwait(false); - if (cachedProcedure is null) + var parameterCount = command.RawParameters?.Count ?? 0; + + if (UseProcedureCache) { - var name = NormalizedSchema.MustNormalize(command.CommandText!, command.Connection.Database); - throw new MySqlException($"Procedure or function '{name.Component}' cannot be found in database '{name.Schema}'."); + var cachedProcedure = await command.Connection!.GetCachedProcedure(commandText, revalidateMissing: false, ioBehavior, cancellationToken).ConfigureAwait(false); + if (cachedProcedure is null) + { + var name = NormalizedSchema.MustNormalize(command.CommandText!, command.Connection.Database); + throw new MySqlException($"Procedure or function '{name.Component}' cannot be found in database '{name.Schema}, or procedure caching is disabled"); + } + + parameterCount = cachedProcedure.Parameters.Count; } - var parameterCount = cachedProcedure.Parameters.Count; #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER commandToPrepare = string.Create(commandText.Length + 7 + (parameterCount * 2) + (parameterCount == 0 ? 1 : 0), (commandText, parameterCount), static (buffer, state) => { @@ -607,6 +614,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella } m_payloadHandler.ByteHandler.RemainingTimeout = Constants.InfiniteTimeout; + UseProcedureCache = cs.UseProcedureCache; return redirectionUrl; } catch (ArgumentException ex) diff --git a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs index e6c2c641d..a1dcc5c15 100644 --- a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs +++ b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs @@ -184,8 +184,8 @@ private static void WriteBinaryParameters(ByteBufferWriter writer, MySqlParamete private static bool WriteStoredProcedure(IMySqlCommand command, IDictionary cachedProcedures, ByteBufferWriter writer) { var parameterCollection = command.RawParameters; - var cachedProcedure = cachedProcedures[command.CommandText!]; - if (cachedProcedure is not null) + var cachedProcedureExist = cachedProcedures.TryGetValue(command.CommandText!, out var cachedProcedure); + if (cachedProcedureExist && cachedProcedure is not null) parameterCollection = cachedProcedure.AlignParamsWithDb(parameterCollection); MySqlParameter? returnParameter = null; diff --git a/src/MySqlConnector/MySqlCommandBuilder.cs b/src/MySqlConnector/MySqlCommandBuilder.cs index 7b63dd5ef..64e9c4edf 100644 --- a/src/MySqlConnector/MySqlCommandBuilder.cs +++ b/src/MySqlConnector/MySqlCommandBuilder.cs @@ -31,7 +31,7 @@ private static async Task DeriveParametersAsync(IOBehavior ioBehavior, MySqlComm if (cachedProcedure is null) { var name = NormalizedSchema.MustNormalize(command.CommandText!, command.Connection.Database); - throw new MySqlException($"Procedure or function '{name.Component}' cannot be found in database '{name.Schema}'."); + throw new MySqlException($"Procedure or function '{name.Component}' cannot be found in database '{name.Schema}', or procedure caching is disabled"); } command.Parameters.Clear(); diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs index 27219bb47..4055b8d42 100644 --- a/src/MySqlConnector/MySqlConnection.cs +++ b/src/MySqlConnector/MySqlConnection.cs @@ -962,6 +962,9 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel) internal async Task GetCachedProcedure(string name, bool revalidateMissing, IOBehavior ioBehavior, CancellationToken cancellationToken) { + if (!m_session!.UseProcedureCache) + return null; + Log.GettingCachedProcedure(m_logger, m_session!.Id, name); if (State != ConnectionState.Open) throw new InvalidOperationException("Connection is not open."); diff --git a/src/MySqlConnector/MySqlConnectionStringBuilder.cs b/src/MySqlConnector/MySqlConnectionStringBuilder.cs index 762bcc639..22ac81c06 100644 --- a/src/MySqlConnector/MySqlConnectionStringBuilder.cs +++ b/src/MySqlConnector/MySqlConnectionStringBuilder.cs @@ -800,6 +800,19 @@ public bool UseXaTransactions set => MySqlConnectionStringOption.UseXaTransactions.SetValue(this, value); } + /// + /// Enables procedure cache. + /// + [Category("Other")] + [DefaultValue(true)] + [Description("Enables procedure cache.")] + [DisplayName("Use Procedure Cache")] + public bool UseProcedureCache + { + get => MySqlConnectionStringOption.UseProcedureCache.GetValue(this); + set => MySqlConnectionStringOption.UseProcedureCache.SetValue(this, value); + } + // Other Methods /// @@ -958,6 +971,7 @@ internal abstract partial class MySqlConnectionStringOption public static readonly MySqlConnectionStringValueOption UseAffectedRows; public static readonly MySqlConnectionStringValueOption UseCompression; public static readonly MySqlConnectionStringValueOption UseXaTransactions; + public static readonly MySqlConnectionStringValueOption UseProcedureCache; public static MySqlConnectionStringOption? TryGetOptionForKey(string key) => s_options.TryGetValue(key, out var option) ? option : null; @@ -1262,6 +1276,10 @@ static MySqlConnectionStringOption() AddOption(options, UseXaTransactions = new( keys: ["Use XA Transactions", "UseXaTransactions"], defaultValue: true)); + + AddOption(options, UseProcedureCache = new( + keys: ["Use Procedure Cache", "UseProcedureCache"], + defaultValue: true)); #pragma warning restore SA1118 // Parameter should not span multiple lines #if NET8_0_OR_GREATER