< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.Data.Persistence.ErasedSubjects
Assembly: ArturRios.Heimdall.Data
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/ErasedSubjects.cs
Line coverage
94%
Covered lines: 16
Uncovered lines: 1
Coverable lines: 17
Total lines: 54
Line coverage: 94.1%
Branch coverage
50%
Covered branches: 1
Total branches: 2
Branch coverage: 50%
Method coverage
100%
Covered methods: 2
Fully covered methods: 1
Total methods: 2
Method coverage: 100%
Full method coverage: 50%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
RecordAsync()50%2291.66%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Infrastructure/ArturRios.Heimdall.Data/Persistence/ErasedSubjects.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="IErasedSubjects" /> on PostgreSQL: one <c>INSERT … ON CONFLICT</c> for the whole set.
 10/// </summary>
 11/// <remarks>
 12///     <para>
 13///         A tracked insert would fail on the primary key when a retried hard delete tombstones an id
 14///         again, and checking first would race a concurrent retry. <c>ON CONFLICT (public_id) DO
 15///         UPDATE</c> makes the database settle it: a new id is inserted, a known one keeps its row and
 16///         takes the new <c>created_at</c>, so the grace period counts from the attempt that goes on to
 17///         delete the row.
 18///     </para>
 19///     <para>
 20///         The ids travel as arrays and are unnested, so a scope's members are tombstoned in one
 21///         statement however many there are, and all or none of them are. The statement bypasses the
 22///         change tracker; nothing in a hard delete reads the tombstones back.
 23///     </para>
 24/// </remarks>
 2125public sealed class ErasedSubjects(AppDbContext context) : IErasedSubjects
 26{
 27    public async Task RecordAsync(IReadOnlyCollection<ErasedSubject> subjects)
 28    {
 1329        if (subjects.Count == 0)
 30        {
 031            return;
 32        }
 33
 34        // The table name comes from the model, never from input, so building it into the SQL is safe;
 35        // the values stay parameters.
 1336        var table = context.Model.FindEntityType(typeof(ErasedSubject))!.GetSchemaQualifiedTableName();
 37
 38        // One row per id, as ON CONFLICT cannot touch the same row twice in one statement.
 3039        var distinct = subjects.DistinctBy(subject => subject.Id).ToList();
 40
 1341        var sql =
 1342            $$"""
 1343              INSERT INTO {{table}} (public_id, kind, created_at)
 1344              SELECT * FROM unnest({0}::uuid[], {1}::integer[], {2}::timestamptz[])
 1345              ON CONFLICT (public_id) DO UPDATE SET created_at = EXCLUDED.created_at
 1346              """;
 47
 1348        await context.Database.ExecuteSqlRawAsync(
 1349            sql,
 1650            distinct.Select(subject => subject.Id).ToArray(),
 1651            distinct.Select(subject => subject.Kind).ToArray(),
 2952            distinct.Select(subject => subject.CreatedAt).ToArray());
 1353    }
 54}