Добавьте файлы проекта.
This commit is contained in:
8
SQLVision.Services/Configuration/CacheOptions.cs
Normal file
8
SQLVision.Services/Configuration/CacheOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SQLVision.Services.Configuration;
|
||||
|
||||
public class CacheOptions
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int DurationMinutes { get; set; } = 10;
|
||||
public long MaxSizeBytes { get; set; } = 100 * 1024 * 1024; // 100MB
|
||||
}
|
||||
13
SQLVision.Services/Configuration/DatabaseOptions.cs
Normal file
13
SQLVision.Services/Configuration/DatabaseOptions.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace SQLVision.Services.Configuration;
|
||||
|
||||
public class DatabaseOptions
|
||||
{
|
||||
public const string SectionName = "Database";
|
||||
|
||||
public string DefaultConnection { get; set; } = string.Empty;
|
||||
public Dictionary<string, string> ConnectionStrings { get; set; } = new();
|
||||
public int CommandTimeout { get; set; } = 300;
|
||||
public bool EnableStatistics { get; set; } = true;
|
||||
public CacheOptions Cache { get; set; } = new();
|
||||
public RetryOptions Retry { get; set; } = new();
|
||||
}
|
||||
8
SQLVision.Services/Configuration/RetryOptions.cs
Normal file
8
SQLVision.Services/Configuration/RetryOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace SQLVision.Services.Configuration;
|
||||
|
||||
public class RetryOptions
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public int MaxRetries { get; set; } = 3;
|
||||
public int DelayMilliseconds { get; set; } = 1000;
|
||||
}
|
||||
86
SQLVision.Services/Exporters/CsvExporter.cs
Normal file
86
SQLVision.Services/Exporters/CsvExporter.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using CsvHelper;
|
||||
using CsvHelper.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Data;
|
||||
|
||||
namespace SQLVision.Services.Exporters;
|
||||
|
||||
public class CsvExporter : IExportHandler
|
||||
{
|
||||
private readonly ILogger<CsvExporter> _logger;
|
||||
|
||||
public string FormatName => "CSV";
|
||||
|
||||
public CsvExporter(ILogger<CsvExporter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
|
||||
{
|
||||
var config = new CsvConfiguration(System.Globalization.CultureInfo.CurrentCulture)
|
||||
{
|
||||
Delimiter = options.CustomOptions.TryGetValue("Delimiter", out var delimiter)
|
||||
? delimiter.ToString() ?? ","
|
||||
: ","
|
||||
};
|
||||
|
||||
using var writer = new StreamWriter(filePath);
|
||||
using var csv = new CsvWriter(writer, config);
|
||||
|
||||
// Запись заголовков
|
||||
if (options.IncludeHeaders)
|
||||
{
|
||||
foreach (DataColumn column in data.Columns)
|
||||
{
|
||||
csv.WriteField(column.ColumnName);
|
||||
}
|
||||
csv.NextRecord();
|
||||
}
|
||||
|
||||
// Запись данных
|
||||
foreach (DataRow row in data.Rows)
|
||||
{
|
||||
for (int i = 0; i < data.Columns.Count; i++)
|
||||
{
|
||||
csv.WriteField(row[i]);
|
||||
}
|
||||
csv.NextRecord();
|
||||
}
|
||||
|
||||
await csv.FlushAsync();
|
||||
_logger.LogInformation("Exported {Rows} rows to CSV: {FilePath}", data.Rows.Count, filePath);
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var writer = new StreamWriter(stream);
|
||||
using var csv = new CsvWriter(writer, System.Globalization.CultureInfo.CurrentCulture);
|
||||
|
||||
// Запись заголовков
|
||||
if (options.IncludeHeaders)
|
||||
{
|
||||
foreach (DataColumn column in data.Columns)
|
||||
{
|
||||
csv.WriteField(column.ColumnName);
|
||||
}
|
||||
csv.NextRecord();
|
||||
}
|
||||
|
||||
// Запись данных
|
||||
foreach (DataRow row in data.Rows)
|
||||
{
|
||||
for (int i = 0; i < data.Columns.Count; i++)
|
||||
{
|
||||
csv.WriteField(row[i]);
|
||||
}
|
||||
csv.NextRecord();
|
||||
}
|
||||
|
||||
await csv.FlushAsync();
|
||||
await writer.FlushAsync();
|
||||
return stream.ToArray();
|
||||
}
|
||||
}
|
||||
139
SQLVision.Services/Exporters/ExcelExporter.cs
Normal file
139
SQLVision.Services/Exporters/ExcelExporter.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using ClosedXML.Excel;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Data;
|
||||
|
||||
namespace SQLVision.Services.Exporters;
|
||||
|
||||
public class ExcelExporter : IExportHandler
|
||||
{
|
||||
private readonly ILogger<ExcelExporter> _logger;
|
||||
|
||||
public string FormatName => "Excel";
|
||||
|
||||
public ExcelExporter(ILogger<ExcelExporter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Data");
|
||||
|
||||
WriteDataToWorksheet(worksheet, data, options);
|
||||
|
||||
if (options.IncludeCharts && data.Rows.Count > 0)
|
||||
{
|
||||
AddCharts(worksheet, data, options);
|
||||
}
|
||||
|
||||
await Task.Run(() => workbook.SaveAs(filePath));
|
||||
_logger.LogInformation("Exported {Rows} rows to Excel: {FilePath}", data.Rows.Count, filePath);
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
|
||||
{
|
||||
using var workbook = new XLWorkbook();
|
||||
var worksheet = workbook.Worksheets.Add("Data");
|
||||
|
||||
WriteDataToWorksheet(worksheet, data, options);
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
await Task.Run(() => workbook.SaveAs(stream));
|
||||
return stream.ToArray();
|
||||
}
|
||||
|
||||
private void WriteDataToWorksheet(IXLWorksheet worksheet, DataTable data, ExportOptions options)
|
||||
{
|
||||
if (options.IncludeHeaders)
|
||||
{
|
||||
for (int col = 0; col < data.Columns.Count; col++)
|
||||
{
|
||||
worksheet.Cell(1, col + 1).Value = data.Columns[col].ColumnName;
|
||||
worksheet.Cell(1, col + 1).Style.Font.Bold = true;
|
||||
}
|
||||
}
|
||||
|
||||
int startRow = options.IncludeHeaders ? 2 : 1;
|
||||
|
||||
for (int row = 0; row < data.Rows.Count; row++)
|
||||
{
|
||||
for (int col = 0; col < data.Columns.Count; col++)
|
||||
{
|
||||
var value = data.Rows[row][col];
|
||||
worksheet.Cell(startRow + row, col + 1).Value = ConvertValue(value);
|
||||
|
||||
ApplyFormatting(worksheet.Cell(startRow + row, col + 1), value);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.AutoFilter)
|
||||
{
|
||||
var endRow = startRow + data.Rows.Count - 1;
|
||||
worksheet.Range(1, 1, endRow, data.Columns.Count).SetAutoFilter();
|
||||
}
|
||||
|
||||
worksheet.Columns().AdjustToContents();
|
||||
}
|
||||
|
||||
private XLCellValue ConvertValue(object value)
|
||||
{
|
||||
if (value == null || value == DBNull.Value) return Blank.Value;
|
||||
|
||||
if (value is DateTime dateTime)
|
||||
return dateTime;
|
||||
|
||||
if (value is decimal || value is double || value is float)
|
||||
return Convert.ToDouble(value);
|
||||
|
||||
if (value is bool b)
|
||||
return b;
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
private void ApplyFormatting(IXLCell cell, object value)
|
||||
{
|
||||
if (value is DateTime)
|
||||
{
|
||||
cell.Style.DateFormat.Format = "dd.MM.yyyy HH:mm:ss";
|
||||
}
|
||||
else if (value is decimal || value is double || value is float)
|
||||
{
|
||||
cell.Style.NumberFormat.Format = "#,##0.00";
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCharts(IXLWorksheet worksheet, DataTable data, ExportOptions options)
|
||||
{
|
||||
if (data.Columns.Count < 2) return;
|
||||
|
||||
//TODO: chart
|
||||
/*var chartType = GetChartType(options.ChartType);
|
||||
var chart = worksheet.CreateChart(0, data.Columns.Count + 2, 20, data.Columns.Count + 10);
|
||||
chart.ChartType = chartType;
|
||||
|
||||
// Добавление серий на основе данных
|
||||
for (int col = 1; col < Math.Min(5, data.Columns.Count); col++)
|
||||
{
|
||||
var series = chart.AddSeries(
|
||||
worksheet.Range(2, col + 1, data.Rows.Count + 1, col + 1),
|
||||
worksheet.Range(2, 1, data.Rows.Count + 1, 1));
|
||||
|
||||
series.ChartType = chartType;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private XLChartType GetChartType(string? chartType) => chartType?.ToLower() switch
|
||||
{
|
||||
"line" => XLChartType.Line,
|
||||
"column" => XLChartType.ColumnClustered,
|
||||
"bar" => XLChartType.BarClustered,
|
||||
"pie" => XLChartType.Pie,
|
||||
"area" => XLChartType.Area,
|
||||
_ => XLChartType.Line
|
||||
};
|
||||
}
|
||||
|
||||
59
SQLVision.Services/Exporters/JsonExporter.cs
Normal file
59
SQLVision.Services/Exporters/JsonExporter.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SQLVision.Services.Exporters;
|
||||
|
||||
public class JsonExporter : IExportHandler
|
||||
{
|
||||
private readonly ILogger<JsonExporter> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public string FormatName => "JSON";
|
||||
|
||||
public JsonExporter(ILogger<JsonExporter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
}
|
||||
|
||||
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
|
||||
{
|
||||
var records = ConvertDataTableToList(data);
|
||||
var json = JsonSerializer.Serialize(records, _jsonOptions);
|
||||
await File.WriteAllTextAsync(filePath, json);
|
||||
|
||||
_logger.LogInformation("Exported {Rows} rows to JSON: {FilePath}", data.Rows.Count, filePath);
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
|
||||
{
|
||||
var records = ConvertDataTableToList(data);
|
||||
var json = JsonSerializer.Serialize(records, _jsonOptions);
|
||||
return System.Text.Encoding.UTF8.GetBytes(json);
|
||||
}
|
||||
|
||||
private List<Dictionary<string, object>> ConvertDataTableToList(DataTable data)
|
||||
{
|
||||
var list = new List<Dictionary<string, object>>();
|
||||
|
||||
foreach (DataRow row in data.Rows)
|
||||
{
|
||||
var dict = new Dictionary<string, object>();
|
||||
|
||||
foreach (DataColumn column in data.Columns)
|
||||
{
|
||||
dict[column.ColumnName] = row[column] ?? DBNull.Value;
|
||||
}
|
||||
|
||||
list.Add(dict);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
312
SQLVision.Services/Parsers/SqlScriptParser.cs
Normal file
312
SQLVision.Services/Parsers/SqlScriptParser.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Interfaces;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SQLVision.Services.Parsers;
|
||||
|
||||
public class SqlScriptParser : ISqlScriptParser
|
||||
{
|
||||
private readonly ILogger<SqlScriptParser> _logger;
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
|
||||
public SqlScriptParser(ILogger<SqlScriptParser> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_jsonOptions = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
WriteIndented = true
|
||||
};
|
||||
}
|
||||
|
||||
public ScriptMetadata Parse(string filePath, string sqlContent)
|
||||
{
|
||||
var metadata = new ScriptMetadata
|
||||
{
|
||||
FileName = Path.GetFileName(filePath),
|
||||
FullPath = filePath,
|
||||
RawSql = sqlContent,
|
||||
LastModified = File.GetLastWriteTimeUtc(filePath)
|
||||
};
|
||||
|
||||
var lines = sqlContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
|
||||
var sqlBuilder = new StringBuilder();
|
||||
var inMultilineComment = false;
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmedLine = line.Trim();
|
||||
|
||||
// Обработка многострочных комментариев
|
||||
if (trimmedLine.StartsWith("/*"))
|
||||
{
|
||||
inMultilineComment = true;
|
||||
|
||||
if (trimmedLine.Contains("*/"))
|
||||
{
|
||||
inMultilineComment = false;
|
||||
ProcessInlineMultilineComment(trimmedLine, metadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessMultilineCommentStart(trimmedLine, metadata);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inMultilineComment)
|
||||
{
|
||||
if (trimmedLine.Contains("*/"))
|
||||
{
|
||||
inMultilineComment = false;
|
||||
ProcessMultilineCommentEnd(trimmedLine, metadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessMultilineCommentContent(trimmedLine, metadata);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Обработка однострочных комментариев
|
||||
if (trimmedLine.StartsWith("--"))
|
||||
{
|
||||
ProcessSingleLineComment(trimmedLine, metadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
sqlBuilder.AppendLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
metadata.ProcessedSql = sqlBuilder.ToString();
|
||||
ExtractCategoryAndTags(metadata);
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
private void ProcessSingleLineComment(string line, ScriptMetadata metadata)
|
||||
{
|
||||
// Удаляем "--" и триммируем
|
||||
var content = line.Substring(2).Trim();
|
||||
|
||||
// Проверяем на директивы
|
||||
if (content.StartsWith("@"))
|
||||
{
|
||||
ProcessDirective(content, metadata);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(metadata.Description))
|
||||
{
|
||||
// Первый комментарий без директивы - это описание
|
||||
metadata.Description = content;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDirective(string content, ScriptMetadata metadata) // Убрали ref
|
||||
{
|
||||
// Убираем "@"
|
||||
content = content.Substring(1).Trim();
|
||||
|
||||
var spaceIndex = content.IndexOf(' ');
|
||||
if (spaceIndex <= 0) return;
|
||||
|
||||
var directive = content.Substring(0, spaceIndex).ToLower();
|
||||
var value = content.Substring(spaceIndex + 1).Trim();
|
||||
|
||||
try
|
||||
{
|
||||
switch (directive)
|
||||
{
|
||||
case "description":
|
||||
metadata.Description = value.Trim('"');
|
||||
break;
|
||||
|
||||
case "param":
|
||||
var param = ParseParameter(value);
|
||||
if (param != null)
|
||||
metadata.Parameters.Add(param);
|
||||
break;
|
||||
|
||||
case "output":
|
||||
var output = ParseOutput(value);
|
||||
if (output != null)
|
||||
metadata.Outputs.Add(output);
|
||||
break;
|
||||
|
||||
case "connection":
|
||||
metadata.ConnectionString = value.Trim('"');
|
||||
break;
|
||||
|
||||
case "database":
|
||||
if (Enum.TryParse<DatabaseProvider>(value, true, out var provider))
|
||||
metadata.DatabaseProvider = provider;
|
||||
break;
|
||||
|
||||
case "category":
|
||||
metadata.Category = value.Trim('"');
|
||||
break;
|
||||
|
||||
case "tags":
|
||||
metadata.Tags = value.Split(',')
|
||||
.Select(t => t.Trim().Trim('"'))
|
||||
.Where(t => !string.IsNullOrEmpty(t))
|
||||
.ToList();
|
||||
break;
|
||||
|
||||
case "metadata":
|
||||
try
|
||||
{
|
||||
var metadataJson = JsonSerializer.Deserialize<Dictionary<string, object>>(
|
||||
value, _jsonOptions);
|
||||
foreach (var kvp in metadataJson)
|
||||
metadata.Metadata[kvp.Key] = kvp.Value;
|
||||
}
|
||||
catch { /* Игнорируем ошибки парсинга JSON */ }
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Error parsing directive: {Directive}", directive);
|
||||
}
|
||||
}
|
||||
|
||||
private ScriptParameter? ParseParameter(string value)
|
||||
{
|
||||
// Два формата: JSON и старый текстовый
|
||||
if (value.TrimStart().StartsWith("{"))
|
||||
{
|
||||
return ParseJsonParameter(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ParseLegacyParameter(value);
|
||||
}
|
||||
}
|
||||
|
||||
private ScriptParameter? ParseJsonParameter(string json)
|
||||
{
|
||||
try
|
||||
{
|
||||
var param = JsonSerializer.Deserialize<ScriptParameter>(json, _jsonOptions);
|
||||
|
||||
// Валидация обязательных полей
|
||||
if (string.IsNullOrEmpty(param?.Name))
|
||||
throw new ArgumentException("Parameter name is required");
|
||||
|
||||
if (param.Type == ParameterType.Table && string.IsNullOrEmpty(param.TableQuery))
|
||||
throw new ArgumentException("TableQuery is required for Table type");
|
||||
|
||||
return param;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error parsing JSON parameter");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private ScriptParameter? ParseLegacyParameter(string legacy)
|
||||
{
|
||||
// Формат: Type Name "Display Name" default="value"
|
||||
var match = Regex.Match(legacy,
|
||||
@"(\w+)\s+(\w+)\s+""([^""]+)""(?:\s+default=""([^""]*)"")?(?:\s+@table\s+""([^""]+)"")?");
|
||||
|
||||
if (!match.Success) return null;
|
||||
|
||||
return new ScriptParameter
|
||||
{
|
||||
Type = Enum.TryParse<ParameterType>(match.Groups[1].Value, true, out var type)
|
||||
? type : ParameterType.String,
|
||||
Name = match.Groups[2].Value,
|
||||
DisplayName = match.Groups[3].Value,
|
||||
DefaultValue = match.Groups[4].Success ? match.Groups[4].Value : null,
|
||||
TableQuery = match.Groups[5].Success ? match.Groups[5].Value : null
|
||||
};
|
||||
}
|
||||
|
||||
private OutputDefinition? ParseOutput(string value)
|
||||
{
|
||||
if (value.TrimStart().StartsWith("{"))
|
||||
{
|
||||
return JsonSerializer.Deserialize<OutputDefinition>(value, _jsonOptions);
|
||||
}
|
||||
|
||||
// Старый формат: type:subtype "Description"
|
||||
var match = Regex.Match(value, @"(\w+)(?::(\w+))?\s+""([^""]+)""");
|
||||
|
||||
if (!match.Success) return null;
|
||||
|
||||
return new OutputDefinition
|
||||
{
|
||||
Type = Enum.TryParse<OutputType>(match.Groups[1].Value, true, out var type)
|
||||
? type : OutputType.Table,
|
||||
SubType = match.Groups[2].Success ? match.Groups[2].Value : null,
|
||||
Description = match.Groups[3].Value
|
||||
};
|
||||
}
|
||||
|
||||
private void ExtractCategoryAndTags(ScriptMetadata metadata)
|
||||
{
|
||||
// Извлекаем категорию из пути файла
|
||||
if (string.IsNullOrEmpty(metadata.Category))
|
||||
{
|
||||
var relativePath = Path.GetDirectoryName(metadata.FullPath);
|
||||
if (!string.IsNullOrEmpty(relativePath))
|
||||
{
|
||||
metadata.Category = Path.GetFileName(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Автоматическое добавление тегов на основе имени файла
|
||||
var fileName = Path.GetFileNameWithoutExtension(metadata.FileName);
|
||||
var words = fileName.Split('_', '-', ' ')
|
||||
.Where(w => w.Length > 2)
|
||||
.Select(w => w.ToLower());
|
||||
|
||||
metadata.Tags.AddRange(words);
|
||||
metadata.Tags = metadata.Tags.Distinct().ToList();
|
||||
}
|
||||
|
||||
public async Task<ScriptMetadata> ParseAsync(string filePath, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var content = await File.ReadAllTextAsync(filePath, cancellationToken);
|
||||
return Parse(filePath, content);
|
||||
}
|
||||
|
||||
// Вспомогательные методы для многострочных комментариев
|
||||
private void ProcessInlineMultilineComment(string line, ScriptMetadata metadata)
|
||||
{
|
||||
var content = line.Substring(2, line.IndexOf("*/") - 2).Trim();
|
||||
ProcessCommentContent(content, metadata);
|
||||
}
|
||||
|
||||
private void ProcessMultilineCommentStart(string line, ScriptMetadata metadata)
|
||||
{
|
||||
var content = line.Substring(2).Trim();
|
||||
ProcessCommentContent(content, metadata);
|
||||
}
|
||||
|
||||
private void ProcessMultilineCommentEnd(string line, ScriptMetadata metadata)
|
||||
{
|
||||
var content = line.Substring(0, line.IndexOf("*/")).Trim();
|
||||
ProcessCommentContent(content, metadata);
|
||||
}
|
||||
|
||||
private void ProcessMultilineCommentContent(string line, ScriptMetadata metadata)
|
||||
{
|
||||
ProcessCommentContent(line, metadata);
|
||||
}
|
||||
|
||||
private void ProcessCommentContent(string content, ScriptMetadata metadata) // Убрали ref
|
||||
{
|
||||
if (content.StartsWith("@"))
|
||||
{
|
||||
ProcessDirective(content, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
SQLVision.Services/SQLVision.Services.csproj
Normal file
32
SQLVision.Services/SQLVision.Services.csproj
Normal file
@@ -0,0 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SQLVision.Core\SQLVision.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- MSSQL драйвер -->
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.3" />
|
||||
|
||||
<!-- Экспорт в Excel -->
|
||||
<PackageReference Include="ClosedXML" Version="0.105.0" />
|
||||
|
||||
<!-- Экспорт в CSV -->
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
|
||||
<!-- DI, конфигурация, кэширование -->
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
122
SQLVision.Services/Services/ExportService.cs
Normal file
122
SQLVision.Services/Services/ExportService.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLVision.Core.Interfaces;
|
||||
using SQLVision.Core.Models;
|
||||
using SQLVision.Services.Exporters;
|
||||
using System.Data;
|
||||
|
||||
namespace SQLVision.Services.Services;
|
||||
|
||||
public class ExportService : IExportService
|
||||
{
|
||||
private readonly ILogger<ExportService> _logger;
|
||||
private readonly Dictionary<string, IExportHandler> _exportHandlers;
|
||||
|
||||
public ExportService(
|
||||
ILogger<ExportService> logger,
|
||||
IEnumerable<IExportHandler> exportHandlers)
|
||||
{
|
||||
_logger = logger;
|
||||
_exportHandlers = exportHandlers.ToDictionary(h => h.FormatName, h => h);
|
||||
}
|
||||
|
||||
public async Task ExportAsync(DataTable data, string filePath, ExportOptions options)
|
||||
{
|
||||
if (data == null) throw new ArgumentNullException(nameof(data));
|
||||
|
||||
var extension = Path.GetExtension(filePath).TrimStart('.').ToLower();
|
||||
var format = options?.Format ?? GetFormatFromExtension(extension);
|
||||
|
||||
if (_exportHandlers.TryGetValue(format, out var handler))
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.ExportAsync(data, filePath, options ?? new ExportOptions());
|
||||
_logger.LogInformation("Exported {Rows} rows to {FilePath} as {Format}",
|
||||
data.Rows.Count, filePath, format);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error exporting to {Format}", format);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Export format '{format}' is not supported");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ExportAsync(DataSet dataSet, string filePath, ExportOptions options)
|
||||
{
|
||||
if (dataSet.Tables.Count == 0)
|
||||
throw new InvalidOperationException("DataSet contains no tables");
|
||||
|
||||
if (dataSet.Tables.Count == 1)
|
||||
{
|
||||
await ExportAsync(dataSet.Tables[0], filePath, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Для Excel создаем несколько листов
|
||||
var extension = Path.GetExtension(filePath);
|
||||
if (extension.Equals(".xlsx", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
await ExportToMultiSheetExcel(dataSet, filePath, options);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Для других форматов - экспортируем каждый лист в отдельный файл
|
||||
var basePath = Path.Combine(Path.GetDirectoryName(filePath)!,
|
||||
Path.GetFileNameWithoutExtension(filePath));
|
||||
|
||||
for (int i = 0; i < dataSet.Tables.Count; i++)
|
||||
{
|
||||
var table = dataSet.Tables[i];
|
||||
var tableFilePath = $"{basePath}_{table.TableName}_{i + 1}{extension}";
|
||||
await ExportAsync(table, tableFilePath, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<byte[]> ExportToMemoryAsync(DataTable data, ExportOptions options)
|
||||
{
|
||||
var format = options?.Format ?? "Excel";
|
||||
|
||||
if (_exportHandlers.TryGetValue(format, out var handler))
|
||||
{
|
||||
return await handler.ExportToMemoryAsync(data, options ?? new ExportOptions());
|
||||
}
|
||||
|
||||
throw new NotSupportedException($"Export format '{format}' is not supported");
|
||||
}
|
||||
|
||||
private async Task ExportToMultiSheetExcel(DataSet dataSet, string filePath, ExportOptions options)
|
||||
{
|
||||
using var workbook = new ClosedXML.Excel.XLWorkbook();
|
||||
|
||||
for (int i = 0; i < dataSet.Tables.Count; i++)
|
||||
{
|
||||
var table = dataSet.Tables[i];
|
||||
var worksheet = workbook.Worksheets.Add(table.TableName ?? $"Sheet{i + 1}");
|
||||
|
||||
// Используем ExcelExporter для записи данных
|
||||
if (_exportHandlers.TryGetValue("Excel", out var excelHandler) &&
|
||||
excelHandler is ExcelExporter excelExporter)
|
||||
{
|
||||
var excelOptions = options ?? new ExportOptions();
|
||||
await excelExporter.ExportAsync(table, filePath, excelOptions);
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Run(() => workbook.SaveAs(filePath));
|
||||
}
|
||||
|
||||
private string GetFormatFromExtension(string extension) => extension switch
|
||||
{
|
||||
"xlsx" => "Excel",
|
||||
"csv" => "CSV",
|
||||
"json" => "JSON",
|
||||
_ => "Excel"
|
||||
};
|
||||
}
|
||||
122
SQLVision.Services/Services/PluginManager.cs
Normal file
122
SQLVision.Services/Services/PluginManager.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Interfaces;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SQLVision.Services.Services;
|
||||
|
||||
public class PluginManager : IPluginManager
|
||||
{
|
||||
private readonly ILogger<PluginManager> _logger;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly List<ISqlVisionPlugin> _plugins = new();
|
||||
|
||||
public PluginManager(ILogger<PluginManager> logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public void LoadPlugins(string pluginsDirectory)
|
||||
{
|
||||
if (!Directory.Exists(pluginsDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(pluginsDirectory);
|
||||
_logger.LogInformation("Created plugins directory: {Directory}", pluginsDirectory);
|
||||
return;
|
||||
}
|
||||
|
||||
var pluginFiles = Directory.GetFiles(pluginsDirectory, "*.dll");
|
||||
_logger.LogInformation("Found {Count} plugin files", pluginFiles.Length);
|
||||
|
||||
foreach (var pluginFile in pluginFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFrom(pluginFile);
|
||||
var pluginTypes = assembly.GetTypes()
|
||||
.Where(t => typeof(ISqlVisionPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract);
|
||||
|
||||
foreach (var pluginType in pluginTypes)
|
||||
{
|
||||
try
|
||||
{
|
||||
var plugin = (ISqlVisionPlugin)Activator.CreateInstance(pluginType)!;
|
||||
var context = new PluginContext(_serviceProvider);
|
||||
plugin.InitializeAsync(context).Wait();
|
||||
_plugins.Add(plugin);
|
||||
_logger.LogInformation("Loaded plugin: {Name} v{Version}", plugin.Name, plugin.Version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to initialize plugin from type {Type}", pluginType.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to load plugin from {File}", pluginFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ISqlVisionPlugin> GetPlugins() => _plugins.AsReadOnly();
|
||||
|
||||
public T? GetPlugin<T>() where T : ISqlVisionPlugin
|
||||
=> _plugins.OfType<T>().FirstOrDefault();
|
||||
|
||||
public async Task BeforeExecutionAsync(ScriptMetadata script, Dictionary<string, object> parameters)
|
||||
{
|
||||
foreach (var plugin in _plugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
await plugin.InitializeAsync(new PluginContext(_serviceProvider));
|
||||
// TODO: Добавить метод BeforeExecution в ISqlVisionPlugin если нужно
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in plugin {Plugin} BeforeExecution", plugin.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AfterExecutionAsync(ScriptMetadata script, ExecutionResult result)
|
||||
{
|
||||
foreach (var plugin in _plugins)
|
||||
{
|
||||
try
|
||||
{
|
||||
// TODO: Добавить метод AfterExecution в ISqlVisionPlugin если нужно
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in plugin {Plugin} AfterExecution", plugin.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class PluginContext : IPluginContext
|
||||
{
|
||||
public IServiceProvider ServiceProvider { get; }
|
||||
public IConfiguration Configuration { get; }
|
||||
public ILogger Logger { get; }
|
||||
|
||||
public PluginContext(IServiceProvider serviceProvider)
|
||||
{
|
||||
ServiceProvider = serviceProvider;
|
||||
Configuration = serviceProvider.GetRequiredService<IConfiguration>();
|
||||
Logger = serviceProvider.GetRequiredService<ILogger<PluginManager>>();
|
||||
}
|
||||
|
||||
public Task ShowNotificationAsync(string message, NotificationType type)
|
||||
{
|
||||
// TODO: Реализовать показ уведомлений через UI
|
||||
Logger.LogInformation("Plugin notification ({Type}): {Message}", type, message);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
197
SQLVision.Services/Services/ScriptManager.cs
Normal file
197
SQLVision.Services/Services/ScriptManager.cs
Normal file
@@ -0,0 +1,197 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SQLVision.Core.Enums;
|
||||
using SQLVision.Core.Interfaces;
|
||||
using SQLVision.Core.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace SQLVision.Services.Services;
|
||||
|
||||
public class ScriptManager : IScriptManager, IDisposable
|
||||
{
|
||||
private readonly ISqlScriptParser _parser;
|
||||
private readonly ILogger<ScriptManager> _logger;
|
||||
private readonly FileSystemWatcher _watcher;
|
||||
private readonly ConcurrentDictionary<string, ScriptMetadata> _scripts;
|
||||
private readonly string _scriptsDirectory;
|
||||
|
||||
public event EventHandler<ScriptChangedEventArgs>? ScriptChanged;
|
||||
public event EventHandler<ScriptsReloadedEventArgs>? ScriptsReloaded;
|
||||
|
||||
public ScriptManager(ISqlScriptParser parser, IConfiguration configuration, ILogger<ScriptManager> logger)
|
||||
{
|
||||
_parser = parser;
|
||||
_logger = logger;
|
||||
_scripts = new ConcurrentDictionary<string, ScriptMetadata>();
|
||||
_scriptsDirectory = configuration["Scripts:Directory"] ?? "Scripts";
|
||||
|
||||
_watcher = new FileSystemWatcher
|
||||
{
|
||||
Path = _scriptsDirectory,
|
||||
Filter = "*.sql",
|
||||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName,
|
||||
EnableRaisingEvents = false
|
||||
};
|
||||
|
||||
_watcher.Changed += OnScriptChanged;
|
||||
_watcher.Created += OnScriptCreated;
|
||||
_watcher.Deleted += OnScriptDeleted;
|
||||
_watcher.Renamed += OnScriptRenamed;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ScriptMetadata>> LoadScriptsAsync(string? directory = null)
|
||||
{
|
||||
var targetDirectory = directory ?? _scriptsDirectory;
|
||||
|
||||
if (!Directory.Exists(targetDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
_logger.LogInformation("Created scripts directory: {Directory}", targetDirectory);
|
||||
return Enumerable.Empty<ScriptMetadata>();
|
||||
}
|
||||
|
||||
var sqlFiles = Directory.GetFiles(targetDirectory, "*.sql", SearchOption.AllDirectories);
|
||||
var tasks = sqlFiles.Select(LoadScriptAsync);
|
||||
var results = await Task.WhenAll(tasks);
|
||||
|
||||
_scripts.Clear();
|
||||
foreach (var script in results.Where(s => s != null))
|
||||
{
|
||||
_scripts[script!.FullPath] = script;
|
||||
}
|
||||
|
||||
StartWatching();
|
||||
|
||||
ScriptsReloaded?.Invoke(this, new ScriptsReloadedEventArgs(results.Where(s => s != null).ToList()!));
|
||||
|
||||
_logger.LogInformation("Loaded {Count} scripts from {Directory}", _scripts.Count, targetDirectory);
|
||||
return _scripts.Values;
|
||||
}
|
||||
|
||||
private async Task<ScriptMetadata?> LoadScriptAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _parser.ParseAsync(filePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading script: {FilePath}", filePath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ScriptMetadata> ReloadScriptAsync(string filePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var script = await LoadScriptAsync(filePath);
|
||||
if (script != null)
|
||||
{
|
||||
_scripts[filePath] = script;
|
||||
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(filePath, ScriptChangeType.Updated, script));
|
||||
}
|
||||
return script!;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error reloading script: {FilePath}", filePath);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScriptChanged(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
// Задержка для избежания многократных вызовов
|
||||
Task.Delay(300).ContinueWith(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var script = await ReloadScriptAsync(e.FullPath);
|
||||
if (script != null)
|
||||
{
|
||||
_logger.LogInformation("Script changed: {FileName}", e.Name);
|
||||
}
|
||||
}
|
||||
catch { /* Игнорируем ошибки */ }
|
||||
});
|
||||
}
|
||||
|
||||
private void OnScriptCreated(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
Task.Delay(300).ContinueWith(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var script = await LoadScriptAsync(e.FullPath);
|
||||
if (script != null)
|
||||
{
|
||||
_scripts[e.FullPath] = script;
|
||||
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Created, script));
|
||||
_logger.LogInformation("Script created: {FileName}", e.Name);
|
||||
}
|
||||
}
|
||||
catch { /* Игнорируем */ }
|
||||
});
|
||||
}
|
||||
|
||||
private void OnScriptDeleted(object sender, FileSystemEventArgs e)
|
||||
{
|
||||
if (_scripts.TryRemove(e.FullPath, out var script))
|
||||
{
|
||||
ScriptChanged?.Invoke(this, new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Deleted, script));
|
||||
_logger.LogInformation("Script deleted: {FileName}", e.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScriptRenamed(object sender, RenamedEventArgs e)
|
||||
{
|
||||
Task.Delay(300).ContinueWith(async _ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Удаляем старый файл
|
||||
_scripts.TryRemove(e.OldFullPath, out var s);
|
||||
|
||||
// Загружаем новый
|
||||
var script = await LoadScriptAsync(e.FullPath);
|
||||
if (script != null)
|
||||
{
|
||||
_scripts[e.FullPath] = script;
|
||||
ScriptChanged?.Invoke(this,
|
||||
new ScriptChangedEventArgs(e.FullPath, ScriptChangeType.Renamed, script));
|
||||
_logger.LogInformation("Script renamed: {OldName} -> {NewName}",
|
||||
Path.GetFileName(e.OldFullPath), e.Name);
|
||||
}
|
||||
}
|
||||
catch { /* Игнорируем */ }
|
||||
});
|
||||
}
|
||||
|
||||
private void StartWatching()
|
||||
{
|
||||
if (!_watcher.EnableRaisingEvents)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
_logger.LogDebug("Started watching directory: {Directory}", _scriptsDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
public void WatchDirectory(string directory, Action<string> onScriptChanged)
|
||||
{
|
||||
if (_watcher.EnableRaisingEvents)
|
||||
{
|
||||
_watcher.EnableRaisingEvents = false;
|
||||
}
|
||||
|
||||
_watcher.Path = directory;
|
||||
_watcher.EnableRaisingEvents = true;
|
||||
|
||||
ScriptChanged += (sender, e) => onScriptChanged?.Invoke(e.FilePath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_watcher?.Dispose();
|
||||
}
|
||||
}
|
||||
36
SQLVision.Services/Services/ServiceExtensions.cs
Normal file
36
SQLVision.Services/Services/ServiceExtensions.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using SQLVision.Core.Interfaces;
|
||||
using SQLVision.Services.Exporters;
|
||||
using SQLVision.Services.Parsers;
|
||||
using SQLVision.Services.Services;
|
||||
|
||||
namespace SQLVision.Services;
|
||||
|
||||
public static class ServiceExtensions
|
||||
{
|
||||
public static IServiceCollection AddSqlVisionServices(this IServiceCollection services)
|
||||
{
|
||||
// Регистрация парсера
|
||||
services.TryAddSingleton<ISqlScriptParser, SqlScriptParser>();
|
||||
|
||||
// Регистрация сервиса выполнения SQL
|
||||
services.TryAddSingleton<ISqlExecutionService, SqlExecutionService>();
|
||||
|
||||
// Регистрация менеджера скриптов
|
||||
services.TryAddSingleton<IScriptManager, ScriptManager>();
|
||||
|
||||
// Регистрация сервиса экспорта
|
||||
services.TryAddSingleton<IExportService, ExportService>();
|
||||
|
||||
// Регистрация менеджера плагинов
|
||||
services.TryAddSingleton<IPluginManager, PluginManager>();
|
||||
|
||||
// Регистрация экспортеров
|
||||
services.TryAddSingleton<IExportHandler, ExcelExporter>();
|
||||
services.TryAddSingleton<IExportHandler, CsvExporter>();
|
||||
services.TryAddSingleton<IExportHandler, JsonExporter>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
305
SQLVision.Services/Services/SqlExecutionService.cs
Normal file
305
SQLVision.Services/Services/SqlExecutionService.cs
Normal file
@@ -0,0 +1,305 @@
|
||||
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<SqlExecutionService> _logger;
|
||||
private readonly IOptions<DatabaseOptions> _options;
|
||||
private readonly ConcurrentDictionary<string, Task<DataTable>> _loadingComboBoxData = new();
|
||||
|
||||
public SqlExecutionService(
|
||||
IMemoryCache cache,
|
||||
ILogger<SqlExecutionService> logger,
|
||||
IOptions<DatabaseOptions> options)
|
||||
{
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(
|
||||
ScriptMetadata script,
|
||||
Dictionary<string, object> parameters,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
var result = new ExecutionResult
|
||||
{
|
||||
Parameters = new Dictionary<string, object>(parameters),
|
||||
ExecutionDate = DateTime.UtcNow,
|
||||
ConnectionName = script.ConnectionString
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
// Генерация ключа кэша
|
||||
var cacheKey = GenerateCacheKey(script, parameters);
|
||||
if (_options.Value.Cache.Enabled)
|
||||
{
|
||||
if (_cache.TryGetValue<ExecutionResult>(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<DataTable>().Sum(t => t.Rows.Count);
|
||||
result.Metrics = new Dictionary<string, object>
|
||||
{
|
||||
["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<DataSet> ExecuteQueryAsync(
|
||||
string sql,
|
||||
List<SqlParameter> 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<SqlParameter> Parameters) PrepareSql(
|
||||
string sql,
|
||||
Dictionary<string, object> parameters,
|
||||
DatabaseProvider provider)
|
||||
{
|
||||
var dbParameters = new List<SqlParameter>();
|
||||
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<ExecutionResult> ExecuteAsync(
|
||||
string sql,
|
||||
Dictionary<string, object> 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<bool> 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<DataTable> 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<string, object> 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user