move database utils to Database/Utils, create DatabaseMigrator

This commit is contained in:
spiral 2021-08-01 11:13:32 -04:00
parent d633cd4906
commit 3d2435eb2e
No known key found for this signature in database
GPG Key ID: A6059F0CA0E1BD31
7 changed files with 91 additions and 62 deletions

View File

@ -18,21 +18,21 @@ namespace PluralKit.Core
{ {
internal class Database: IDatabase internal class Database: IDatabase
{ {
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 14;
private readonly CoreConfig _config; private readonly CoreConfig _config;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IMetrics _metrics; private readonly IMetrics _metrics;
private readonly DbConnectionCountHolder _countHolder; private readonly DbConnectionCountHolder _countHolder;
private readonly DatabaseMigrator _migrator;
private readonly string _connectionString; private readonly string _connectionString;
public Database(CoreConfig config, DbConnectionCountHolder countHolder, ILogger logger, public Database(CoreConfig config, DbConnectionCountHolder countHolder, ILogger logger,
IMetrics metrics) IMetrics metrics, DatabaseMigrator migrator)
{ {
_config = config; _config = config;
_countHolder = countHolder; _countHolder = countHolder;
_metrics = metrics; _metrics = metrics;
_migrator = migrator;
_logger = logger.ForContext<Database>(); _logger = logger.ForContext<Database>();
_connectionString = new NpgsqlConnectionStringBuilder(_config.Database) _connectionString = new NpgsqlConnectionStringBuilder(_config.Database)
@ -92,65 +92,8 @@ namespace PluralKit.Core
public async Task ApplyMigrations() public async Task ApplyMigrations()
{ {
// Run everything in a transaction using var conn = await Obtain();
await using var conn = await Obtain(); await _migrator.ApplyMigrations(conn);
await using var tx = await conn.BeginTransactionAsync();
// Before applying migrations, clean out views/functions to prevent type errors
await ExecuteSqlFile($"{RootPath}.clean.sql", conn, tx);
// Apply all migrations between the current database version and the target version
await ApplyMigrations(conn, tx);
// Now, reapply views/functions (we deleted them above, no need to worry about conflicts)
await ExecuteSqlFile($"{RootPath}.Views.views.sql", conn, tx);
await ExecuteSqlFile($"{RootPath}.Functions.functions.sql", conn, tx);
// Finally, commit tx
await tx.CommitAsync();
}
private async Task ApplyMigrations(IPKConnection conn, IDbTransaction tx)
{
var currentVersion = await GetCurrentDatabaseVersion(conn);
_logger.Information("Current schema version: {CurrentVersion}", currentVersion);
for (var migration = currentVersion + 1; migration <= TargetSchemaVersion; migration++)
{
_logger.Information("Applying schema migration {MigrationId}", migration);
await ExecuteSqlFile($"{RootPath}.Migrations.{migration}.sql", conn, tx);
}
}
private async Task ExecuteSqlFile(string resourceName, IPKConnection conn, IDbTransaction tx = null)
{
await using var stream = typeof(Database).Assembly.GetManifestResourceStream(resourceName);
if (stream == null) throw new ArgumentException($"Invalid resource name '{resourceName}'");
using var reader = new StreamReader(stream);
var query = await reader.ReadToEndAsync();
await conn.ExecuteAsync(query, transaction: tx);
// If the above creates new enum/composite types, we must tell Npgsql to reload the internal type caches
// This will propagate to every other connection as well, since it marks the global type mapper collection dirty.
((PKConnection) conn).ReloadTypes();
}
private async Task<int> GetCurrentDatabaseVersion(IPKConnection conn)
{
// First, check if the "info" table exists (it may not, if this is a *really* old database)
var hasInfoTable =
await conn.QuerySingleOrDefaultAsync<int>(
"select count(*) from information_schema.tables where table_name = 'info'") == 1;
// If we have the table, read the schema version
if (hasInfoTable)
return await conn.QuerySingleOrDefaultAsync<int>("select schema_version from info");
// If not, we return version "-1"
// This means migration 0 will get executed, getting us into a consistent state
// Then, migration 1 gets executed, which creates the info table and sets version to 1
return -1;
} }
private class PassthroughTypeHandler<T>: SqlMapper.TypeHandler<T> private class PassthroughTypeHandler<T>: SqlMapper.TypeHandler<T>

View File

@ -0,0 +1,85 @@
using System;
using System.Data;
using System.IO;
using System.Threading.Tasks;
using Dapper;
using Serilog;
namespace PluralKit.Core
{
internal class DatabaseMigrator
{
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files
private const int TargetSchemaVersion = 14;
private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger)
{
_logger = logger;
}
public async Task ApplyMigrations(IPKConnection conn)
{
// Run everything in a transaction
await using var tx = await conn.BeginTransactionAsync();
// Before applying migrations, clean out views/functions to prevent type errors
await ExecuteSqlFile($"{RootPath}.clean.sql", conn, tx);
// Apply all migrations between the current database version and the target version
await ApplyMigrations(conn, tx);
// Now, reapply views/functions (we deleted them above, no need to worry about conflicts)
await ExecuteSqlFile($"{RootPath}.Views.views.sql", conn, tx);
await ExecuteSqlFile($"{RootPath}.Functions.functions.sql", conn, tx);
// Finally, commit tx
await tx.CommitAsync();
}
private async Task ApplyMigrations(IPKConnection conn, IDbTransaction tx)
{
var currentVersion = await GetCurrentDatabaseVersion(conn);
_logger.Information("Current schema version: {CurrentVersion}", currentVersion);
for (var migration = currentVersion + 1; migration <= TargetSchemaVersion; migration++)
{
_logger.Information("Applying schema migration {MigrationId}", migration);
await ExecuteSqlFile($"{RootPath}.Migrations.{migration}.sql", conn, tx);
}
}
private async Task ExecuteSqlFile(string resourceName, IPKConnection conn, IDbTransaction tx = null)
{
await using var stream = typeof(Database).Assembly.GetManifestResourceStream(resourceName);
if (stream == null) throw new ArgumentException($"Invalid resource name '{resourceName}'");
using var reader = new StreamReader(stream);
var query = await reader.ReadToEndAsync();
await conn.ExecuteAsync(query, transaction: tx);
// If the above creates new enum/composite types, we must tell Npgsql to reload the internal type caches
// This will propagate to every other connection as well, since it marks the global type mapper collection dirty.
((PKConnection) conn).ReloadTypes();
}
private async Task<int> GetCurrentDatabaseVersion(IPKConnection conn)
{
// First, check if the "info" table exists (it may not, if this is a *really* old database)
var hasInfoTable =
await conn.QuerySingleOrDefaultAsync<int>(
"select count(*) from information_schema.tables where table_name = 'info'") == 1;
// If we have the table, read the schema version
if (hasInfoTable)
return await conn.QuerySingleOrDefaultAsync<int>("select schema_version from info");
// If not, we return version "-1"
// This means migration 0 will get executed, getting us into a consistent state
// Then, migration 1 gets executed, which creates the info table and sets version to 1
return -1;
}
}
}

View File

@ -10,6 +10,7 @@ namespace PluralKit.Core
protected override void Load(ContainerBuilder builder) protected override void Load(ContainerBuilder builder)
{ {
builder.RegisterType<DbConnectionCountHolder>().SingleInstance(); builder.RegisterType<DbConnectionCountHolder>().SingleInstance();
builder.RegisterType<DatabaseMigrator>().SingleInstance();
builder.RegisterType<Database>().As<IDatabase>().SingleInstance(); builder.RegisterType<Database>().As<IDatabase>().SingleInstance();
builder.RegisterType<ModelRepository>().AsSelf().SingleInstance(); builder.RegisterType<ModelRepository>().AsSelf().SingleInstance();