using Microsoft.Data.SqlClient; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SQLVision.Core.Enums; using SQLVision.Core.Interfaces; using SQLVision.Core.Models; using SQLVision.Services.Configuration; using System.Collections.Concurrent; using System.Data; using System.Diagnostics; using System.Security.Cryptography; using System.Text; namespace SQLVision.Services.Services; public class SqlExecutionService : ISqlExecutionService, IDisposable { private readonly IMemoryCache _cache; private readonly ILogger _logger; private readonly IOptions _options; private readonly ConcurrentDictionary> _loadingComboBoxData = new(); public SqlExecutionService( IMemoryCache cache, ILogger logger, IOptions options) { _cache = cache; _logger = logger; _options = options; } public async Task ExecuteAsync( ScriptMetadata script, Dictionary parameters, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); var result = new ExecutionResult { Parameters = new Dictionary(parameters), ExecutionDate = DateTime.UtcNow, ConnectionName = script.ConnectionString }; try { // Генерация ключа кэша var cacheKey = GenerateCacheKey(script, parameters); if (_options.Value.Cache.Enabled) { if (_cache.TryGetValue(cacheKey, out var cachedResult)) { _logger.LogDebug("Returning cached result for {Script}", script.FileName); cachedResult!.IsFromCache = true; cachedResult.ExecutionTime = stopwatch.Elapsed; return cachedResult; } } // Подготовка SQL с параметрами var (processedSql, dbParameters) = PrepareSql( script.ProcessedSql, parameters, script.DatabaseProvider); result.ExecutedSql = processedSql; // Выполнение запроса var dataSet = await ExecuteQueryAsync( processedSql, dbParameters, script.ConnectionString ?? _options.Value.DefaultConnection, cancellationToken); stopwatch.Stop(); result.Data = dataSet; result.IsSuccess = true; result.ExecutionTime = stopwatch.Elapsed; result.RowCount = dataSet.Tables.Cast().Sum(t => t.Rows.Count); result.Metrics = new Dictionary { ["ExecutionTimeMs"] = stopwatch.ElapsedMilliseconds, ["RowsAffected"] = result.RowCount, ["TablesCount"] = dataSet.Tables.Count }; // Кэширование результата if (_options.Value.Cache.Enabled && result.RowCount > 0) { var cacheOptions = new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_options.Value.Cache.DurationMinutes), Size = CalculateDataSetSize(dataSet) }; _cache.Set(cacheKey, result, cacheOptions); } _logger.LogInformation( "Executed {Script} in {ElapsedMs}ms, returned {Rows} rows", script.FileName, stopwatch.ElapsedMilliseconds, result.RowCount); return result; } catch (Exception ex) { stopwatch.Stop(); result.IsSuccess = false; result.ErrorMessage = ex.Message; result.ExecutionTime = stopwatch.Elapsed; _logger.LogError(ex, "Error executing script {Script}", script.FileName); return result; } } private async Task ExecuteQueryAsync( string sql, List parameters, string connectionString, CancellationToken cancellationToken) { var dataSet = new DataSet(); await using var connection = new SqlConnection(connectionString); await connection.OpenAsync(cancellationToken); await using var command = new SqlCommand(sql, connection); command.CommandTimeout = _options.Value.CommandTimeout; // Добавление параметров command.Parameters.AddRange(parameters.ToArray()); await using var reader = await command.ExecuteReaderAsync(cancellationToken); do { var dataTable = new DataTable(); dataTable.Load(reader); dataSet.Tables.Add(dataTable); } while (!reader.IsClosed && await reader.NextResultAsync(cancellationToken)); return dataSet; } private (string Sql, List Parameters) PrepareSql( string sql, Dictionary parameters, DatabaseProvider provider) { var dbParameters = new List(); var processedSql = new StringBuilder(sql); foreach (var (key, value) in parameters) { var paramName = $"@{key}"; var sqlParam = CreateSqlParameter(paramName, value); dbParameters.Add(sqlParam); } return (processedSql.ToString(), dbParameters); } private SqlParameter CreateSqlParameter(string name, object? value) { var sqlParam = new SqlParameter(name, value ?? DBNull.Value); // Автоматическое определение типа данных if (value is DateTime dateTime) { sqlParam.SqlDbType = SqlDbType.DateTime2; sqlParam.Value = dateTime; } else if (value is int intValue) { sqlParam.SqlDbType = SqlDbType.Int; sqlParam.Value = intValue; } else if (value is decimal decimalValue) { sqlParam.SqlDbType = SqlDbType.Decimal; sqlParam.Value = decimalValue; } else if (value is bool boolValue) { sqlParam.SqlDbType = SqlDbType.Bit; sqlParam.Value = boolValue; } else if (value is string stringValue) { sqlParam.SqlDbType = SqlDbType.NVarChar; sqlParam.Value = stringValue; sqlParam.Size = Math.Min(stringValue.Length * 2, 4000); // Ограничение для NVARCHAR } return sqlParam; } public async Task ExecuteAsync( string sql, Dictionary parameters, string connectionString, CancellationToken cancellationToken = default) { var script = new ScriptMetadata { ProcessedSql = sql, ConnectionString = connectionString, DatabaseProvider = DatabaseProvider.SqlServer }; return await ExecuteAsync(script, parameters, cancellationToken); } public async Task TestConnectionAsync( string connectionString, DatabaseProvider provider, CancellationToken cancellationToken = default) { try { await using var connection = new SqlConnection(connectionString); await connection.OpenAsync(cancellationToken); await connection.CloseAsync(); return true; } catch (Exception ex) { _logger.LogWarning(ex, "Connection test failed for {Provider}", provider); return false; } } public async Task LoadComboBoxDataAsync( string query, string connectionString, DatabaseProvider provider, CancellationToken cancellationToken = default) { var cacheKey = $"ComboBox_{provider}_{connectionString}_{query.GetHashCode()}"; return await _loadingComboBoxData.GetOrAdd(cacheKey, async key => { try { await using var connection = new SqlConnection(connectionString); await using var command = new SqlCommand(query, connection); command.CommandTimeout = 30; await connection.OpenAsync(cancellationToken); await using var reader = await command.ExecuteReaderAsync(cancellationToken); var dataTable = new DataTable(); dataTable.Load(reader); return dataTable; } catch (Exception ex) { _logger.LogError(ex, "Failed to load combo box data for query: {Query}", query); throw; } finally { _loadingComboBoxData.TryRemove(key, out _); } }); } private string GenerateCacheKey(ScriptMetadata script, Dictionary parameters) { using var sha256 = SHA256.Create(); // Создаем строку для хэширования var keyBuilder = new StringBuilder(); keyBuilder.Append(script.Id); foreach (var param in parameters.OrderBy(p => p.Key)) { keyBuilder.Append($"|{param.Key}={param.Value}"); } var keyData = keyBuilder.ToString(); var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData)); return Convert.ToBase64String(hash); } private long CalculateDataSetSize(DataSet dataSet) { long size = 0; foreach (DataTable table in dataSet.Tables) { // Примерный расчет размера: кол-во строк * кол-во столбцов * средний размер size += table.Rows.Count * table.Columns.Count * 64; // 64 байта на ячейку } return size; } public void Dispose() { // Очищаем кэш загрузки данных для ComboBox _loadingComboBoxData.Clear(); } }