Добавьте файлы проекта.
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user