122 lines
4 KiB
C#
122 lines
4 KiB
C#
using Npgsql;
|
|
|
|
namespace Femto.Database;
|
|
|
|
internal class Migrator(string migrationDirectory, NpgsqlDataSource dataSource) : IAsyncDisposable
|
|
{
|
|
private record MigrationScript(string Id, string Script);
|
|
|
|
public async Task Migrate()
|
|
{
|
|
var migrations = await LoadMigrationScripts();
|
|
|
|
await this.EnsureMigrationsTableExists();
|
|
var executedMigrations = await GetExecutedMigrations();
|
|
|
|
EnsureConsistentMigrationHistory(migrations, executedMigrations);
|
|
|
|
if (executedMigrations.Count == migrations.Count)
|
|
{
|
|
Console.WriteLine("up to date");
|
|
return;
|
|
}
|
|
|
|
|
|
await DoMigrations(migrations.Skip(executedMigrations.Count));
|
|
}
|
|
|
|
private async Task<IList<MigrationScript>> LoadMigrationScripts()
|
|
{
|
|
if (!Directory.Exists(migrationDirectory))
|
|
throw new DirectoryNotFoundException($"Migration directory not found: {migrationDirectory}");
|
|
|
|
var scriptFiles = Directory.EnumerateFiles(migrationDirectory, "*.sql")
|
|
.OrderBy(Path.GetFileName)
|
|
.ToList();
|
|
|
|
if (!scriptFiles.Any())
|
|
throw new Exception("No migration scripts found in the specified directory.");
|
|
|
|
var migrationScripts = new List<MigrationScript>();
|
|
|
|
foreach (var file in scriptFiles)
|
|
{
|
|
var scriptContent = await File.ReadAllTextAsync(file);
|
|
var scriptId = Path.GetFileName(file);
|
|
|
|
migrationScripts.Add(new MigrationScript(scriptId, scriptContent));
|
|
}
|
|
|
|
return migrationScripts;
|
|
}
|
|
|
|
private async Task EnsureMigrationsTableExists()
|
|
{
|
|
await using var createSchemaCommand = dataSource.CreateCommand(
|
|
"""CREATE SCHEMA IF NOT EXISTS __migrations;"""
|
|
);
|
|
|
|
await createSchemaCommand.ExecuteNonQueryAsync();
|
|
|
|
await using var createTableCommand = dataSource.CreateCommand(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS __migrations.__migration_history ( migration VARCHAR(127) PRIMARY KEY );
|
|
"""
|
|
);
|
|
|
|
await createTableCommand.ExecuteNonQueryAsync();
|
|
}
|
|
|
|
private async Task<IList<string>> GetExecutedMigrations()
|
|
{
|
|
await using var command = dataSource.CreateCommand(
|
|
"""SELECT migration FROM __migrations.__migration_history"""
|
|
);
|
|
var reader = await command.ExecuteReaderAsync();
|
|
var migrations = new List<string>();
|
|
while (await reader.ReadAsync())
|
|
{
|
|
migrations.Add(reader.GetString(0));
|
|
}
|
|
return migrations;
|
|
}
|
|
|
|
private void EnsureConsistentMigrationHistory(
|
|
IList<MigrationScript> migrationScripts,
|
|
IList<string> executedMigrationIds
|
|
)
|
|
{
|
|
if (executedMigrationIds.Count > migrationScripts.Count)
|
|
throw new Exception("inconsistent migration history");
|
|
|
|
for (var i = 0; i < executedMigrationIds.Count; i++)
|
|
{
|
|
var migration = migrationScripts[i];
|
|
var executedMigrationId = executedMigrationIds[i];
|
|
|
|
if (migration.Id != executedMigrationId)
|
|
throw new Exception($"unexpected migration in history {executedMigrationId}");
|
|
}
|
|
}
|
|
|
|
private async Task DoMigrations(IEnumerable<MigrationScript> migrationScripts)
|
|
{
|
|
foreach (var migration in migrationScripts)
|
|
{
|
|
Console.WriteLine($"applying migration {migration.Id}");
|
|
await using var command = dataSource.CreateCommand(migration.Script);
|
|
await command.ExecuteNonQueryAsync();
|
|
|
|
await using var addToHistoryCommand = dataSource.CreateCommand(
|
|
"""INSERT INTO __migrations.__migration_history (migration) VALUES (@migration)"""
|
|
);
|
|
addToHistoryCommand.Parameters.AddWithValue("@migration", migration.Id);
|
|
await addToHistoryCommand.ExecuteNonQueryAsync();
|
|
}
|
|
}
|
|
|
|
public ValueTask DisposeAsync()
|
|
{
|
|
return dataSource.DisposeAsync();
|
|
}
|
|
}
|