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 _logger; private readonly JsonSerializerOptions _jsonOptions; public SqlScriptParser(ILogger 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(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>( 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(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(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(value, _jsonOptions); } // Старый формат: type:subtype "Description" var match = Regex.Match(value, @"(\w+)(?::(\w+))?\s+""([^""]+)"""); if (!match.Success) return null; return new OutputDefinition { Type = Enum.TryParse(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 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); } } }