wip: doctor/cli/docs/api to vector db consolidation; api hardening for descriptions, tenant, and scopes; migrations and conversions of all DALs to EF v10
This commit is contained in:
@@ -8,13 +8,36 @@
|
||||
- `docs/operations/artifact-migration-runbook.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
- `docs/technical/testing/TEST_SUITE_OVERVIEW.md`
|
||||
- `docs/db/EF_CORE_MODEL_GENERATION_STANDARDS.md`
|
||||
- `docs/db/EF_CORE_RUNTIME_CUTOVER_STRATEGY.md`
|
||||
|
||||
## Working Agreements
|
||||
- Deterministic outputs (ordering, timestamps, hashing).
|
||||
- Offline-friendly; avoid runtime network calls.
|
||||
- Note cross-module impacts in the active sprint tracker.
|
||||
|
||||
## DAL Technology
|
||||
- **EF Core v10** for PostgreSQL artifact index repository (converted from raw Npgsql in Sprint 077).
|
||||
- Schema: `evidence` (shared with Evidence.Persistence module).
|
||||
- SQL migrations remain authoritative; no EF auto-migrations at runtime.
|
||||
- Compiled model used for default schema path; reflection-based model building for non-default schemas.
|
||||
- UPSERT operations use `ExecuteSqlRawAsync` for the multi-column ON CONFLICT pattern.
|
||||
|
||||
## EF Core Directory Structure
|
||||
```
|
||||
EfCore/
|
||||
Context/
|
||||
ArtifactDbContext.cs # Main DbContext
|
||||
ArtifactDesignTimeDbContextFactory.cs # For dotnet ef CLI
|
||||
Models/
|
||||
ArtifactIndexEntity.cs # Entity POCO
|
||||
CompiledModels/
|
||||
ArtifactDbContextModel.cs # Compiled model stub
|
||||
Postgres/
|
||||
ArtifactDbContextFactory.cs # Runtime factory with UseModel()
|
||||
```
|
||||
|
||||
## Testing Expectations
|
||||
- Add or update unit tests under `src/__Libraries/__Tests`.
|
||||
- Run `dotnet test` for affected test projects when changes are made.
|
||||
|
||||
- Build sequentially (`-p:BuildInParallel=false` or `--no-dependencies`).
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
|
||||
#pragma warning disable 219, 612, 618
|
||||
#nullable disable
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure.EfCore.CompiledModels;
|
||||
|
||||
/// <summary>
|
||||
/// Compiled model stub for ArtifactDbContext.
|
||||
/// This is a placeholder that delegates to runtime model building.
|
||||
/// Replace with output from <c>dotnet ef dbcontext optimize</c> when a provisioned DB is available.
|
||||
/// </summary>
|
||||
[DbContext(typeof(Context.ArtifactDbContext))]
|
||||
public partial class ArtifactDbContextModel : RuntimeModel
|
||||
{
|
||||
private static ArtifactDbContextModel _instance;
|
||||
|
||||
public static IModel Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance == null)
|
||||
{
|
||||
_instance = new ArtifactDbContextModel();
|
||||
_instance.Initialize();
|
||||
_instance.Customize();
|
||||
}
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
partial void Initialize();
|
||||
|
||||
partial void Customize();
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StellaOps.Artifact.Infrastructure.EfCore.Models;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure.EfCore.Context;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core DbContext for the Artifact Infrastructure module.
|
||||
/// Maps to the evidence PostgreSQL schema: artifact_index table.
|
||||
/// Scaffolded from SQL migration 001_artifact_index_schema.sql.
|
||||
/// </summary>
|
||||
public partial class ArtifactDbContext : DbContext
|
||||
{
|
||||
private readonly string _schemaName;
|
||||
|
||||
public ArtifactDbContext(DbContextOptions<ArtifactDbContext> options, string? schemaName = null)
|
||||
: base(options)
|
||||
{
|
||||
_schemaName = string.IsNullOrWhiteSpace(schemaName)
|
||||
? "evidence"
|
||||
: schemaName.Trim();
|
||||
}
|
||||
|
||||
public virtual DbSet<ArtifactIndexEntity> ArtifactIndexes { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var schemaName = _schemaName;
|
||||
|
||||
// -- artifact_index --------------------------------------------------
|
||||
modelBuilder.Entity<ArtifactIndexEntity>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id).HasName("artifact_index_pkey");
|
||||
entity.ToTable("artifact_index", schemaName);
|
||||
|
||||
// Unique constraint for UPSERT conflict target
|
||||
entity.HasAlternateKey(e => new { e.TenantId, e.BomRef, e.SerialNumber, e.ArtifactId })
|
||||
.HasName("uq_artifact_index_key");
|
||||
|
||||
// Indexes matching SQL migration
|
||||
entity.HasIndex(e => new { e.TenantId, e.BomRef }, "idx_artifact_index_bom_ref")
|
||||
.HasFilter("(NOT is_deleted)");
|
||||
|
||||
entity.HasIndex(e => e.Sha256, "idx_artifact_index_sha256")
|
||||
.HasFilter("(NOT is_deleted)");
|
||||
|
||||
entity.HasIndex(e => new { e.TenantId, e.ArtifactType }, "idx_artifact_index_type")
|
||||
.HasFilter("(NOT is_deleted)");
|
||||
|
||||
entity.HasIndex(e => new { e.TenantId, e.BomRef, e.SerialNumber }, "idx_artifact_index_serial")
|
||||
.HasFilter("(NOT is_deleted)");
|
||||
|
||||
entity.HasIndex(e => new { e.TenantId, e.CreatedAt }, "idx_artifact_index_created")
|
||||
.IsDescending(false, true)
|
||||
.HasFilter("(NOT is_deleted)");
|
||||
|
||||
// Column mappings
|
||||
entity.Property(e => e.Id)
|
||||
.HasDefaultValueSql("gen_random_uuid()")
|
||||
.HasColumnName("id");
|
||||
|
||||
entity.Property(e => e.TenantId)
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
entity.Property(e => e.BomRef)
|
||||
.HasColumnName("bom_ref");
|
||||
|
||||
entity.Property(e => e.SerialNumber)
|
||||
.HasColumnName("serial_number");
|
||||
|
||||
entity.Property(e => e.ArtifactId)
|
||||
.HasColumnName("artifact_id");
|
||||
|
||||
entity.Property(e => e.StorageKey)
|
||||
.HasColumnName("storage_key");
|
||||
|
||||
entity.Property(e => e.ArtifactType)
|
||||
.HasColumnName("artifact_type");
|
||||
|
||||
entity.Property(e => e.ContentType)
|
||||
.HasColumnName("content_type");
|
||||
|
||||
entity.Property(e => e.Sha256)
|
||||
.HasColumnName("sha256");
|
||||
|
||||
entity.Property(e => e.SizeBytes)
|
||||
.HasColumnName("size_bytes");
|
||||
|
||||
entity.Property(e => e.CreatedAt)
|
||||
.HasDefaultValueSql("now()")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
entity.Property(e => e.UpdatedAt)
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
entity.Property(e => e.IsDeleted)
|
||||
.HasDefaultValue(false)
|
||||
.HasColumnName("is_deleted");
|
||||
|
||||
entity.Property(e => e.DeletedAt)
|
||||
.HasColumnName("deleted_at");
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure.EfCore.Context;
|
||||
|
||||
/// <summary>
|
||||
/// Design-time factory for <see cref="ArtifactDbContext"/>.
|
||||
/// Used by dotnet ef CLI tooling (scaffold, optimize, migrations).
|
||||
/// </summary>
|
||||
public sealed class ArtifactDesignTimeDbContextFactory : IDesignTimeDbContextFactory<ArtifactDbContext>
|
||||
{
|
||||
private const string DefaultConnectionString =
|
||||
"Host=localhost;Port=55433;Database=postgres;Username=postgres;Password=postgres;Search Path=evidence,public";
|
||||
|
||||
private const string ConnectionStringEnvironmentVariable = "STELLAOPS_ARTIFACT_EF_CONNECTION";
|
||||
|
||||
public ArtifactDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
var connectionString = ResolveConnectionString();
|
||||
var options = new DbContextOptionsBuilder<ArtifactDbContext>()
|
||||
.UseNpgsql(connectionString)
|
||||
.Options;
|
||||
|
||||
return new ArtifactDbContext(options);
|
||||
}
|
||||
|
||||
private static string ResolveConnectionString()
|
||||
{
|
||||
var fromEnvironment = Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable);
|
||||
return string.IsNullOrWhiteSpace(fromEnvironment) ? DefaultConnectionString : fromEnvironment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace StellaOps.Artifact.Infrastructure.EfCore.Models;
|
||||
|
||||
/// <summary>
|
||||
/// EF Core entity for the evidence.artifact_index table.
|
||||
/// Scaffolded from SQL migration 001_artifact_index_schema.sql.
|
||||
/// </summary>
|
||||
public partial class ArtifactIndexEntity
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid TenantId { get; set; }
|
||||
public string BomRef { get; set; } = null!;
|
||||
public string SerialNumber { get; set; } = null!;
|
||||
public string ArtifactId { get; set; } = null!;
|
||||
public string StorageKey { get; set; } = null!;
|
||||
public string ArtifactType { get; set; } = null!;
|
||||
public string ContentType { get; set; } = null!;
|
||||
public string Sha256 { get; set; } = null!;
|
||||
public long SizeBytes { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
public bool IsDeleted { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
using StellaOps.Artifact.Infrastructure.EfCore.CompiledModels;
|
||||
using StellaOps.Artifact.Infrastructure.EfCore.Context;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure.Postgres;
|
||||
|
||||
/// <summary>
|
||||
/// Runtime factory for creating <see cref="ArtifactDbContext"/> instances.
|
||||
/// Uses the static compiled model when schema matches the default; falls back to
|
||||
/// reflection-based model building for non-default schemas (integration tests).
|
||||
/// </summary>
|
||||
internal static class ArtifactDbContextFactory
|
||||
{
|
||||
public static ArtifactDbContext Create(NpgsqlConnection connection, int commandTimeoutSeconds, string schemaName)
|
||||
{
|
||||
var normalizedSchema = string.IsNullOrWhiteSpace(schemaName)
|
||||
? ArtifactDataSource.DefaultSchemaName
|
||||
: schemaName.Trim();
|
||||
|
||||
var optionsBuilder = new DbContextOptionsBuilder<ArtifactDbContext>()
|
||||
.UseNpgsql(connection, npgsql => npgsql.CommandTimeout(commandTimeoutSeconds));
|
||||
|
||||
if (string.Equals(normalizedSchema, ArtifactDataSource.DefaultSchemaName, StringComparison.Ordinal))
|
||||
{
|
||||
// Use the static compiled model when schema mapping matches the default model.
|
||||
optionsBuilder.UseModel(ArtifactDbContextModel.Instance);
|
||||
}
|
||||
|
||||
return new ArtifactDbContext(optionsBuilder.Options, normalizedSchema);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PostgresArtifactIndexRepository.Find.cs
|
||||
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
|
||||
// Task: AS-003 - Create ArtifactStore PostgreSQL index
|
||||
// Description: Query operations for the artifact repository
|
||||
// Sprint: SPRINT_20260222_077_Artifact_infrastructure_dal_to_efcore
|
||||
// Task: ARTIF-EF-03 - Convert DAL repositories to EF Core
|
||||
// Description: Query operations for the artifact repository (EF Core)
|
||||
// -----------------------------------------------------------------------------
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StellaOps.Artifact.Core;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure;
|
||||
@@ -13,11 +14,16 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ArtifactIndexEntry>> FindByBomRefAsync(string bomRef, CancellationToken ct = default)
|
||||
{
|
||||
return await QueryAsync(_tenantKey, ArtifactIndexSql.SelectByBomRef, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", _tenantId);
|
||||
AddParameter(cmd, "bom_ref", bomRef);
|
||||
}, MapEntry, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateReadContextAsync(ct);
|
||||
|
||||
var entities = await dbContext.ArtifactIndexes
|
||||
.AsNoTracking()
|
||||
.Where(e => e.TenantId == _tenantId && e.BomRef == bomRef && !e.IsDeleted)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entities.Select(MapToEntry).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -26,21 +32,32 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
string serialNumber,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
return await QueryAsync(_tenantKey, ArtifactIndexSql.SelectByBomRefAndSerial, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", _tenantId);
|
||||
AddParameter(cmd, "bom_ref", bomRef);
|
||||
AddParameter(cmd, "serial_number", serialNumber);
|
||||
}, MapEntry, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateReadContextAsync(ct);
|
||||
|
||||
var entities = await dbContext.ArtifactIndexes
|
||||
.AsNoTracking()
|
||||
.Where(e => e.TenantId == _tenantId && e.BomRef == bomRef && e.SerialNumber == serialNumber && !e.IsDeleted)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entities.Select(MapToEntry).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<ArtifactIndexEntry>> FindBySha256Async(string sha256, CancellationToken ct = default)
|
||||
{
|
||||
return await QueryAsync(_tenantKey, ArtifactIndexSql.SelectBySha256, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "sha256", sha256);
|
||||
}, MapEntry, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateReadContextAsync(ct);
|
||||
|
||||
var entities = await dbContext.ArtifactIndexes
|
||||
.AsNoTracking()
|
||||
.Where(e => e.Sha256 == sha256 && !e.IsDeleted)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.Take(100)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entities.Select(MapToEntry).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -51,12 +68,19 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var tenantKey = tenantId.ToString("D");
|
||||
return await QueryAsync(tenantKey, ArtifactIndexSql.SelectByType, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "artifact_type", type.ToString());
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapEntry, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateReadContextAsync(tenantKey, ct);
|
||||
|
||||
var typeString = type.ToString();
|
||||
|
||||
var entities = await dbContext.ArtifactIndexes
|
||||
.AsNoTracking()
|
||||
.Where(e => e.TenantId == tenantId && e.ArtifactType == typeString && !e.IsDeleted)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entities.Select(MapToEntry).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,12 +94,19 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var tenantKey = tenantId.ToString("D");
|
||||
return await QueryAsync(tenantKey, ArtifactIndexSql.SelectByTimeRange, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
AddParameter(cmd, "from", from);
|
||||
AddParameter(cmd, "to", to);
|
||||
AddParameter(cmd, "limit", limit);
|
||||
}, MapEntry, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateReadContextAsync(tenantKey, ct);
|
||||
|
||||
var fromUtc = from.UtcDateTime;
|
||||
var toUtc = to.UtcDateTime;
|
||||
|
||||
var entities = await dbContext.ArtifactIndexes
|
||||
.AsNoTracking()
|
||||
.Where(e => e.TenantId == tenantId && e.CreatedAt >= fromUtc && e.CreatedAt < toUtc && !e.IsDeleted)
|
||||
.OrderByDescending(e => e.CreatedAt)
|
||||
.Take(limit)
|
||||
.ToListAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entities.Select(MapToEntry).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PostgresArtifactIndexRepository.Index.cs
|
||||
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
|
||||
// Task: AS-003 - Create ArtifactStore PostgreSQL index
|
||||
// Description: Index write operations for the artifact repository
|
||||
// Sprint: SPRINT_20260222_077_Artifact_infrastructure_dal_to_efcore
|
||||
// Task: ARTIF-EF-03 - Convert DAL repositories to EF Core
|
||||
// Description: Index write operations for the artifact repository (EF Core)
|
||||
// -----------------------------------------------------------------------------
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure;
|
||||
|
||||
public sealed partial class PostgresArtifactIndexRepository
|
||||
@@ -12,22 +14,42 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
public async Task IndexAsync(ArtifactIndexEntry entry, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(entry);
|
||||
await using var connection = await DataSource.OpenConnectionAsync(_tenantKey, "writer", ct)
|
||||
.ConfigureAwait(false);
|
||||
await using var command = CreateCommand(ArtifactIndexSql.Insert, connection);
|
||||
|
||||
AddParameter(command, "id", entry.Id);
|
||||
AddParameter(command, "tenant_id", entry.TenantId);
|
||||
AddParameter(command, "bom_ref", entry.BomRef);
|
||||
AddParameter(command, "serial_number", entry.SerialNumber);
|
||||
AddParameter(command, "artifact_id", entry.ArtifactId);
|
||||
AddParameter(command, "storage_key", entry.StorageKey);
|
||||
AddParameter(command, "artifact_type", entry.Type.ToString());
|
||||
AddParameter(command, "content_type", entry.ContentType);
|
||||
AddParameter(command, "sha256", entry.Sha256);
|
||||
AddParameter(command, "size_bytes", entry.SizeBytes);
|
||||
AddParameter(command, "created_at", entry.CreatedAt);
|
||||
// The original SQL used INSERT ... ON CONFLICT DO UPDATE (multi-column conflict clause).
|
||||
// Using ExecuteSqlRawAsync to preserve the exact upsert semantics per cutover strategy guidance.
|
||||
await using var dbContext = await CreateWriteContextAsync(ct);
|
||||
|
||||
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"""
|
||||
INSERT INTO evidence.artifact_index (
|
||||
id, tenant_id, bom_ref, serial_number, artifact_id, storage_key,
|
||||
artifact_type, content_type, sha256, size_bytes, created_at
|
||||
) VALUES (
|
||||
{0}, {1}, {2}, {3}, {4}, {5},
|
||||
{6}, {7}, {8}, {9}, {10}
|
||||
)
|
||||
ON CONFLICT (tenant_id, bom_ref, serial_number, artifact_id)
|
||||
DO UPDATE SET
|
||||
storage_key = EXCLUDED.storage_key,
|
||||
artifact_type = EXCLUDED.artifact_type,
|
||||
content_type = EXCLUDED.content_type,
|
||||
sha256 = EXCLUDED.sha256,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
updated_at = NOW(),
|
||||
is_deleted = FALSE,
|
||||
deleted_at = NULL
|
||||
""",
|
||||
entry.Id,
|
||||
entry.TenantId,
|
||||
entry.BomRef,
|
||||
entry.SerialNumber,
|
||||
entry.ArtifactId,
|
||||
entry.StorageKey,
|
||||
entry.Type.ToString(),
|
||||
entry.ContentType,
|
||||
entry.Sha256,
|
||||
entry.SizeBytes,
|
||||
entry.CreatedAt.UtcDateTime,
|
||||
ct).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +1,73 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PostgresArtifactIndexRepository.Mapping.cs
|
||||
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
|
||||
// Task: AS-003 - Create ArtifactStore PostgreSQL index
|
||||
// Description: Row mapping helpers for artifact index repository
|
||||
// Sprint: SPRINT_20260222_077_Artifact_infrastructure_dal_to_efcore
|
||||
// Task: ARTIF-EF-03 - Convert DAL repositories to EF Core
|
||||
// Description: Mapping helpers between EF Core entities and domain models
|
||||
// -----------------------------------------------------------------------------
|
||||
using Npgsql;
|
||||
using StellaOps.Artifact.Core;
|
||||
using StellaOps.Artifact.Infrastructure.EfCore.Models;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure;
|
||||
|
||||
public sealed partial class PostgresArtifactIndexRepository
|
||||
{
|
||||
private static ArtifactIndexEntry MapEntry(NpgsqlDataReader reader)
|
||||
private static ArtifactIndexEntry MapToEntry(ArtifactIndexEntity entity)
|
||||
{
|
||||
var artifactTypeString = reader.GetString(6);
|
||||
var artifactType = Enum.TryParse<ArtifactType>(artifactTypeString, out var parsedType)
|
||||
var artifactType = Enum.TryParse<ArtifactType>(entity.ArtifactType, out var parsedType)
|
||||
? parsedType
|
||||
: ArtifactType.Unknown;
|
||||
|
||||
return new ArtifactIndexEntry
|
||||
{
|
||||
Id = reader.GetGuid(0),
|
||||
TenantId = reader.GetGuid(1),
|
||||
BomRef = reader.GetString(2),
|
||||
SerialNumber = reader.GetString(3),
|
||||
ArtifactId = reader.GetString(4),
|
||||
StorageKey = reader.GetString(5),
|
||||
Id = entity.Id,
|
||||
TenantId = entity.TenantId,
|
||||
BomRef = entity.BomRef,
|
||||
SerialNumber = entity.SerialNumber,
|
||||
ArtifactId = entity.ArtifactId,
|
||||
StorageKey = entity.StorageKey,
|
||||
Type = artifactType,
|
||||
ContentType = reader.GetString(7),
|
||||
Sha256 = reader.GetString(8),
|
||||
SizeBytes = reader.GetInt64(9),
|
||||
CreatedAt = reader.GetFieldValue<DateTimeOffset>(10),
|
||||
UpdatedAt = reader.IsDBNull(11) ? null : reader.GetFieldValue<DateTimeOffset>(11),
|
||||
IsDeleted = reader.GetBoolean(12),
|
||||
DeletedAt = reader.IsDBNull(13) ? null : reader.GetFieldValue<DateTimeOffset>(13)
|
||||
ContentType = entity.ContentType,
|
||||
Sha256 = entity.Sha256,
|
||||
SizeBytes = entity.SizeBytes,
|
||||
CreatedAt = entity.CreatedAt.Kind == DateTimeKind.Utc
|
||||
? new DateTimeOffset(entity.CreatedAt, TimeSpan.Zero)
|
||||
: new DateTimeOffset(DateTime.SpecifyKind(entity.CreatedAt, DateTimeKind.Utc), TimeSpan.Zero),
|
||||
UpdatedAt = entity.UpdatedAt.HasValue
|
||||
? new DateTimeOffset(
|
||||
entity.UpdatedAt.Value.Kind == DateTimeKind.Utc
|
||||
? entity.UpdatedAt.Value
|
||||
: DateTime.SpecifyKind(entity.UpdatedAt.Value, DateTimeKind.Utc),
|
||||
TimeSpan.Zero)
|
||||
: null,
|
||||
IsDeleted = entity.IsDeleted,
|
||||
DeletedAt = entity.DeletedAt.HasValue
|
||||
? new DateTimeOffset(
|
||||
entity.DeletedAt.Value.Kind == DateTimeKind.Utc
|
||||
? entity.DeletedAt.Value
|
||||
: DateTime.SpecifyKind(entity.DeletedAt.Value, DateTimeKind.Utc),
|
||||
TimeSpan.Zero)
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
private static ArtifactIndexEntity MapToEntity(ArtifactIndexEntry entry)
|
||||
{
|
||||
return new ArtifactIndexEntity
|
||||
{
|
||||
Id = entry.Id,
|
||||
TenantId = entry.TenantId,
|
||||
BomRef = entry.BomRef,
|
||||
SerialNumber = entry.SerialNumber,
|
||||
ArtifactId = entry.ArtifactId,
|
||||
StorageKey = entry.StorageKey,
|
||||
ArtifactType = entry.Type.ToString(),
|
||||
ContentType = entry.ContentType,
|
||||
Sha256 = entry.Sha256,
|
||||
SizeBytes = entry.SizeBytes,
|
||||
CreatedAt = entry.CreatedAt.UtcDateTime,
|
||||
UpdatedAt = entry.UpdatedAt?.UtcDateTime,
|
||||
IsDeleted = entry.IsDeleted,
|
||||
DeletedAt = entry.DeletedAt?.UtcDateTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PostgresArtifactIndexRepository.Mutate.cs
|
||||
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
|
||||
// Task: AS-003 - Create ArtifactStore PostgreSQL index
|
||||
// Description: Mutation operations for the artifact repository
|
||||
// Sprint: SPRINT_20260222_077_Artifact_infrastructure_dal_to_efcore
|
||||
// Task: ARTIF-EF-03 - Convert DAL repositories to EF Core
|
||||
// Description: Mutation operations for the artifact repository (EF Core)
|
||||
// -----------------------------------------------------------------------------
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure;
|
||||
|
||||
public sealed partial class PostgresArtifactIndexRepository
|
||||
@@ -15,15 +17,19 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
string artifactId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var results = await QueryAsync(_tenantKey, ArtifactIndexSql.SelectByKey, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", _tenantId);
|
||||
AddParameter(cmd, "bom_ref", bomRef);
|
||||
AddParameter(cmd, "serial_number", serialNumber);
|
||||
AddParameter(cmd, "artifact_id", artifactId);
|
||||
}, MapEntry, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateReadContextAsync(ct);
|
||||
|
||||
return results.Count > 0 ? results[0] : null;
|
||||
var entity = await dbContext.ArtifactIndexes
|
||||
.AsNoTracking()
|
||||
.Where(e => e.TenantId == _tenantId
|
||||
&& e.BomRef == bomRef
|
||||
&& e.SerialNumber == serialNumber
|
||||
&& e.ArtifactId == artifactId
|
||||
&& !e.IsDeleted)
|
||||
.FirstOrDefaultAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return entity is null ? null : MapToEntry(entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -33,13 +39,20 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
string artifactId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var rowsAffected = await ExecuteAsync(_tenantKey, ArtifactIndexSql.UpdateSoftDelete, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", _tenantId);
|
||||
AddParameter(cmd, "bom_ref", bomRef);
|
||||
AddParameter(cmd, "serial_number", serialNumber);
|
||||
AddParameter(cmd, "artifact_id", artifactId);
|
||||
}, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateWriteContextAsync(ct);
|
||||
|
||||
var rowsAffected = await dbContext.ArtifactIndexes
|
||||
.Where(e => e.TenantId == _tenantId
|
||||
&& e.BomRef == bomRef
|
||||
&& e.SerialNumber == serialNumber
|
||||
&& e.ArtifactId == artifactId
|
||||
&& !e.IsDeleted)
|
||||
.ExecuteUpdateAsync(setters => setters
|
||||
.SetProperty(e => e.IsDeleted, true)
|
||||
.SetProperty(e => e.DeletedAt, DateTime.UtcNow)
|
||||
.SetProperty(e => e.UpdatedAt, DateTime.UtcNow),
|
||||
ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
@@ -50,11 +63,14 @@ public sealed partial class PostgresArtifactIndexRepository
|
||||
public async Task<int> CountAsync(Guid tenantId, CancellationToken ct = default)
|
||||
{
|
||||
var tenantKey = tenantId.ToString("D");
|
||||
var result = await ExecuteScalarAsync<long>(tenantKey, ArtifactIndexSql.CountByTenant, cmd =>
|
||||
{
|
||||
AddParameter(cmd, "tenant_id", tenantId);
|
||||
}, ct).ConfigureAwait(false);
|
||||
await using var dbContext = await CreateReadContextAsync(tenantKey, ct);
|
||||
|
||||
return (int)result;
|
||||
var count = await dbContext.ArtifactIndexes
|
||||
.AsNoTracking()
|
||||
.Where(e => e.TenantId == tenantId && !e.IsDeleted)
|
||||
.LongCountAsync(ct)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return (int)count;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// PostgresArtifactIndexRepository.cs
|
||||
// Sprint: SPRINT_20260118_017_Evidence_artifact_store_unification
|
||||
// Task: AS-003 - Create ArtifactStore PostgreSQL index
|
||||
// Description: PostgreSQL implementation of artifact index repository
|
||||
// Sprint: SPRINT_20260222_077_Artifact_infrastructure_dal_to_efcore
|
||||
// Task: ARTIF-EF-03 - Convert DAL repositories to EF Core
|
||||
// Description: PostgreSQL (EF Core) implementation of artifact index repository
|
||||
// -----------------------------------------------------------------------------
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Infrastructure.Postgres.Repositories;
|
||||
using StellaOps.Artifact.Infrastructure.EfCore.Context;
|
||||
using StellaOps.Artifact.Infrastructure.Postgres;
|
||||
|
||||
namespace StellaOps.Artifact.Infrastructure;
|
||||
|
||||
/// <summary>
|
||||
/// PostgreSQL implementation of <see cref="IArtifactIndexRepository"/>.
|
||||
/// PostgreSQL (EF Core) implementation of <see cref="IArtifactIndexRepository"/>.
|
||||
/// </summary>
|
||||
public sealed partial class PostgresArtifactIndexRepository : RepositoryBase<ArtifactDataSource>, IArtifactIndexRepository
|
||||
public sealed partial class PostgresArtifactIndexRepository : IArtifactIndexRepository
|
||||
{
|
||||
private readonly ArtifactDataSource _dataSource;
|
||||
private readonly ILogger<PostgresArtifactIndexRepository> _logger;
|
||||
private readonly Guid _tenantId;
|
||||
private readonly string _tenantKey;
|
||||
|
||||
@@ -21,10 +24,38 @@ public sealed partial class PostgresArtifactIndexRepository : RepositoryBase<Art
|
||||
ArtifactDataSource dataSource,
|
||||
ILogger<PostgresArtifactIndexRepository> logger,
|
||||
IArtifactTenantContext tenantContext)
|
||||
: base(dataSource, logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dataSource);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
ArgumentNullException.ThrowIfNull(tenantContext);
|
||||
_dataSource = dataSource;
|
||||
_logger = logger;
|
||||
_tenantId = tenantContext.TenantId;
|
||||
_tenantKey = tenantContext.TenantIdValue;
|
||||
}
|
||||
|
||||
private int CommandTimeoutSeconds => _dataSource.CommandTimeoutSeconds;
|
||||
|
||||
private string GetSchemaName() => ArtifactDataSource.DefaultSchemaName;
|
||||
|
||||
private async Task<ArtifactDbContext> CreateReadContextAsync(CancellationToken ct)
|
||||
{
|
||||
var connection = await _dataSource.OpenConnectionAsync(_tenantKey, "reader", ct)
|
||||
.ConfigureAwait(false);
|
||||
return ArtifactDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
|
||||
}
|
||||
|
||||
private async Task<ArtifactDbContext> CreateWriteContextAsync(CancellationToken ct)
|
||||
{
|
||||
var connection = await _dataSource.OpenConnectionAsync(_tenantKey, "writer", ct)
|
||||
.ConfigureAwait(false);
|
||||
return ArtifactDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
|
||||
}
|
||||
|
||||
private async Task<ArtifactDbContext> CreateReadContextAsync(string tenantKey, CancellationToken ct)
|
||||
{
|
||||
var connection = await _dataSource.OpenConnectionAsync(tenantKey, "reader", ct)
|
||||
.ConfigureAwait(false);
|
||||
return ArtifactDbContextFactory.Create(connection, CommandTimeoutSeconds, GetSchemaName());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.S3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
|
||||
<PackageReference Include="Npgsql" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options" />
|
||||
</ItemGroup>
|
||||
@@ -22,10 +25,16 @@
|
||||
<ProjectReference Include="..\StellaOps.Artifact.Core\StellaOps.Artifact.Core.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Determinism.Abstractions\StellaOps.Determinism.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
|
||||
<ProjectReference Include="..\StellaOps.Infrastructure.EfCore\StellaOps.Infrastructure.EfCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Migrations\**\*.sql" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Prevent automatic compiled-model binding so non-default schemas can build runtime models. -->
|
||||
<Compile Remove="EfCore\CompiledModels\ArtifactDbContextAssemblyAttributes.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
# StellaOps.Artifact.Infrastructure Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
Source of truth: `docs/implplan/SPRINT_20260222_077_Artifact_infrastructure_dal_to_efcore.md`.
|
||||
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | DONE | Remediation complete; split store/migration/index, tenant context + deterministic time/ID, S3 integration tests added; dotnet test src/__Libraries/StellaOps.Artifact.Core.Tests/StellaOps.Artifact.Core.Tests.csproj passed 2026-02-03 (25 tests, MTP0001 warning). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| ARTIF-EF-01 | DONE | AGENTS.md verified; migration plugin added to Evidence module (multi-source) in Platform MigrationModulePlugins.cs. |
|
||||
| ARTIF-EF-02 | DONE | EF Core model scaffolded: ArtifactDbContext, ArtifactIndexEntity, design-time factory, compiled model stub. |
|
||||
| ARTIF-EF-03 | DONE | PostgresArtifactIndexRepository converted from Npgsql/RepositoryBase to EF Core. Interface preserved. UPSERT via ExecuteSqlRawAsync per cutover strategy. |
|
||||
| ARTIF-EF-04 | DONE | Compiled model stub, design-time factory, runtime factory with UseModel() for default schema. Assembly attribute exclusion in csproj. |
|
||||
| ARTIF-EF-05 | DONE | Sequential build (0 errors, 0 warnings); tests pass (25/25); docs updated. |
|
||||
|
||||
Reference in New Issue
Block a user