using System.Data; using System.Globalization; using System.Security.Cryptography; using System.Text; using MBN_STOCK_WEBVIEW.Infrastructure; using MMoneyCoderSharp.Data; internal static class NxtThemeRestoreDbAudit { private const string OriginalConfigurationPath = @"C:\Users\MD\source\repos\MBN_STOCK_N\MBN_STOCK_N\RES\MmoneyCoder.ini"; private const string ActiveRowsQueryName = "AUDIT_ACTIVE_NXT_THEME_ROWS"; private const string ActiveRowsSql = """ SELECT LIST_TEXT FROM ( SELECT p.LIST_TEXT FROM DC_LIST d JOIN PLAY_LIST p ON p.PG_CODE = d.DC_CODE WHERE p.LIST_TEXT LIKE '1^테마_NXT^%' ORDER BY d.DC_CODE, TO_NUMBER(p.ITEM_INDEX) ) WHERE ROWNUM <= :row_limit """; internal static async Task RunAsync( IDataQueryExecutor executor, MariaDbDatabaseOptions currentOptions, CancellationToken cancellationToken) { await RunEndpointAuditAsync(executor, currentOptions, cancellationToken) .ConfigureAwait(false); const int maximumRows = 1_000; var spec = new DataQuerySpec( ActiveRowsSql, [new DataQueryParameter("row_limit", maximumRows + 1, DbType.Int32)]); spec.ValidateFor(DataSourceKind.Oracle); var table = await executor.ExecuteAsync( DataSourceKind.Oracle, ActiveRowsQueryName, spec, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); if (table.Columns.Count != 1 || table.Columns[0].ColumnName != "LIST_TEXT" || table.Columns[0].DataType != typeof(string) || table.Rows.Count > maximumRows) { throw new InvalidDataException("The active NXT-theme audit boundary is invalid."); } var rows = new List(table.Rows.Count); var grammar = new Dictionary(StringComparer.Ordinal); foreach (DataRow dataRow in table.Rows) { if (dataRow[0] is not string text) { throw new InvalidDataException("An active NXT-theme row is not text."); } var fields = text.Split('^'); if (fields.Length != 7 || fields[0] != "1" || fields[1] != "테마_NXT") { throw new InvalidDataException("An active NXT-theme row has an invalid shape."); } var pageText = fields[5].Replace(" ", string.Empty, StringComparison.Ordinal); rows.Add(new LegacyNamedNxtThemeRestoreRow( rows.Count, IsEnabled: true, fields[1], fields[2], fields[3], fields[4], pageText, fields[6])); var key = string.Join('|', fields[3], fields[4]); grammar[key] = grammar.GetValueOrDefault(key) + 1; } if (rows.Count == 0) { Console.WriteLine("NXT_THEME_RESTORE_AUDIT: active=0"); return; } var result = await new LegacyNamedNxtThemeRestoreService(executor) .ResolveAsync(rows, cancellationToken); var resolved = result.Rows.Count(row => row.IsResolved); var identityMapped = result.Rows.Count(row => row.ObservedCurrentThemeCode is not null); var remapped = result.Rows .Select((row, index) => row.ObservedCurrentThemeCode is not null && !string.Equals( row.ObservedCurrentThemeCode, rows[index].DataCode, StringComparison.Ordinal)) .Count(static value => value); var failures = result.Rows .Where(row => row.Failure.HasValue) .GroupBy(row => row.Failure!.Value) .OrderBy(group => group.Key) .Select(group => $"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}") .ToArray(); var actions = result.Rows .Where(row => row.IsResolved) .GroupBy(row => row.Identity!.ActionId) .OrderBy(group => group.Key, StringComparer.Ordinal) .Select(group => $"{group.Key}={group.Count().ToString(CultureInfo.InvariantCulture)}") .ToArray(); var liveCounts = result.Rows .Where(row => row.IsResolved) .Select(row => row.Identity!.LiveItemCount) .ToArray(); Console.WriteLine( "NXT_THEME_RESTORE_AUDIT: " + $"active={rows.Count.ToString(CultureInfo.InvariantCulture)} " + $"distinctTitles={rows.Select(row => row.Subject).Distinct(StringComparer.Ordinal).Count().ToString(CultureInfo.InvariantCulture)} " + $"grammarVariants={grammar.Count.ToString(CultureInfo.InvariantCulture)} " + $"identityMapped={identityMapped.ToString(CultureInfo.InvariantCulture)} " + $"resolved={resolved.ToString(CultureInfo.InvariantCulture)} " + $"remapped={remapped.ToString(CultureInfo.InvariantCulture)} " + $"blocked={(rows.Count - resolved).ToString(CultureInfo.InvariantCulture)} " + $"session={LegacyNamedNxtThemeRestoreService.SessionForLocalHour(DateTime.Now.Hour)}"); Console.WriteLine( "NXT_THEME_RESTORE_GRAMMAR: " + $"variants={grammar.Count.ToString(CultureInfo.InvariantCulture)} " + $"rows={grammar.Values.Sum().ToString(CultureInfo.InvariantCulture)} " + "rawValuesPrinted=false"); Console.WriteLine("NXT_THEME_RESTORE_ACTIONS: " + (actions.Length == 0 ? "none" : string.Join(",", actions))); Console.WriteLine("NXT_THEME_RESTORE_FAILURES: " + (failures.Length == 0 ? "none" : string.Join(",", failures))); Console.WriteLine("NXT_THEME_RESTORE_LIVE_ITEMS: " + (liveCounts.Length == 0 ? "none" : $"min={liveCounts.Min().ToString(CultureInfo.InvariantCulture)}," + $"max={liveCounts.Max().ToString(CultureInfo.InvariantCulture)}," + $"sum={liveCounts.Sum().ToString(CultureInfo.InvariantCulture)}")); await RunJoinDiagnosticAsync(executor, rows, result, cancellationToken) .ConfigureAwait(false); } private static async Task RunJoinDiagnosticAsync( IDataQueryExecutor executor, IReadOnlyList sourceRows, LegacyNamedNxtThemeRestoreResult restore, CancellationToken cancellationToken) { var codes = new Dictionary(StringComparer.Ordinal); for (var index = 0; index < restore.Rows.Count; index++) { var current = restore.Rows[index].ObservedCurrentThemeCode; if (current is null) { continue; } var stored = sourceRows[index].DataCode; if (codes.TryGetValue(current, out var previous) && !string.Equals(previous, stored, StringComparison.Ordinal)) { throw new InvalidDataException( "The NXT join audit found conflicting stored codes for one current identity."); } codes[current] = stored; } if (codes.Count == 0) { Console.WriteLine("NXT_THEME_JOIN_DIAGNOSTIC: currentThemes=0"); return; } // Sort only to make repeated aggregate runs deterministic. Neither key // is printed or included in an exception. var identities = codes .OrderBy(static pair => pair.Key, StringComparer.Ordinal) .Select(static pair => new LegacyNxtThemeJoinAuditIdentity(pair.Key, pair.Value)) .ToArray(); var audit = await new LegacyNxtThemeJoinAuditService(executor) .AuditAsync(identities, cancellationToken) .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); var evidence = audit.Themes; var rawCurrent = evidence.Sum(static row => row.RawCurrentItemCount); var rawStored = evidence.Sum(static row => row.RawStoredItemCount); var currentOriginal = evidence.Sum(static row => row.CurrentOriginalJoinCount); var currentNonStopped = evidence.Sum(static row => row.CurrentNonStoppedJoinCount); var currentLive = evidence.Sum(static row => row.CurrentLiveJoinCount); var storedOriginal = evidence.Sum(static row => row.StoredOriginalJoinCount); var storedNonStopped = evidence.Sum(static row => row.StoredNonStoppedJoinCount); var storedLive = evidence.Sum(static row => row.StoredLiveJoinCount); var exactCandidate = evidence.Sum(static row => row.CurrentExactJoinCount); var stripBothCandidate = evidence.Sum(static row => row.CurrentStripBothJoinCount); var rightSixCandidate = evidence.Sum(static row => row.CurrentRightSixJoinCount); var validCodes = evidence.Sum(static row => row.CurrentValidItemCodeCount); var kospiMaster = evidence.Sum(static row => row.CurrentKospiMasterJoinCount); var kosdaqMaster = evidence.Sum(static row => row.CurrentKosdaqMasterJoinCount); var kospiOnline = evidence.Sum(static row => row.CurrentKospiOnlineJoinCount); var kosdaqOnline = evidence.Sum(static row => row.CurrentKosdaqOnlineJoinCount); var rawCurrentZero = evidence.Count(static row => row.RawCurrentItemCount == 0); var rawStoredPositive = evidence.Count(static row => row.RawStoredItemCount > 0); var originalCurrentPositive = evidence.Count(static row => row.CurrentOriginalJoinCount > 0); Console.WriteLine( "NXT_THEME_JOIN_DIAGNOSTIC: " + $"currentThemes={evidence.Count.ToString(CultureInfo.InvariantCulture)} " + $"remappedThemes={identities.Count(identity => !string.Equals(identity.CurrentThemeCode, identity.StoredThemeCode, StringComparison.Ordinal)).ToString(CultureInfo.InvariantCulture)} " + $"rawCurrent={rawCurrent.ToString(CultureInfo.InvariantCulture)} " + $"rawStored={rawStored.ToString(CultureInfo.InvariantCulture)} " + $"currentOriginalJoin={currentOriginal.ToString(CultureInfo.InvariantCulture)} " + $"currentNonStopped={currentNonStopped.ToString(CultureInfo.InvariantCulture)} " + $"currentLive={currentLive.ToString(CultureInfo.InvariantCulture)} " + $"storedOriginalJoin={storedOriginal.ToString(CultureInfo.InvariantCulture)} " + $"storedNonStopped={storedNonStopped.ToString(CultureInfo.InvariantCulture)} " + $"storedLive={storedLive.ToString(CultureInfo.InvariantCulture)} " + $"currentValidItemCodes={validCodes.ToString(CultureInfo.InvariantCulture)}"); Console.WriteLine( "NXT_THEME_MASTER_JOIN_DIAGNOSTIC: " + $"kospiMaster={kospiMaster.ToString(CultureInfo.InvariantCulture)} " + $"kosdaqMaster={kosdaqMaster.ToString(CultureInfo.InvariantCulture)} " + $"kospiOnline={kospiOnline.ToString(CultureInfo.InvariantCulture)} " + $"kosdaqOnline={kosdaqOnline.ToString(CultureInfo.InvariantCulture)}"); Console.WriteLine( "NXT_THEME_JOIN_CANDIDATES_DIAGNOSTIC_ONLY: " + $"exact={exactCandidate.ToString(CultureInfo.InvariantCulture)} " + $"stripBoth={stripBothCandidate.ToString(CultureInfo.InvariantCulture)} " + $"rightSix={rightSixCandidate.ToString(CultureInfo.InvariantCulture)}"); Console.WriteLine( "NXT_THEME_JOIN_CLASSIFICATION: " + $"currentRawZeroThemes={rawCurrentZero.ToString(CultureInfo.InvariantCulture)} " + $"storedRawPositiveThemes={rawStoredPositive.ToString(CultureInfo.InvariantCulture)} " + $"currentOriginalPositiveThemes={originalCurrentPositive.ToString(CultureInfo.InvariantCulture)} " + $"root={ClassifyRoot(evidence)} " + "action=read-only-review-no-fallback"); } private static string ClassifyRoot( IReadOnlyList evidence) { if (evidence.All(static row => row.RawCurrentItemCount == 0) && evidence.All(static row => row.RawStoredItemCount > 0)) { return "CurrentThemeCodeHasNoSbItemRowsStoredCodeHasRows"; } if (evidence.Any(static row => row.RawCurrentItemCount > 0) && evidence.All(static row => row.CurrentOriginalJoinCount == 0) && evidence.All(static row => row.CurrentKospiMasterJoinCount == 0 && row.CurrentKosdaqMasterJoinCount == 0)) { return "CurrentItemsMissingFromNxtStockMasters"; } if (evidence.Any(static row => row.RawCurrentItemCount > 0) && evidence.All(static row => row.CurrentOriginalJoinCount == 0) && evidence.Any(static row => row.CurrentKospiMasterJoinCount > 0 || row.CurrentKosdaqMasterJoinCount > 0)) { return "VAllStockExcludesCurrentNxtMasterRows"; } if (evidence.Any(static row => row.RawCurrentItemCount > 0) && evidence.All(static row => row.CurrentOriginalJoinCount == 0) && evidence.Any(static row => row.CurrentExactJoinCount > 0 || row.CurrentStripBothJoinCount > 0 || row.CurrentRightSixJoinCount > 0)) { return "ItemCodeJoinShapeMismatch"; } if (evidence.Any(static row => row.RawCurrentItemCount > 0) && evidence.All(static row => row.CurrentOriginalJoinCount == 0)) { return "CurrentItemsHaveNoStockIdentityJoin"; } return "MixedOrUnclassifiedReadOnlyEvidence"; } private static async Task RunEndpointAuditAsync( IDataQueryExecutor executor, MariaDbDatabaseOptions currentOptions, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(executor); ArgumentNullException.ThrowIfNull(currentOptions); cancellationToken.ThrowIfCancellationRequested(); var original = ReadOriginalEndpoint(OriginalConfigurationPath); var current = NormalizeEndpoint( currentOptions.Host, currentOptions.Port, currentOptions.Database); var spec = new DataQuerySpec( "SELECT DATABASE() CURRENT_DATABASE, VERSION() SERVER_VERSION"); spec.ValidateFor(DataSourceKind.MariaDb); var table = await executor.ExecuteAsync( DataSourceKind.MariaDb, "AUDIT_NXT_ENDPOINT_METADATA", spec, cancellationToken) .ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (table is null || table.Columns.Count != 2 || table.Rows.Count != 1 || !HasStringColumn(table.Columns[0], "CURRENT_DATABASE") || !HasStringColumn(table.Columns[1], "SERVER_VERSION") || table.Rows[0][0] is not string actualDatabase || table.Rows[0][1] is not string serverVersion || string.IsNullOrWhiteSpace(actualDatabase) || string.IsNullOrWhiteSpace(serverVersion)) { throw new InvalidDataException( "The NXT endpoint metadata audit returned an invalid closed result."); } var originalHash = HashEndpoint(original); var currentHash = HashEndpoint(current); var configuredEqual = string.Equals(original, current, StringComparison.Ordinal); var actualDatabaseMatches = string.Equals( NormalizeDatabase(actualDatabase), NormalizeDatabase(currentOptions.Database), StringComparison.Ordinal); var serverFamily = serverVersion.Contains( "MariaDB", StringComparison.OrdinalIgnoreCase) ? "MariaDB" : "MySqlCompatible"; var versionParts = serverVersion.Split('.', '-', '+'); var versionMajorMinor = versionParts.Length >= 2 && int.TryParse(versionParts[0], NumberStyles.None, CultureInfo.InvariantCulture, out var major) && int.TryParse(versionParts[1], NumberStyles.None, CultureInfo.InvariantCulture, out var minor) ? $"{major.ToString(CultureInfo.InvariantCulture)}.{minor.ToString(CultureInfo.InvariantCulture)}" : "unclassified"; Console.WriteLine( "NXT_ENDPOINT_AUDIT: " + $"originalSha256={originalHash} " + $"currentSha256={currentHash} " + $"configuredEqual={configuredEqual.ToString().ToLowerInvariant()} " + $"actualDatabaseMatchesCurrent={actualDatabaseMatches.ToString().ToLowerInvariant()} " + $"serverFamily={serverFamily} " + $"serverMajorMinor={versionMajorMinor}"); } private static string ReadOriginalEndpoint(string path) { if (!File.Exists(path)) { throw new InvalidDataException( "The fixed read-only original MariaDB configuration is unavailable."); } var source = new FileInfo(path); if ((source.Attributes & (FileAttributes.Directory | FileAttributes.ReparsePoint)) != 0 || source.Length is < 1 or > 64 * 1024) { throw new InvalidDataException( "The fixed read-only original MariaDB configuration failed its file boundary."); } var inMariaSection = false; string? connectionName = null; foreach (var rawLine in File.ReadLines(path)) { var line = rawLine.Trim(); if (line.StartsWith("[", StringComparison.Ordinal) && line.EndsWith("]", StringComparison.Ordinal)) { inMariaSection = string.Equals(line, "[Maria]", StringComparison.Ordinal); continue; } if (!inMariaSection || line.Length == 0 || line.StartsWith("//", StringComparison.Ordinal)) { continue; } const string prefix = "ConnectionName="; if (line.StartsWith(prefix, StringComparison.Ordinal)) { if (connectionName is not null) { throw new InvalidDataException( "The original MariaDB endpoint is ambiguous."); } connectionName = line[prefix.Length..]; } } if (connectionName is null) { throw new InvalidDataException( "The original MariaDB endpoint is unavailable."); } var slash = connectionName.LastIndexOf('/'); var colon = connectionName.LastIndexOf(':', slash - 1); if (slash <= 0 || colon <= 0 || colon >= slash - 1 || slash == connectionName.Length - 1 || !int.TryParse( connectionName.AsSpan(colon + 1, slash - colon - 1), NumberStyles.None, CultureInfo.InvariantCulture, out var port)) { throw new InvalidDataException( "The original MariaDB endpoint has an invalid closed shape."); } return NormalizeEndpoint( connectionName[..colon], port, connectionName[(slash + 1)..]); } private static string NormalizeEndpoint(string? host, int port, string? database) { var normalizedHost = (host ?? string.Empty) .Trim() .TrimEnd('.') .ToLowerInvariant(); var normalizedDatabase = NormalizeDatabase(database); if (normalizedHost.Length == 0 || normalizedHost.Length > 253 || normalizedDatabase.Length == 0 || normalizedDatabase.Length > 128 || port is < 1 or > 65_535 || normalizedHost.Any(static value => char.IsControl(value)) || normalizedDatabase.Any(static value => char.IsControl(value))) { throw new InvalidDataException( "A MariaDB endpoint is invalid at the closed audit boundary."); } return string.Join( '\u001f', normalizedHost, port.ToString(CultureInfo.InvariantCulture), normalizedDatabase); } private static string NormalizeDatabase(string? database) => (database ?? string.Empty).Trim().Normalize(NormalizationForm.FormC); private static string HashEndpoint(string endpoint) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(endpoint))); private static bool HasStringColumn(DataColumn column, string name) => string.Equals(column.ColumnName, name, StringComparison.Ordinal) && column.DataType == typeof(string); }