move database utils to Database/Utils, create DatabaseMigrator
This commit is contained in:
		| @@ -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> | ||||||
|   | |||||||
							
								
								
									
										85
									
								
								PluralKit.Core/Database/Utils/DatabaseMigrator.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								PluralKit.Core/Database/Utils/DatabaseMigrator.cs
									
									
									
									
									
										Normal 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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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(); | ||||||
|              |              | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user