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:
master
2026-02-23 15:30:50 +02:00
parent bd8fee6ed8
commit e746577380
1424 changed files with 81225 additions and 25251 deletions

View File

@@ -0,0 +1,90 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.Platform.Database.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Platform.Database.EfCore.CompiledModels
{
internal partial class ContextEnvironmentEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.Platform.Database.EfCore.Models.ContextEnvironment",
typeof(ContextEnvironment),
baseEntityType);
var environmentId = runtimeEntityType.AddProperty(
"EnvironmentId",
typeof(string),
propertyInfo: typeof(ContextEnvironment).GetProperty("EnvironmentId",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
environmentId.AddAnnotation("Relational:ColumnName", "environment_id");
var regionId = runtimeEntityType.AddProperty(
"RegionId",
typeof(string),
propertyInfo: typeof(ContextEnvironment).GetProperty("RegionId",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
regionId.AddAnnotation("Relational:ColumnName", "region_id");
var environmentType = runtimeEntityType.AddProperty(
"EnvironmentType",
typeof(string),
propertyInfo: typeof(ContextEnvironment).GetProperty("EnvironmentType",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
environmentType.AddAnnotation("Relational:ColumnName", "environment_type");
var displayName = runtimeEntityType.AddProperty(
"DisplayName",
typeof(string),
propertyInfo: typeof(ContextEnvironment).GetProperty("DisplayName",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
displayName.AddAnnotation("Relational:ColumnName", "display_name");
var sortOrder = runtimeEntityType.AddProperty(
"SortOrder",
typeof(int),
propertyInfo: typeof(ContextEnvironment).GetProperty("SortOrder",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
sortOrder.AddAnnotation("Relational:ColumnName", "sort_order");
var enabled = runtimeEntityType.AddProperty(
"Enabled",
typeof(bool),
propertyInfo: typeof(ContextEnvironment).GetProperty("Enabled",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
enabled.AddAnnotation("Relational:ColumnName", "enabled");
enabled.AddAnnotation("Relational:DefaultValue", true);
var pk = runtimeEntityType.AddKey(new[] { environmentId });
pk.AddAnnotation("Relational:Name", "context_environments_pkey");
runtimeEntityType.SetPrimaryKey(pk);
runtimeEntityType.AddIndex(new[] { regionId, sortOrder, environmentId },
"ix_platform_context_environments_region_sort");
runtimeEntityType.AddIndex(new[] { sortOrder, regionId, environmentId },
"ix_platform_context_environments_sort");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "platform");
runtimeEntityType.AddAnnotation("Relational:TableName", "context_environments");
}
}
}

View File

@@ -0,0 +1,72 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.Platform.Database.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Platform.Database.EfCore.CompiledModels
{
internal partial class ContextRegionEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.Platform.Database.EfCore.Models.ContextRegion",
typeof(ContextRegion),
baseEntityType);
var regionId = runtimeEntityType.AddProperty(
"RegionId",
typeof(string),
propertyInfo: typeof(ContextRegion).GetProperty("RegionId",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
regionId.AddAnnotation("Relational:ColumnName", "region_id");
var displayName = runtimeEntityType.AddProperty(
"DisplayName",
typeof(string),
propertyInfo: typeof(ContextRegion).GetProperty("DisplayName",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
displayName.AddAnnotation("Relational:ColumnName", "display_name");
var sortOrder = runtimeEntityType.AddProperty(
"SortOrder",
typeof(int),
propertyInfo: typeof(ContextRegion).GetProperty("SortOrder",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
sortOrder.AddAnnotation("Relational:ColumnName", "sort_order");
var enabled = runtimeEntityType.AddProperty(
"Enabled",
typeof(bool),
propertyInfo: typeof(ContextRegion).GetProperty("Enabled",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
enabled.AddAnnotation("Relational:ColumnName", "enabled");
enabled.AddAnnotation("Relational:DefaultValue", true);
var pk = runtimeEntityType.AddKey(new[] { regionId });
pk.AddAnnotation("Relational:Name", "context_regions_pkey");
runtimeEntityType.SetPrimaryKey(pk);
runtimeEntityType.AddIndex(new[] { sortOrder, regionId },
"ux_platform_context_regions_sort",
unique: true);
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "platform");
runtimeEntityType.AddAnnotation("Relational:TableName", "context_regions");
}
}
}

View File

@@ -0,0 +1,69 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.Platform.Database.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Platform.Database.EfCore.CompiledModels
{
internal partial class EnvironmentSettingEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.Platform.Database.EfCore.Models.EnvironmentSetting",
typeof(EnvironmentSetting),
baseEntityType);
var key = runtimeEntityType.AddProperty(
"Key",
typeof(string),
propertyInfo: typeof(EnvironmentSetting).GetProperty("Key",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
key.AddAnnotation("Relational:ColumnName", "key");
var value = runtimeEntityType.AddProperty(
"Value",
typeof(string),
propertyInfo: typeof(EnvironmentSetting).GetProperty("Value",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
value.AddAnnotation("Relational:ColumnName", "value");
var updatedAt = runtimeEntityType.AddProperty(
"UpdatedAt",
typeof(DateTime),
propertyInfo: typeof(EnvironmentSetting).GetProperty("UpdatedAt",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
updatedAt.AddAnnotation("Relational:DefaultValueSql", "now()");
var updatedBy = runtimeEntityType.AddProperty(
"UpdatedBy",
typeof(string),
propertyInfo: typeof(EnvironmentSetting).GetProperty("UpdatedBy",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
updatedBy.AddAnnotation("Relational:ColumnName", "updated_by");
updatedBy.AddAnnotation("Relational:DefaultValueSql", "'system'");
var pk = runtimeEntityType.AddKey(new[] { key });
pk.AddAnnotation("Relational:Name", "environment_settings_pkey");
runtimeEntityType.SetPrimaryKey(pk);
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "platform");
runtimeEntityType.AddAnnotation("Relational:TableName", "environment_settings");
}
}
}

View File

@@ -0,0 +1,9 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using StellaOps.Platform.Database.EfCore.CompiledModels;
using StellaOps.Platform.Database.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
[assembly: DbContextModel(typeof(PlatformDbContext), typeof(PlatformDbContextModel))]

View File

@@ -0,0 +1,48 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using StellaOps.Platform.Database.EfCore.Context;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Platform.Database.EfCore.CompiledModels
{
[DbContext(typeof(PlatformDbContext))]
public partial class PlatformDbContextModel : RuntimeModel
{
private static readonly bool _useOldBehavior31751 =
System.AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue31751", out var enabled31751) && enabled31751;
static PlatformDbContextModel()
{
var model = new PlatformDbContextModel();
if (_useOldBehavior31751)
{
model.Initialize();
}
else
{
var thread = new System.Threading.Thread(RunInitialization, 10 * 1024 * 1024);
thread.Start();
thread.Join();
void RunInitialization()
{
model.Initialize();
}
}
model.Customize();
_instance = (PlatformDbContextModel)model.FinalizeModel();
}
private static PlatformDbContextModel _instance;
public static IModel Instance => _instance;
partial void Initialize();
partial void Customize();
}
}

View File

@@ -0,0 +1,36 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Platform.Database.EfCore.CompiledModels
{
public partial class PlatformDbContextModel
{
private PlatformDbContextModel()
: base(skipDetectChanges: false, modelId: new Guid("b2a4e6c8-1d3f-4a5b-9c7e-0f8a2b4d6e10"), entityTypeCount: 4)
{
}
partial void Initialize()
{
var environmentSetting = EnvironmentSettingEntityType.Create(this);
var contextRegion = ContextRegionEntityType.Create(this);
var contextEnvironment = ContextEnvironmentEntityType.Create(this);
var uiContextPreference = UiContextPreferenceEntityType.Create(this);
EnvironmentSettingEntityType.CreateAnnotations(environmentSetting);
ContextRegionEntityType.CreateAnnotations(contextRegion);
ContextEnvironmentEntityType.CreateAnnotations(contextEnvironment);
UiContextPreferenceEntityType.CreateAnnotations(uiContextPreference);
AddAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
AddAnnotation("ProductVersion", "10.0.0");
AddAnnotation("Relational:MaxIdentifierLength", 63);
}
}
}

View File

@@ -0,0 +1,99 @@
// <auto-generated />
using System;
using System.Reflection;
using Microsoft.EntityFrameworkCore.Metadata;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using StellaOps.Platform.Database.EfCore.Models;
#pragma warning disable 219, 612, 618
#nullable disable
namespace StellaOps.Platform.Database.EfCore.CompiledModels
{
internal partial class UiContextPreferenceEntityType
{
public static RuntimeEntityType Create(RuntimeModel model, RuntimeEntityType baseEntityType = null)
{
var runtimeEntityType = model.AddEntityType(
"StellaOps.Platform.Database.EfCore.Models.UiContextPreference",
typeof(UiContextPreference),
baseEntityType);
var tenantId = runtimeEntityType.AddProperty(
"TenantId",
typeof(string),
propertyInfo: typeof(UiContextPreference).GetProperty("TenantId",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
tenantId.AddAnnotation("Relational:ColumnName", "tenant_id");
var actorId = runtimeEntityType.AddProperty(
"ActorId",
typeof(string),
propertyInfo: typeof(UiContextPreference).GetProperty("ActorId",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
actorId.AddAnnotation("Relational:ColumnName", "actor_id");
var regions = runtimeEntityType.AddProperty(
"Regions",
typeof(string[]),
propertyInfo: typeof(UiContextPreference).GetProperty("Regions",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
regions.AddAnnotation("Relational:ColumnName", "regions");
regions.AddAnnotation("Relational:DefaultValueSql", "ARRAY[]::text[]");
var environments = runtimeEntityType.AddProperty(
"Environments",
typeof(string[]),
propertyInfo: typeof(UiContextPreference).GetProperty("Environments",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
environments.AddAnnotation("Relational:ColumnName", "environments");
environments.AddAnnotation("Relational:DefaultValueSql", "ARRAY[]::text[]");
var timeWindow = runtimeEntityType.AddProperty(
"TimeWindow",
typeof(string),
propertyInfo: typeof(UiContextPreference).GetProperty("TimeWindow",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
timeWindow.AddAnnotation("Relational:ColumnName", "time_window");
timeWindow.AddAnnotation("Relational:DefaultValueSql", "'24h'");
var updatedAt = runtimeEntityType.AddProperty(
"UpdatedAt",
typeof(DateTime),
propertyInfo: typeof(UiContextPreference).GetProperty("UpdatedAt",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
updatedAt.AddAnnotation("Relational:ColumnName", "updated_at");
updatedAt.AddAnnotation("Relational:DefaultValueSql", "now()");
var updatedBy = runtimeEntityType.AddProperty(
"UpdatedBy",
typeof(string),
propertyInfo: typeof(UiContextPreference).GetProperty("UpdatedBy",
BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly),
fieldInfo: null);
updatedBy.AddAnnotation("Relational:ColumnName", "updated_by");
updatedBy.AddAnnotation("Relational:DefaultValueSql", "'system'");
var pk = runtimeEntityType.AddKey(new[] { tenantId, actorId });
pk.AddAnnotation("Relational:Name", "ui_context_preferences_pkey");
runtimeEntityType.SetPrimaryKey(pk);
runtimeEntityType.AddIndex(new[] { updatedAt, tenantId, actorId },
"ix_platform_ui_context_preferences_updated");
return runtimeEntityType;
}
public static void CreateAnnotations(RuntimeEntityType runtimeEntityType)
{
runtimeEntityType.AddAnnotation("Relational:Schema", "platform");
runtimeEntityType.AddAnnotation("Relational:TableName", "ui_context_preferences");
}
}
}

View File

@@ -0,0 +1,118 @@
using Microsoft.EntityFrameworkCore;
using StellaOps.Platform.Database.EfCore.Models;
namespace StellaOps.Platform.Database.EfCore.Context;
public partial class PlatformDbContext : DbContext
{
private readonly string _schemaName;
public PlatformDbContext(DbContextOptions<PlatformDbContext> options, string? schemaName = null)
: base(options)
{
_schemaName = string.IsNullOrWhiteSpace(schemaName)
? "platform"
: schemaName.Trim();
}
public virtual DbSet<EnvironmentSetting> EnvironmentSettings { get; set; }
public virtual DbSet<ContextRegion> ContextRegions { get; set; }
public virtual DbSet<ContextEnvironment> ContextEnvironments { get; set; }
public virtual DbSet<UiContextPreference> UiContextPreferences { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var schemaName = _schemaName;
modelBuilder.Entity<EnvironmentSetting>(entity =>
{
entity.HasKey(e => e.Key).HasName("environment_settings_pkey");
entity.ToTable("environment_settings", schemaName);
entity.Property(e => e.Key).HasColumnName("key");
entity.Property(e => e.Value).HasColumnName("value");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("updated_at");
entity.Property(e => e.UpdatedBy)
.HasDefaultValueSql("'system'")
.HasColumnName("updated_by");
});
modelBuilder.Entity<ContextRegion>(entity =>
{
entity.HasKey(e => e.RegionId).HasName("context_regions_pkey");
entity.ToTable("context_regions", schemaName);
entity.HasIndex(e => new { e.SortOrder, e.RegionId }, "ux_platform_context_regions_sort")
.IsUnique();
entity.Property(e => e.RegionId).HasColumnName("region_id");
entity.Property(e => e.DisplayName).HasColumnName("display_name");
entity.Property(e => e.SortOrder).HasColumnName("sort_order");
entity.Property(e => e.Enabled)
.HasDefaultValue(true)
.HasColumnName("enabled");
});
modelBuilder.Entity<ContextEnvironment>(entity =>
{
entity.HasKey(e => e.EnvironmentId).HasName("context_environments_pkey");
entity.ToTable("context_environments", schemaName);
entity.HasIndex(e => new { e.RegionId, e.SortOrder, e.EnvironmentId },
"ix_platform_context_environments_region_sort");
entity.HasIndex(e => new { e.SortOrder, e.RegionId, e.EnvironmentId },
"ix_platform_context_environments_sort");
entity.Property(e => e.EnvironmentId).HasColumnName("environment_id");
entity.Property(e => e.RegionId).HasColumnName("region_id");
entity.Property(e => e.EnvironmentType).HasColumnName("environment_type");
entity.Property(e => e.DisplayName).HasColumnName("display_name");
entity.Property(e => e.SortOrder).HasColumnName("sort_order");
entity.Property(e => e.Enabled)
.HasDefaultValue(true)
.HasColumnName("enabled");
});
modelBuilder.Entity<UiContextPreference>(entity =>
{
entity.HasKey(e => new { e.TenantId, e.ActorId }).HasName("ui_context_preferences_pkey");
entity.ToTable("ui_context_preferences", schemaName);
entity.HasIndex(e => new { e.UpdatedAt, e.TenantId, e.ActorId },
"ix_platform_ui_context_preferences_updated")
.IsDescending(true, false, false);
entity.Property(e => e.TenantId).HasColumnName("tenant_id");
entity.Property(e => e.ActorId).HasColumnName("actor_id");
entity.Property(e => e.Regions)
.HasDefaultValueSql("ARRAY[]::text[]")
.HasColumnName("regions");
entity.Property(e => e.Environments)
.HasDefaultValueSql("ARRAY[]::text[]")
.HasColumnName("environments");
entity.Property(e => e.TimeWindow)
.HasDefaultValueSql("'24h'")
.HasColumnName("time_window");
entity.Property(e => e.UpdatedAt)
.HasDefaultValueSql("now()")
.HasColumnName("updated_at");
entity.Property(e => e.UpdatedBy)
.HasDefaultValueSql("'system'")
.HasColumnName("updated_by");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}

View File

@@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace StellaOps.Platform.Database.EfCore.Context;
public sealed class PlatformDesignTimeDbContextFactory : IDesignTimeDbContextFactory<PlatformDbContext>
{
private const string DefaultConnectionString = "Host=localhost;Port=55434;Database=postgres;Username=postgres;Password=postgres;Search Path=platform,public";
private const string ConnectionStringEnvironmentVariable = "STELLAOPS_PLATFORM_EF_CONNECTION";
public PlatformDbContext CreateDbContext(string[] args)
{
var connectionString = ResolveConnectionString();
var options = new DbContextOptionsBuilder<PlatformDbContext>()
.UseNpgsql(connectionString)
.Options;
return new PlatformDbContext(options);
}
private static string ResolveConnectionString()
{
var fromEnvironment = Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable);
return string.IsNullOrWhiteSpace(fromEnvironment) ? DefaultConnectionString : fromEnvironment;
}
}

View File

@@ -0,0 +1,16 @@
namespace StellaOps.Platform.Database.EfCore.Models;
public partial class ContextEnvironment
{
public string EnvironmentId { get; set; } = null!;
public string RegionId { get; set; } = null!;
public string EnvironmentType { get; set; } = null!;
public string DisplayName { get; set; } = null!;
public int SortOrder { get; set; }
public bool Enabled { get; set; }
}

View File

@@ -0,0 +1,12 @@
namespace StellaOps.Platform.Database.EfCore.Models;
public partial class ContextRegion
{
public string RegionId { get; set; } = null!;
public string DisplayName { get; set; } = null!;
public int SortOrder { get; set; }
public bool Enabled { get; set; }
}

View File

@@ -0,0 +1,14 @@
using System;
namespace StellaOps.Platform.Database.EfCore.Models;
public partial class EnvironmentSetting
{
public string Key { get; set; } = null!;
public string Value { get; set; } = null!;
public DateTime UpdatedAt { get; set; }
public string UpdatedBy { get; set; } = null!;
}

View File

@@ -0,0 +1,20 @@
using System;
namespace StellaOps.Platform.Database.EfCore.Models;
public partial class UiContextPreference
{
public string TenantId { get; set; } = null!;
public string ActorId { get; set; } = null!;
public string[] Regions { get; set; } = [];
public string[] Environments { get; set; } = [];
public string TimeWindow { get; set; } = null!;
public DateTime UpdatedAt { get; set; }
public string UpdatedBy { get; set; } = null!;
}

View File

@@ -0,0 +1,175 @@
using StellaOps.Infrastructure.Postgres.Migrations;
using System.Security.Cryptography;
using System.Text;
namespace StellaOps.Platform.Database;
/// <summary>
/// Consolidated migration artifact generated from all configured sources of a module.
/// </summary>
public sealed record MigrationModuleConsolidatedArtifact(
string MigrationName,
string Script,
string Checksum,
IReadOnlyList<MigrationModuleConsolidatedSourceMigration> SourceMigrations);
/// <summary>
/// Source migration metadata retained for compatibility backfill.
/// </summary>
public sealed record MigrationModuleConsolidatedSourceMigration(
string Name,
MigrationCategory Category,
string Checksum,
string Content,
string SourceResourceName);
/// <summary>
/// Builds deterministic consolidated migration scripts per service module.
/// </summary>
public static class MigrationModuleConsolidation
{
public static MigrationModuleConsolidatedArtifact Build(MigrationModuleInfo module)
{
ArgumentNullException.ThrowIfNull(module);
var migrationsByName = new Dictionary<string, MigrationModuleConsolidatedSourceMigration>(StringComparer.Ordinal);
foreach (var source in module.Sources)
{
foreach (var migration in LoadMigrationsFromSource(source))
{
if (migrationsByName.TryGetValue(migration.Name, out var existing))
{
if (!string.Equals(existing.Checksum, migration.Checksum, StringComparison.Ordinal))
{
throw new InvalidOperationException(
$"Duplicate migration name '{migration.Name}' with different content discovered while consolidating module '{module.Name}'.");
}
continue;
}
migrationsByName[migration.Name] = migration;
}
}
if (migrationsByName.Count == 0)
{
throw new InvalidOperationException(
$"Module '{module.Name}' has no migration resources to consolidate.");
}
var sourceMigrations = migrationsByName.Values
.OrderBy(static migration => migration.Name, StringComparer.Ordinal)
.ToArray();
var script = BuildConsolidatedScript(module, sourceMigrations);
var checksum = ComputeChecksum(script);
var migrationName = $"100_consolidated_{NormalizeModuleName(module.Name)}.sql";
return new MigrationModuleConsolidatedArtifact(
migrationName,
script,
checksum,
sourceMigrations);
}
private static IReadOnlyList<MigrationModuleConsolidatedSourceMigration> LoadMigrationsFromSource(
MigrationModuleSourceInfo source)
{
var resources = source.MigrationsAssembly
.GetManifestResourceNames()
.Where(static name => name.EndsWith(".sql", StringComparison.OrdinalIgnoreCase))
.Where(name =>
string.IsNullOrWhiteSpace(source.ResourcePrefix) ||
name.Contains(source.ResourcePrefix, StringComparison.OrdinalIgnoreCase))
.OrderBy(static name => name, StringComparer.Ordinal);
var migrations = new List<MigrationModuleConsolidatedSourceMigration>();
foreach (var resourceName in resources)
{
using var stream = source.MigrationsAssembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
continue;
}
using var reader = new StreamReader(stream);
var content = reader.ReadToEnd();
var fileName = ExtractFileName(resourceName);
var category = MigrationCategoryExtensions.GetCategory(fileName);
var checksum = ComputeChecksum(content);
migrations.Add(new MigrationModuleConsolidatedSourceMigration(
Name: fileName,
Category: category,
Checksum: checksum,
Content: content,
SourceResourceName: resourceName));
}
return migrations;
}
private static string BuildConsolidatedScript(
MigrationModuleInfo module,
IReadOnlyList<MigrationModuleConsolidatedSourceMigration> sourceMigrations)
{
var builder = new StringBuilder();
builder.Append("-- Consolidated migration for module '");
builder.Append(module.Name);
builder.AppendLine("'.");
builder.Append("-- Generated deterministically from ");
builder.Append(sourceMigrations.Count);
builder.AppendLine(" source migrations.");
builder.AppendLine();
foreach (var migration in sourceMigrations)
{
builder.Append("-- BEGIN ");
builder.AppendLine(migration.SourceResourceName);
builder.AppendLine(migration.Content.TrimEnd());
builder.Append("-- END ");
builder.AppendLine(migration.SourceResourceName);
builder.AppendLine();
}
return builder.ToString();
}
private static string ExtractFileName(string resourceName)
{
var lastSlash = resourceName.LastIndexOf('/');
if (lastSlash >= 0)
{
return resourceName[(lastSlash + 1)..];
}
var parts = resourceName.Split('.');
for (var i = parts.Length - 1; i >= 0; i--)
{
if (parts[i].EndsWith("sql", StringComparison.OrdinalIgnoreCase))
{
return i > 0 ? $"{parts[i - 1]}.sql" : parts[i];
}
}
return resourceName;
}
private static string ComputeChecksum(string content)
{
var normalized = content.Replace("\r\n", "\n", StringComparison.Ordinal)
.Replace("\r", "\n", StringComparison.Ordinal);
var bytes = Encoding.UTF8.GetBytes(normalized);
var hash = SHA256.HashData(bytes);
return Convert.ToHexStringLower(hash);
}
private static string NormalizeModuleName(string moduleName)
{
var chars = moduleName.Where(char.IsLetterOrDigit)
.Select(char.ToLowerInvariant)
.ToArray();
return chars.Length == 0 ? "module" : new string(chars);
}
}

View File

@@ -32,6 +32,23 @@ internal static class MigrationModulePluginDiscovery
$"Invalid migration module plugin '{plugin.GetType().FullName}': schema name is required.");
}
if (module.Sources.Count == 0)
{
throw new InvalidOperationException(
$"Invalid migration module plugin '{plugin.GetType().FullName}': at least one migration source is required.");
}
var sourceSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var source in module.Sources)
{
var sourceIdentity = $"{source.MigrationsAssembly.FullName}|{source.ResourcePrefix}";
if (!sourceSet.Add(sourceIdentity))
{
throw new InvalidOperationException(
$"Invalid migration module plugin '{plugin.GetType().FullName}': duplicate migration source '{sourceIdentity}' for module '{module.Name}'.");
}
}
if (!modulesByName.TryAdd(module.Name, module))
{
throw new InvalidOperationException(
@@ -161,4 +178,3 @@ internal static class MigrationModulePluginDiscovery
return directories.OrderBy(static directory => directory, StringComparer.Ordinal).ToArray();
}
}

View File

@@ -1,100 +1,296 @@
using StellaOps.AdvisoryAI.Storage.Postgres;
using StellaOps.Attestor.Persistence;
using StellaOps.Eventing.Postgres;
using StellaOps.AirGap.Persistence.Postgres;
using StellaOps.BinaryIndex.GoldenSet;
using StellaOps.BinaryIndex.Persistence;
using StellaOps.EvidenceLocker.Infrastructure.Db;
using StellaOps.Artifact.Infrastructure;
using StellaOps.Authority.Persistence.Postgres;
using StellaOps.Concelier.Persistence.Postgres;
using StellaOps.Evidence.Persistence.Postgres;
using StellaOps.Excititor.Persistence.Postgres;
using StellaOps.Notify.Persistence.Postgres;
using StellaOps.Plugin.Registry;
using StellaOps.Policy.Persistence.Postgres;
using StellaOps.ReachGraph.Persistence.Postgres;
using StellaOps.Remediation.Persistence.Postgres;
using StellaOps.SbomService.Lineage.Persistence;
using StellaOps.Scanner.Storage.Postgres;
using StellaOps.Scanner.Triage;
using StellaOps.Scheduler.Persistence.Postgres;
using StellaOps.Timeline.Core.Postgres;
using StellaOps.TimelineIndexer.Infrastructure;
using StellaOps.Verdict.Persistence.Postgres;
using StellaOps.Signals.Persistence.Postgres;
using StellaOps.Graph.Indexer.Persistence.Postgres;
using StellaOps.Unknowns.Persistence.Postgres;
using StellaOps.VexHub.Persistence.Postgres;
using StellaOps.VexLens.Persistence.Postgres;
using StellaOps.Findings.Ledger.Infrastructure.Postgres;
using StellaOps.Orchestrator.Infrastructure.Postgres;
namespace StellaOps.Platform.Database;
public sealed class AdvisoryAiMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "AdvisoryAI",
schemaName: "advisoryai",
migrationsAssembly: typeof(AdvisoryAiDataSource).Assembly);
}
public sealed class AirGapMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "AirGap",
SchemaName: "airgap",
MigrationsAssembly: typeof(AirGapDataSource).Assembly);
name: "AirGap",
schemaName: "airgap",
migrationsAssembly: typeof(AirGapDataSource).Assembly);
}
public sealed class AttestorMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Attestor",
schemaName: "proofchain",
migrationsAssembly: typeof(ProofChainDbContext).Assembly,
resourcePrefix: "StellaOps.Attestor.Persistence.Migrations");
}
public sealed class BinaryIndexMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "BinaryIndex",
schemaName: "binaries",
sources:
[
new MigrationModuleSourceInfo(
typeof(BinaryIndexMigrationRunner).Assembly,
"StellaOps.BinaryIndex.Persistence.Migrations"),
new MigrationModuleSourceInfo(
typeof(PostgresGoldenSetStore).Assembly,
"StellaOps.BinaryIndex.GoldenSet.Migrations")
]);
}
public sealed class AuthorityMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Authority",
SchemaName: "authority",
MigrationsAssembly: typeof(AuthorityDataSource).Assembly,
ResourcePrefix: "StellaOps.Authority.Persistence.Migrations");
name: "Authority",
schemaName: "authority",
migrationsAssembly: typeof(AuthorityDataSource).Assembly,
resourcePrefix: "StellaOps.Authority.Persistence.Migrations");
}
public sealed class EventingMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Eventing",
schemaName: "timeline",
migrationsAssembly: typeof(EventingDataSource).Assembly);
}
public sealed class GraphMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Graph",
schemaName: "graph",
sources:
[
new MigrationModuleSourceInfo(typeof(GraphIndexerDataSource).Assembly)
]);
}
public sealed class EvidenceMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Evidence",
schemaName: "evidence",
sources:
[
new MigrationModuleSourceInfo(typeof(EvidenceDataSource).Assembly),
new MigrationModuleSourceInfo(typeof(ArtifactDataSource).Assembly)
]);
}
public sealed class EvidenceLockerMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "EvidenceLocker",
schemaName: "evidence_locker",
migrationsAssembly: typeof(EvidenceLockerDataSource).Assembly);
}
public sealed class SchedulerMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Scheduler",
SchemaName: "scheduler",
MigrationsAssembly: typeof(SchedulerDataSource).Assembly,
ResourcePrefix: "StellaOps.Scheduler.Persistence.Migrations");
name: "Scheduler",
schemaName: "scheduler",
migrationsAssembly: typeof(SchedulerDataSource).Assembly,
resourcePrefix: "StellaOps.Scheduler.Persistence.Migrations");
}
public sealed class ConcelierMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Concelier",
SchemaName: "vuln",
MigrationsAssembly: typeof(ConcelierDataSource).Assembly,
ResourcePrefix: "StellaOps.Concelier.Persistence.Migrations");
name: "Concelier",
schemaName: "vuln",
migrationsAssembly: typeof(ConcelierDataSource).Assembly,
resourcePrefix: "StellaOps.Concelier.Persistence.Migrations");
}
public sealed class PolicyMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Policy",
SchemaName: "policy",
MigrationsAssembly: typeof(PolicyDataSource).Assembly,
ResourcePrefix: "StellaOps.Policy.Persistence.Migrations");
name: "Policy",
schemaName: "policy",
migrationsAssembly: typeof(PolicyDataSource).Assembly,
resourcePrefix: "StellaOps.Policy.Persistence.Migrations");
}
public sealed class NotifyMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Notify",
SchemaName: "notify",
MigrationsAssembly: typeof(NotifyDataSource).Assembly,
ResourcePrefix: "StellaOps.Notify.Persistence.Migrations");
name: "Notify",
schemaName: "notify",
migrationsAssembly: typeof(NotifyDataSource).Assembly,
resourcePrefix: "StellaOps.Notify.Persistence.Migrations");
}
public sealed class ExcititorMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Excititor",
SchemaName: "vex",
MigrationsAssembly: typeof(ExcititorDataSource).Assembly,
ResourcePrefix: "StellaOps.Excititor.Persistence.Migrations");
name: "Excititor",
schemaName: "vex",
migrationsAssembly: typeof(ExcititorDataSource).Assembly,
resourcePrefix: "StellaOps.Excititor.Persistence.Migrations");
}
public sealed class PluginRegistryMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "PluginRegistry",
schemaName: "platform",
migrationsAssembly: typeof(PluginRegistryMigrationRunner).Assembly,
resourcePrefix: "StellaOps.Plugin.Registry.Migrations");
}
public sealed class PlatformMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Platform",
SchemaName: "release",
MigrationsAssembly: typeof(ReleaseMigrationRunner).Assembly,
ResourcePrefix: "StellaOps.Platform.Database.Migrations.Release");
name: "Platform",
schemaName: "release",
migrationsAssembly: typeof(ReleaseMigrationRunner).Assembly,
resourcePrefix: "StellaOps.Platform.Database.Migrations.Release");
}
public sealed class ScannerMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "Scanner",
SchemaName: "scanner",
MigrationsAssembly: typeof(ScannerDataSource).Assembly);
name: "Scanner",
schemaName: "scanner",
sources:
[
new MigrationModuleSourceInfo(typeof(ScannerDataSource).Assembly),
new MigrationModuleSourceInfo(
typeof(TriageDbContext).Assembly,
"StellaOps.Scanner.Triage.Migrations")
]);
}
public sealed class SignalsMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Signals",
schemaName: "signals",
migrationsAssembly: typeof(SignalsDataSource).Assembly);
}
public sealed class TimelineIndexerMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
Name: "TimelineIndexer",
SchemaName: "timeline",
MigrationsAssembly: typeof(TimelineIndexerDataSource).Assembly,
ResourcePrefix: "StellaOps.TimelineIndexer.Infrastructure.Db.Migrations");
name: "TimelineIndexer",
schemaName: "timeline",
sources:
[
new MigrationModuleSourceInfo(
typeof(TimelineIndexerDataSource).Assembly,
"StellaOps.TimelineIndexer.Infrastructure.Db.Migrations"),
new MigrationModuleSourceInfo(
typeof(TimelineCoreDataSource).Assembly)
]);
}
public sealed class VexHubMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "VexHub",
schemaName: "vexhub",
migrationsAssembly: typeof(VexHubDataSource).Assembly);
}
public sealed class RemediationMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Remediation",
schemaName: "remediation",
migrationsAssembly: typeof(RemediationDataSource).Assembly);
}
public sealed class VexLensMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "VexLens",
schemaName: "vexlens",
migrationsAssembly: typeof(VexLensDataSource).Assembly);
}
public sealed class SbomLineageMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "SbomLineage",
schemaName: "sbom",
migrationsAssembly: typeof(LineageDataSource).Assembly);
}
public sealed class ReachGraphMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "ReachGraph",
schemaName: "reachgraph",
migrationsAssembly: typeof(ReachGraphDataSource).Assembly);
}
public sealed class UnknownsMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Unknowns",
schemaName: "unknowns",
migrationsAssembly: typeof(UnknownsDataSource).Assembly);
}
public sealed class VerdictMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Verdict",
schemaName: "stellaops",
migrationsAssembly: typeof(VerdictDataSource).Assembly,
resourcePrefix: "StellaOps.Verdict.Persistence.Migrations");
}
public sealed class OrchestratorMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "Orchestrator",
schemaName: "orchestrator",
migrationsAssembly: typeof(OrchestratorDataSource).Assembly);
}
public sealed class FindingsLedgerMigrationModulePlugin : IMigrationModulePlugin
{
public MigrationModuleInfo Module { get; } = new(
name: "FindingsLedger",
schemaName: "public",
migrationsAssembly: typeof(LedgerDataSource).Assembly,
resourcePrefix: "StellaOps.Findings.Ledger.migrations");
}

View File

@@ -4,14 +4,62 @@ using System.Threading;
namespace StellaOps.Platform.Database;
/// <summary>
/// Defines a PostgreSQL module with migration metadata.
/// Defines one migration source (assembly + optional resource prefix) for a service module.
/// </summary>
public sealed record MigrationModuleInfo(
string Name,
string SchemaName,
public sealed record MigrationModuleSourceInfo(
Assembly MigrationsAssembly,
string? ResourcePrefix = null);
/// <summary>
/// Defines a PostgreSQL module with migration metadata.
/// </summary>
public sealed record MigrationModuleInfo
{
public MigrationModuleInfo(
string name,
string schemaName,
Assembly migrationsAssembly,
string? resourcePrefix = null)
: this(
name,
schemaName,
[new MigrationModuleSourceInfo(migrationsAssembly, resourcePrefix)])
{
}
public MigrationModuleInfo(
string name,
string schemaName,
IReadOnlyList<MigrationModuleSourceInfo> sources)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentException.ThrowIfNullOrWhiteSpace(schemaName);
ArgumentNullException.ThrowIfNull(sources);
if (sources.Count == 0)
{
throw new ArgumentException("At least one migration source is required.", nameof(sources));
}
if (sources.Any(static source => source.MigrationsAssembly is null))
{
throw new ArgumentException("Migration source assembly cannot be null.", nameof(sources));
}
Name = name;
SchemaName = schemaName;
Sources = sources.ToArray();
MigrationsAssembly = Sources[0].MigrationsAssembly;
ResourcePrefix = Sources[0].ResourcePrefix;
}
public string Name { get; }
public string SchemaName { get; }
public Assembly MigrationsAssembly { get; }
public string? ResourcePrefix { get; }
public IReadOnlyList<MigrationModuleSourceInfo> Sources { get; }
}
/// <summary>
/// Canonical PostgreSQL migration module registry owned by Platform.
/// </summary>

View File

@@ -0,0 +1,14 @@
-- Release schema prerequisite for tenant fallback lookups.
-- Keeps clean-install migration execution independent from optional shared-schema owners.
CREATE SCHEMA IF NOT EXISTS shared;
CREATE TABLE IF NOT EXISTS shared.tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
is_default BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS uq_shared_tenants_single_default
ON shared.tenants (is_default)
WHERE is_default;

View File

@@ -151,10 +151,11 @@ CREATE TABLE IF NOT EXISTS release.release_tags (
release_id UUID NOT NULL REFERENCES release.releases(id) ON DELETE CASCADE,
environment_id UUID REFERENCES release.environments(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL,
PRIMARY KEY (tenant_id, tag, COALESCE(environment_id, '00000000-0000-0000-0000-000000000000'::UUID))
created_by UUID NOT NULL
);
CREATE UNIQUE INDEX idx_release_tags_tenant_tag_environment
ON release.release_tags (tenant_id, tag, COALESCE(environment_id, '00000000-0000-0000-0000-000000000000'::UUID));
CREATE INDEX idx_release_tags_release ON release.release_tags(release_id);
CREATE INDEX idx_release_tags_environment ON release.release_tags(environment_id)
WHERE environment_id IS NOT NULL;

View File

@@ -85,13 +85,14 @@ COMMENT ON COLUMN release.agent_capabilities.version IS 'Version of the capabili
-- ============================================================================
CREATE TABLE IF NOT EXISTS release.agent_heartbeats (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
id UUID NOT NULL DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
agent_id UUID NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
status JSONB NOT NULL,
latency_ms INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (id, created_at)
-- No FK to agents for partition performance
) PARTITION BY RANGE (created_at);

View File

@@ -87,10 +87,11 @@ CREATE TABLE IF NOT EXISTS release.plugins (
entry_point TEXT,
config_defaults JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::UUID), name)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_plugins_scope_name
ON release.plugins (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::UUID), name);
CREATE INDEX idx_plugins_tenant ON release.plugins(tenant_id);
CREATE INDEX idx_plugins_type ON release.plugins(plugin_type_id);
CREATE INDEX idx_plugins_enabled ON release.plugins(tenant_id, is_enabled)
@@ -154,10 +155,11 @@ CREATE TABLE IF NOT EXISTS release.plugin_instances (
invocation_count BIGINT NOT NULL DEFAULT 0,
error_count BIGINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, plugin_id, COALESCE(instance_name, ''))
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_plugin_instances_scope_name
ON release.plugin_instances (tenant_id, plugin_id, COALESCE(instance_name, ''));
CREATE INDEX idx_plugin_instances_tenant ON release.plugin_instances(tenant_id);
CREATE INDEX idx_plugin_instances_plugin ON release.plugin_instances(plugin_id);
CREATE INDEX idx_plugin_instances_enabled ON release.plugin_instances(tenant_id, is_enabled)

View File

@@ -28,11 +28,11 @@ CREATE TABLE IF NOT EXISTS release.policy_profiles (
on_fail_hard TEXT[] NOT NULL DEFAULT '{}',
created_by UUID,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- Ensure unique names within tenant scope (NULL tenant = instance level)
UNIQUE (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::UUID), name)
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_policy_profiles_scope_name
ON release.policy_profiles (COALESCE(tenant_id, '00000000-0000-0000-0000-000000000000'::UUID), name);
CREATE INDEX idx_policy_profiles_tenant ON release.policy_profiles(tenant_id);
CREATE INDEX idx_policy_profiles_type ON release.policy_profiles(profile_type);
CREATE INDEX idx_policy_profiles_default ON release.policy_profiles(tenant_id)

View File

@@ -0,0 +1,30 @@
using System;
using Microsoft.EntityFrameworkCore;
using Npgsql;
using StellaOps.Platform.Database.EfCore.CompiledModels;
using StellaOps.Platform.Database.EfCore.Context;
namespace StellaOps.Platform.Database.Postgres;
public static class PlatformDbContextFactory
{
public const string DefaultSchemaName = "platform";
public static PlatformDbContext Create(NpgsqlConnection connection, int commandTimeoutSeconds, string schemaName)
{
var normalizedSchema = string.IsNullOrWhiteSpace(schemaName)
? DefaultSchemaName
: schemaName.Trim();
var optionsBuilder = new DbContextOptionsBuilder<PlatformDbContext>()
.UseNpgsql(connection, npgsql => npgsql.CommandTimeout(commandTimeoutSeconds));
if (string.Equals(normalizedSchema, DefaultSchemaName, StringComparison.Ordinal))
{
// Use the static compiled model when schema mapping matches the default model.
optionsBuilder.UseModel(PlatformDbContextModel.Instance);
}
return new PlatformDbContext(optionsBuilder.Options, normalizedSchema);
}
}

View File

@@ -11,16 +11,38 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\AdvisoryAI\StellaOps.AdvisoryAI\StellaOps.AdvisoryAI.csproj" />
<ProjectReference Include="..\..\..\AirGap\__Libraries\StellaOps.AirGap.Persistence\StellaOps.AirGap.Persistence.csproj" />
<ProjectReference Include="..\..\..\Attestor\__Libraries\StellaOps.Attestor.Persistence\StellaOps.Attestor.Persistence.csproj" />
<ProjectReference Include="..\..\..\BinaryIndex\__Libraries\StellaOps.BinaryIndex.Persistence\StellaOps.BinaryIndex.Persistence.csproj" />
<ProjectReference Include="..\..\..\BinaryIndex\__Libraries\StellaOps.BinaryIndex.GoldenSet\StellaOps.BinaryIndex.GoldenSet.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Artifact.Infrastructure\StellaOps.Artifact.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\Authority\__Libraries\StellaOps.Authority.Persistence\StellaOps.Authority.Persistence.csproj" />
<ProjectReference Include="..\..\..\Concelier\__Libraries\StellaOps.Concelier.Persistence\StellaOps.Concelier.Persistence.csproj" />
<ProjectReference Include="..\..\..\Graph\__Libraries\StellaOps.Graph.Indexer.Persistence\StellaOps.Graph.Indexer.Persistence.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Evidence.Persistence\StellaOps.Evidence.Persistence.csproj" />
<ProjectReference Include="..\..\..\Excititor\__Libraries\StellaOps.Excititor.Persistence\StellaOps.Excititor.Persistence.csproj" />
<ProjectReference Include="..\..\..\Notify\__Libraries\StellaOps.Notify.Persistence\StellaOps.Notify.Persistence.csproj" />
<ProjectReference Include="..\..\..\Plugin\StellaOps.Plugin.Registry\StellaOps.Plugin.Registry.csproj" />
<ProjectReference Include="..\..\..\Policy\__Libraries\StellaOps.Policy.Persistence\StellaOps.Policy.Persistence.csproj" />
<ProjectReference Include="..\..\..\SbomService\__Libraries\StellaOps.SbomService.Lineage\StellaOps.SbomService.Lineage.csproj" />
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.Storage\StellaOps.Scanner.Storage.csproj" />
<ProjectReference Include="..\..\..\Signals\__Libraries\StellaOps.Signals.Persistence\StellaOps.Signals.Persistence.csproj" />
<ProjectReference Include="..\..\..\Scanner\__Libraries\StellaOps.Scanner.Triage\StellaOps.Scanner.Triage.csproj" />
<ProjectReference Include="..\..\..\Scheduler\__Libraries\StellaOps.Scheduler.Persistence\StellaOps.Scheduler.Persistence.csproj" />
<ProjectReference Include="..\..\..\Timeline\__Libraries\StellaOps.Timeline.Core\StellaOps.Timeline.Core.csproj" />
<ProjectReference Include="..\..\..\TimelineIndexer\StellaOps.TimelineIndexer\StellaOps.TimelineIndexer.Infrastructure\StellaOps.TimelineIndexer.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\Unknowns\__Libraries\StellaOps.Unknowns.Persistence\StellaOps.Unknowns.Persistence.csproj" />
<ProjectReference Include="..\..\..\VexHub\__Libraries\StellaOps.VexHub.Persistence\StellaOps.VexHub.Persistence.csproj" />
<ProjectReference Include="..\..\..\VexLens\StellaOps.VexLens.Persistence\StellaOps.VexLens.Persistence.csproj" />
<ProjectReference Include="..\..\..\Remediation\StellaOps.Remediation.Persistence\StellaOps.Remediation.Persistence.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.ReachGraph.Persistence\StellaOps.ReachGraph.Persistence.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Verdict\StellaOps.Verdict.csproj" />
<ProjectReference Include="..\..\..\EvidenceLocker\StellaOps.EvidenceLocker\StellaOps.EvidenceLocker.Infrastructure\StellaOps.EvidenceLocker.Infrastructure.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Eventing\StellaOps.Eventing.csproj" />
<ProjectReference Include="..\..\..\__Libraries\StellaOps.Infrastructure.Postgres\StellaOps.Infrastructure.Postgres.csproj" />
<ProjectReference Include="..\..\..\Findings\StellaOps.Findings.Ledger\StellaOps.Findings.Ledger.csproj" />
<ProjectReference Include="..\..\..\Orchestrator\StellaOps.Orchestrator\StellaOps.Orchestrator.Infrastructure\StellaOps.Orchestrator.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
@@ -28,6 +50,17 @@
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
</ItemGroup>
<ItemGroup>
<!-- Prevent automatic compiled-model binding so non-default schemas can build runtime models. -->
<Compile Remove="EfCore\CompiledModels\PlatformDbContextAssemblyAttributes.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" PrivateAssets="all" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Migrations\**\*.sql" />
</ItemGroup>

View File

@@ -6,6 +6,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| --- | --- | --- |
| SPRINT_20260222_051-MGC-04-W1 | DONE | Added platform-owned `MigrationModuleRegistry` canonical module catalog for migration runner entrypoint consolidation; CLI now consumes this registry instead of owning module metadata. |
| SPRINT_20260222_051-MGC-04-W1-PLUGINS | DONE | Replaced hardcoded module catalog with auto-discovered migration plugins (`IMigrationModulePlugin`) so one consolidated plugin descriptor per web service feeds both CLI and Platform API migration execution paths. |
| SPRINT_20260222_051-MGC-04-W1-SOURCES | DONE | Extended service plugin model to support source-set flattening (multiple migration sources per web service), including Scanner storage+triage source registration under one `ScannerMigrationModulePlugin`, plus synthesized per-plugin consolidated migration artifact generation for empty-history execution and partial-history backfill self-healing. |
| B22-01-DB | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added release migration `047_GlobalContextAndFilters.sql` with `platform.context_regions`, `platform.context_environments`, and `platform.ui_context_preferences`. |
| B22-02-DB | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added release migration `048_ReleaseReadModels.sql` with release list/activity/approvals projection tables, correlation keys, and deterministic ordering indexes. |
| B22-03-DB | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added release migration `049_TopologyInventory.sql` with normalized topology inventory projection tables and sync-watermark indexes. |
@@ -13,3 +14,8 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
| B22-05-DB | DONE | Sprint `docs/implplan/SPRINT_20260220_018_Platform_pack22_backend_contracts_and_migrations.md`: added release migration `051_IntegrationSourceHealth.sql` for integrations feed and VEX source health/freshness read-model projection objects. |
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Platform/__Libraries/StellaOps.Platform.Database/StellaOps.Platform.Database.md. |
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
| PLATFORM-EF-01 | DONE | Sprint `docs/implplan/SPRINT_20260222_096_Platform_dal_to_efcore.md`: verified AGENTS.md alignment and `PlatformMigrationModulePlugin` registration in migration registry. |
| PLATFORM-EF-02 | DONE | Sprint 096: scaffolded EF Core model baseline under `EfCore/Context/`, `EfCore/Models/`, `EfCore/CompiledModels/` for platform schema tables (`environment_settings`, `context_regions`, `context_environments`, `ui_context_preferences`). |
| PLATFORM-EF-03 | DONE | Sprint 096: converted `PostgresEnvironmentSettingsStore` and `PostgresPlatformContextStore` reads to EF Core LINQ with `AsNoTracking()`; PostgreSQL-specific upserts retained as raw SQL. `PostgresScoreHistoryStore` retained as raw Npgsql (cross-module signals schema). |
| PLATFORM-EF-04 | DONE | Sprint 096: added design-time factory (`PlatformDesignTimeDbContextFactory`), runtime factory (`PlatformDbContextFactory`) with `UseModel(PlatformDbContextModel.Instance)` for default schema, compiled model stubs with `// <auto-generated />` header, assembly attribute exclusion in csproj. |
| PLATFORM-EF-05 | DONE | Sprint 096: sequential builds pass for Platform.Database (0W/0E), Platform.WebService (0W/0E), Platform.WebService.Tests (0W/0E). TASKS.md and sprint tracker updated. |