< Summary

Information
Class: Services.DataServices.TriggerSelfHealing.MySqlTriggerReloader
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/DataServices/TriggerSelfHealing/MySqlTriggerReloader.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 33
Coverable lines: 33
Total lines: 61
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 20
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
.cctor()100%210%
ReloadAsync()0%420200%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/DataServices/TriggerSelfHealing/MySqlTriggerReloader.cs

#LineLine coverage
 1using System.Text.RegularExpressions;
 2using MySqlConnector;
 3
 4namespace Services.DataServices.TriggerSelfHealing;
 5
 6/// <summary>
 7/// Пересоздаёт триггеры таблицы через <c>CREATE OR REPLACE TRIGGER</c> из тела, которое хранит сам
 8/// сервер (information_schema.TRIGGERS). Атомарно, без окна «без триггера», не требует RELOAD
 9/// (в отличие от FLUSH TABLES) — достаточно привилегии TRIGGER, которая у пользователя приложения есть.
 10/// </summary>
 011public sealed class MySqlTriggerReloader(string connectionString) : ITriggerReloader
 12{
 013    private static readonly Regex SafeIdentifier = new("^[A-Za-z0-9_]+$", RegexOptions.Compiled);
 14
 15    public async Task<int> ReloadAsync(string table, CancellationToken cancellationToken)
 16    {
 017        if (!SafeIdentifier.IsMatch(table))
 018            throw new ArgumentException($"Недопустимое имя таблицы: {table}", nameof(table));
 19
 020        await using var connection = new MySqlConnection(connectionString);
 021        await connection.OpenAsync(cancellationToken);
 22
 023        var triggers = new List<(string Name, string Timing, string Event, string Body)>();
 024        await using (var select = connection.CreateCommand())
 25        {
 026            select.CommandText = """
 027                SELECT TRIGGER_NAME, ACTION_TIMING, EVENT_MANIPULATION, ACTION_STATEMENT
 028                FROM information_schema.TRIGGERS
 029                WHERE TRIGGER_SCHEMA = DATABASE() AND EVENT_OBJECT_TABLE = @table
 030                ORDER BY ACTION_TIMING, EVENT_MANIPULATION, ACTION_ORDER
 031                """;
 032            select.Parameters.AddWithValue("@table", table);
 033            await using var reader = await select.ExecuteReaderAsync(cancellationToken);
 034            while (await reader.ReadAsync(cancellationToken))
 035                triggers.Add((reader.GetString(0), reader.GetString(1), reader.GetString(2), reader.GetString(3)));
 036        }
 37
 038        if (triggers.Count == 0)
 039            return 0;
 40
 41        // DDL берёт эксклюзивный MDL на таблицу: не встаём в очередь надолго, если таблица занята.
 042        await using (var timeout = connection.CreateCommand())
 43        {
 044            timeout.CommandText = "SET SESSION lock_wait_timeout = 10";
 045            await timeout.ExecuteNonQueryAsync(cancellationToken);
 46        }
 47
 048        foreach (var (name, timing, @event, body) in triggers)
 49        {
 050            if (!SafeIdentifier.IsMatch(name))
 51                continue;
 52
 053            await using var ddl = connection.CreateCommand();
 054            ddl.CommandText = $"CREATE OR REPLACE TRIGGER `{name}` {timing} {@event} ON `{table}` FOR EACH ROW {body}";
 055            ddl.CommandTimeout = 30;
 056            await ddl.ExecuteNonQueryAsync(cancellationToken);
 057        }
 58
 059        return triggers.Count;
 060    }
 61}