< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Data.Persistence.AttemptNoticeBudgets
Assembly: ArturRios.Heimdall.Data
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/AttemptNoticeBudgets.cs
Line coverage
100%
Covered lines: 14
Uncovered lines: 0
Coverable lines: 14
Total lines: 50
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage
100%
Covered methods: 2
Fully covered methods: 2
Total methods: 2
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
TryChargeAsync()100%11100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/AttemptNoticeBudgets.cs

#LineLine coverage
 1using ArturRios.Heimdall.Data.Configuration;
 2using ArturRios.Heimdall.Domain.Entities;
 3using ArturRios.Heimdall.Domain.Persistence;
 4using Microsoft.EntityFrameworkCore;
 5
 6namespace ArturRios.Heimdall.Data.Persistence;
 7
 8/// <summary>
 9///     <see cref="IAttemptNoticeBudgets" /> on PostgreSQL: one <c>INSERT … ON CONFLICT … DO UPDATE …
 10///     WHERE</c> per notice.
 11/// </summary>
 12/// <remarks>
 13///     <para>
 14///         An address never charged has no row, so the first charge inserts one and every later charge
 15///         updates it. Reading first and then choosing would race: two first notices would both insert,
 16///         and two notices at the limit would both read "two sent". The upsert leaves it to the
 17///         database. A new address is inserted with one notice; a known one is updated only where the
 18///         <c>WHERE</c> admits it — its window has ended (it starts again at one) or it has had fewer
 19///         than the maximum — and every right-hand side reads the row as it was.
 20///     </para>
 21///     <para>
 22///         The affected-row count is the answer. A refused update affects no row; an insert or an
 23///         admitted update affects one. A competing upsert on the same address waits for the first to
 24///         commit and then evaluates the <c>WHERE</c> against the row as that one left it, as the
 25///         conditional updates of <see cref="AtomicWrites" /> do, so no burst admits more than the
 26///         budget has left.
 27///     </para>
 28/// </remarks>
 4229public sealed class AttemptNoticeBudgets(AppDbContext context) : IAttemptNoticeBudgets
 30{
 31    public async Task<bool> TryChargeAsync(string addressHash, int maxNotices, DateTime now, DateTime windowEndsAt)
 32    {
 33        // The table name comes from the model, never from input, so building it into the SQL is safe;
 34        // the values stay parameters.
 2735        var table = context.Model.FindEntityType(typeof(AttemptNoticeBudget))!.GetSchemaQualifiedTableName();
 36
 2737        var sql =
 2738            $$"""
 2739              INSERT INTO {{table}} AS budget (address_hash, notices_sent, window_ends_at)
 2740              VALUES ({0}, 1, {2})
 2741              ON CONFLICT (address_hash) DO UPDATE SET
 2742                  notices_sent = CASE WHEN budget.window_ends_at <= {1} THEN 1 ELSE budget.notices_sent + 1 END,
 2743                  window_ends_at = CASE WHEN budget.window_ends_at <= {1} THEN EXCLUDED.window_ends_at
 2744                                        ELSE budget.window_ends_at END
 2745              WHERE budget.window_ends_at <= {1} OR budget.notices_sent < {3}
 2746              """;
 47
 2748        return await context.Database.ExecuteSqlRawAsync(sql, addressHash, now, windowEndsAt, maxNotices) == 1;
 2749    }
 50}