312 lines
10 KiB
C#
312 lines
10 KiB
C#
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);
|
|
}
|
|
}
|
|
} |