From 3d2435eb2e66283e2d71ae2b8fa079d66c1b093a Mon Sep 17 00:00:00 2001 From: spiral Date: Sun, 1 Aug 2021 11:13:32 -0400 Subject: [PATCH] move database utils to Database/Utils, create DatabaseMigrator --- PluralKit.Core/Database/Database.cs | 67 ++------------- .../{ => Database}/Utils/ConnectionUtils.cs | 0 .../Database/Utils/DatabaseMigrator.cs | 85 +++++++++++++++++++ .../Utils/DbConnectionCountHolder.cs} | 0 .../{ => Database}/Utils/QueryBuilder.cs | 0 .../Utils/UpdateQueryBuilder.cs | 0 PluralKit.Core/Modules/DataStoreModule.cs | 1 + 7 files changed, 91 insertions(+), 62 deletions(-) rename PluralKit.Core/{ => Database}/Utils/ConnectionUtils.cs (100%) create mode 100644 PluralKit.Core/Database/Utils/DatabaseMigrator.cs rename PluralKit.Core/{Utils/DatabaseUtils.cs => Database/Utils/DbConnectionCountHolder.cs} (100%) rename PluralKit.Core/{ => Database}/Utils/QueryBuilder.cs (100%) rename PluralKit.Core/{ => Database}/Utils/UpdateQueryBuilder.cs (100%) diff --git a/PluralKit.Core/Database/Database.cs b/PluralKit.Core/Database/Database.cs index cd614c92..966e416a 100644 --- a/PluralKit.Core/Database/Database.cs +++ b/PluralKit.Core/Database/Database.cs @@ -18,21 +18,21 @@ namespace PluralKit.Core { 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 ILogger _logger; private readonly IMetrics _metrics; private readonly DbConnectionCountHolder _countHolder; + private readonly DatabaseMigrator _migrator; private readonly string _connectionString; public Database(CoreConfig config, DbConnectionCountHolder countHolder, ILogger logger, - IMetrics metrics) + IMetrics metrics, DatabaseMigrator migrator) { _config = config; _countHolder = countHolder; _metrics = metrics; + _migrator = migrator; _logger = logger.ForContext(); _connectionString = new NpgsqlConnectionStringBuilder(_config.Database) @@ -92,65 +92,8 @@ namespace PluralKit.Core public async Task ApplyMigrations() { - // Run everything in a transaction - await using var conn = await Obtain(); - 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 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( - "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("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; + using var conn = await Obtain(); + await _migrator.ApplyMigrations(conn); } private class PassthroughTypeHandler: SqlMapper.TypeHandler diff --git a/PluralKit.Core/Utils/ConnectionUtils.cs b/PluralKit.Core/Database/Utils/ConnectionUtils.cs similarity index 100% rename from PluralKit.Core/Utils/ConnectionUtils.cs rename to PluralKit.Core/Database/Utils/ConnectionUtils.cs diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs new file mode 100644 index 00000000..ea93938b --- /dev/null +++ b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs @@ -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 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( + "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("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; + } + } +} diff --git a/PluralKit.Core/Utils/DatabaseUtils.cs b/PluralKit.Core/Database/Utils/DbConnectionCountHolder.cs similarity index 100% rename from PluralKit.Core/Utils/DatabaseUtils.cs rename to PluralKit.Core/Database/Utils/DbConnectionCountHolder.cs diff --git a/PluralKit.Core/Utils/QueryBuilder.cs b/PluralKit.Core/Database/Utils/QueryBuilder.cs similarity index 100% rename from PluralKit.Core/Utils/QueryBuilder.cs rename to PluralKit.Core/Database/Utils/QueryBuilder.cs diff --git a/PluralKit.Core/Utils/UpdateQueryBuilder.cs b/PluralKit.Core/Database/Utils/UpdateQueryBuilder.cs similarity index 100% rename from PluralKit.Core/Utils/UpdateQueryBuilder.cs rename to PluralKit.Core/Database/Utils/UpdateQueryBuilder.cs diff --git a/PluralKit.Core/Modules/DataStoreModule.cs b/PluralKit.Core/Modules/DataStoreModule.cs index 6060cbed..5e1c8922 100644 --- a/PluralKit.Core/Modules/DataStoreModule.cs +++ b/PluralKit.Core/Modules/DataStoreModule.cs @@ -10,6 +10,7 @@ namespace PluralKit.Core protected override void Load(ContainerBuilder builder) { builder.RegisterType().SingleInstance(); + builder.RegisterType().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().AsSelf().SingleInstance();