init
This commit is contained in:
commit
ab2e20f7e1
72 changed files with 2000 additions and 0 deletions
122
Femto.Database/Migrator.cs
Normal file
122
Femto.Database/Migrator.cs
Normal file
|
@ -0,0 +1,122 @@
|
|||
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();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue