Bind startup migrations to module schema search path

This commit is contained in:
master
2026-03-10 01:37:02 +02:00
parent 1df79ac75e
commit 6b7168ca3c
4 changed files with 176 additions and 5 deletions

View File

@@ -91,6 +91,7 @@ public abstract class StartupMigrationHost : IHostedService
{
// Step 2: Ensure schema and migrations table exist
await EnsureSchemaAsync(connection, cancellationToken).ConfigureAwait(false);
await SetSearchPathAsync(connection, cancellationToken).ConfigureAwait(false);
await EnsureMigrationsTableAsync(connection, cancellationToken).ConfigureAwait(false);
// Step 3: Load and categorize migrations
@@ -237,17 +238,28 @@ public abstract class StartupMigrationHost : IHostedService
private async Task EnsureSchemaAsync(NpgsqlConnection connection, CancellationToken cancellationToken)
{
var quotedSchema = QuoteIdentifier(_schemaName);
await using var command = new NpgsqlCommand(
$"CREATE SCHEMA IF NOT EXISTS {_schemaName}",
$"CREATE SCHEMA IF NOT EXISTS {quotedSchema}",
connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
private async Task SetSearchPathAsync(NpgsqlConnection connection, CancellationToken cancellationToken)
{
var quotedSchema = QuoteIdentifier(_schemaName);
await using var command = new NpgsqlCommand(
$"SET search_path TO {quotedSchema}, public",
connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
}
private async Task EnsureMigrationsTableAsync(NpgsqlConnection connection, CancellationToken cancellationToken)
{
var quotedSchema = QuoteIdentifier(_schemaName);
await using var command = new NpgsqlCommand(
$"""
CREATE TABLE IF NOT EXISTS {_schemaName}.schema_migrations (
CREATE TABLE IF NOT EXISTS {quotedSchema}.schema_migrations (
migration_name TEXT PRIMARY KEY,
category TEXT NOT NULL DEFAULT 'startup',
checksum TEXT NOT NULL,
@@ -258,7 +270,7 @@ public abstract class StartupMigrationHost : IHostedService
);
CREATE INDEX IF NOT EXISTS idx_schema_migrations_applied_at
ON {_schemaName}.schema_migrations(applied_at DESC);
ON {quotedSchema}.schema_migrations(applied_at DESC);
""",
connection);
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
@@ -269,9 +281,10 @@ public abstract class StartupMigrationHost : IHostedService
CancellationToken cancellationToken)
{
var result = new Dictionary<string, AppliedMigration>(StringComparer.Ordinal);
var quotedSchema = QuoteIdentifier(_schemaName);
await using var command = new NpgsqlCommand(
$"SELECT migration_name, category, checksum, applied_at FROM {_schemaName}.schema_migrations",
$"SELECT migration_name, category, checksum, applied_at FROM {quotedSchema}.schema_migrations",
connection);
await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
@@ -350,6 +363,7 @@ public abstract class StartupMigrationHost : IHostedService
migration.Name, migration.Category);
var sw = Stopwatch.StartNew();
var quotedSchema = QuoteIdentifier(_schemaName);
await using var transaction = await connection.BeginTransactionAsync(cancellationToken)
.ConfigureAwait(false);
@@ -366,7 +380,7 @@ public abstract class StartupMigrationHost : IHostedService
// Record migration
await using (var recordCommand = new NpgsqlCommand(
$"""
INSERT INTO {_schemaName}.schema_migrations
INSERT INTO {quotedSchema}.schema_migrations
(migration_name, category, checksum, duration_ms, applied_by)
VALUES (@name, @category, @checksum, @duration, @applied_by)
ON CONFLICT (migration_name) DO NOTHING
@@ -434,6 +448,12 @@ public abstract class StartupMigrationHost : IHostedService
return lastSlash >= 0 ? resourceName[(lastSlash + 1)..] : resourceName;
}
private static string QuoteIdentifier(string identifier)
{
var escaped = identifier.Replace("\"", "\"\"", StringComparison.Ordinal);
return $"\"{escaped}\"";
}
private record AppliedMigration(string Name, string Category, string Checksum, DateTimeOffset AppliedAt);
private record PendingMigration(string Name, string ResourceName, MigrationCategory Category, string Checksum, string Content);
}