Add basic database schema migration system
This commit is contained in:
		@@ -51,12 +51,12 @@ namespace PluralKit.Bot
 | 
				
			|||||||
                var logger = services.GetRequiredService<ILogger>().ForContext<Initialize>();
 | 
					                var logger = services.GetRequiredService<ILogger>().ForContext<Initialize>();
 | 
				
			||||||
                var coreConfig = services.GetRequiredService<CoreConfig>();
 | 
					                var coreConfig = services.GetRequiredService<CoreConfig>();
 | 
				
			||||||
                var botConfig = services.GetRequiredService<BotConfig>();
 | 
					                var botConfig = services.GetRequiredService<BotConfig>();
 | 
				
			||||||
 | 
					                var schema = services.GetRequiredService<SchemaService>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                using (Sentry.SentrySdk.Init(coreConfig.SentryUrl))
 | 
					                using (Sentry.SentrySdk.Init(coreConfig.SentryUrl))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    logger.Information("Connecting to database");
 | 
					                    logger.Information("Connecting to database");
 | 
				
			||||||
                    using (var conn = await services.GetRequiredService<DbConnectionFactory>().Obtain())
 | 
					                    await schema.ApplyMigrations();
 | 
				
			||||||
                        await Schema.CreateTables(conn);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    logger.Information("Connecting to Discord");
 | 
					                    logger.Information("Connecting to Discord");
 | 
				
			||||||
                    var client = services.GetRequiredService<IDiscordClient>() as DiscordShardedClient;
 | 
					                    var client = services.GetRequiredService<IDiscordClient>() as DiscordShardedClient;
 | 
				
			||||||
@@ -83,6 +83,7 @@ namespace PluralKit.Bot
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            .AddSingleton<DbConnectionCountHolder>()
 | 
					            .AddSingleton<DbConnectionCountHolder>()
 | 
				
			||||||
            .AddTransient<DbConnectionFactory>()
 | 
					            .AddTransient<DbConnectionFactory>()
 | 
				
			||||||
 | 
					            .AddTransient<SchemaService>()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .AddSingleton<IDiscordClient, DiscordShardedClient>(_ => new DiscordShardedClient(new DiscordSocketConfig
 | 
					            .AddSingleton<IDiscordClient, DiscordShardedClient>(_ => new DiscordShardedClient(new DiscordSocketConfig
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,9 @@
 | 
				
			|||||||
 | 
					-- SCHEMA VERSION 0, 2019-12-26
 | 
				
			||||||
 | 
					-- "initial version", considered a "starting point" for the migrations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- also the assumed database layout of someone either migrating from an older version of PK or starting a new instance,
 | 
				
			||||||
 | 
					-- so everything here *should* be idempotent given a schema version older than this or nonexistent.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
-- Create proxy_tag compound type if it doesn't exist
 | 
					-- Create proxy_tag compound type if it doesn't exist
 | 
				
			||||||
do $$ begin
 | 
					do $$ begin
 | 
				
			||||||
    create type proxy_tag as (
 | 
					    create type proxy_tag as (
 | 
				
			||||||
@@ -78,10 +84,6 @@ create table if not exists switches
 | 
				
			|||||||
    system    serial    not null references systems (id) on delete cascade,
 | 
					    system    serial    not null references systems (id) on delete cascade,
 | 
				
			||||||
    timestamp timestamp not null default (current_timestamp at time zone 'utc')
 | 
					    timestamp timestamp not null default (current_timestamp at time zone 'utc')
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_switches_system
 | 
					 | 
				
			||||||
ON switches USING btree (
 | 
					 | 
				
			||||||
	system ASC NULLS LAST
 | 
					 | 
				
			||||||
) INCLUDE ("timestamp");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
create table if not exists switch_members
 | 
					create table if not exists switch_members
 | 
				
			||||||
(
 | 
					(
 | 
				
			||||||
@@ -89,12 +91,6 @@ create table if not exists switch_members
 | 
				
			|||||||
    switch serial not null references switches (id) on delete cascade,
 | 
					    switch serial not null references switches (id) on delete cascade,
 | 
				
			||||||
    member serial not null references members (id) on delete cascade
 | 
					    member serial not null references members (id) on delete cascade
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE INDEX IF NOT EXISTS idx_switch_members_switch
 | 
					 | 
				
			||||||
ON switch_members USING btree (
 | 
					 | 
				
			||||||
	switch ASC NULLS LAST
 | 
					 | 
				
			||||||
) INCLUDE (member);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
create index if not exists idx_message_member on messages (member);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
create table if not exists webhooks
 | 
					create table if not exists webhooks
 | 
				
			||||||
(
 | 
					(
 | 
				
			||||||
@@ -110,3 +106,7 @@ create table if not exists servers
 | 
				
			|||||||
    log_blacklist bigint[] not null default array[]::bigint[],
 | 
					    log_blacklist bigint[] not null default array[]::bigint[],
 | 
				
			||||||
    blacklist     bigint[] not null default array[]::bigint[] 
 | 
					    blacklist     bigint[] not null default array[]::bigint[] 
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					create index if not exists idx_switches_system on switches using btree (system asc nulls last) include ("timestamp");
 | 
				
			||||||
 | 
					create index if not exists idx_switch_members_switch on switch_members using btree (switch asc nulls last) include (member);
 | 
				
			||||||
 | 
					create index if not exists idx_message_member on messages (member);
 | 
				
			||||||
							
								
								
									
										15
									
								
								PluralKit.Core/Migrations/1.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								PluralKit.Core/Migrations/1.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					-- SCHEMA VERSION 1: 2019-12-26
 | 
				
			||||||
 | 
					-- First version introducing the migration system, therefore we add the info/version table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					create table info
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					    id int primary key not null default 1, -- enforced only equal to 1
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    schema_version int,    
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    constraint singleton check (id = 1) -- enforce singleton table/row
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- We do an insert here since we *just* added the table
 | 
				
			||||||
 | 
					-- Future migrations should do an update at the end
 | 
				
			||||||
 | 
					insert into info (schema_version) values (1);
 | 
				
			||||||
@@ -28,8 +28,7 @@
 | 
				
			|||||||
    </ItemGroup>
 | 
					    </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ItemGroup>
 | 
					    <ItemGroup>
 | 
				
			||||||
      <None Remove="db_schema.sql" />
 | 
					      <EmbeddedResource Include="Migrations\*.sql" />
 | 
				
			||||||
      <EmbeddedResource Include="db_schema.sql" />
 | 
					 | 
				
			||||||
    </ItemGroup>
 | 
					    </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</Project>
 | 
					</Project>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +0,0 @@
 | 
				
			|||||||
using System.Data;
 | 
					 | 
				
			||||||
using System.IO;
 | 
					 | 
				
			||||||
using System.Threading.Tasks;
 | 
					 | 
				
			||||||
using Dapper;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace PluralKit {
 | 
					 | 
				
			||||||
    public static class Schema {
 | 
					 | 
				
			||||||
        public static async Task CreateTables(IDbConnection connection)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            // Load the schema from disk (well, embedded resource) and execute the commands in there
 | 
					 | 
				
			||||||
            using (var stream = typeof(Schema).Assembly.GetManifestResourceStream("PluralKit.Core.db_schema.sql"))
 | 
					 | 
				
			||||||
            using (var reader = new StreamReader(stream))
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                var result = await reader.ReadToEndAsync();
 | 
					 | 
				
			||||||
                await connection.ExecuteAsync(result);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										62
									
								
								PluralKit.Core/SchemaService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								PluralKit.Core/SchemaService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					using System;
 | 
				
			||||||
 | 
					using System.Data;
 | 
				
			||||||
 | 
					using System.IO;
 | 
				
			||||||
 | 
					using System.Threading.Tasks;
 | 
				
			||||||
 | 
					using Dapper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using Npgsql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					using Serilog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					namespace PluralKit {
 | 
				
			||||||
 | 
					    public class SchemaService
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        private const int TargetSchemaVersion = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private DbConnectionFactory _conn;
 | 
				
			||||||
 | 
					        private ILogger _logger;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        public SchemaService(DbConnectionFactory conn, ILogger logger)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _conn = conn;
 | 
				
			||||||
 | 
					            _logger = logger.ForContext<SchemaService>();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public async Task ApplyMigrations()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            for (var version = 0; version <= TargetSchemaVersion; version++) 
 | 
				
			||||||
 | 
					                await ApplyMigration(version);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private async Task ApplyMigration(int migrationId)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // migrationId is the *target* version
 | 
				
			||||||
 | 
					            using var conn = await _conn.Obtain();
 | 
				
			||||||
 | 
					            using var tx = conn.BeginTransaction();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            // See if we even have the info table... if not, we implicitly define the version as -1
 | 
				
			||||||
 | 
					            // This means migration 0 will get executed, which ensures we're at a consistent state.
 | 
				
			||||||
 | 
					            // *Technically* this also means schema version 0 will be identified as -1, but since we're only doing these
 | 
				
			||||||
 | 
					            // checks in the above for loop, this doesn't matter.
 | 
				
			||||||
 | 
					            var hasInfoTable = await conn.QuerySingleOrDefaultAsync<int>("select count(*) from information_schema.tables where table_name = 'info'") == 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            int currentVersion;
 | 
				
			||||||
 | 
					            if (hasInfoTable)
 | 
				
			||||||
 | 
					                currentVersion = await conn.QuerySingleOrDefaultAsync<int>("select schema_version from info");
 | 
				
			||||||
 | 
					            else currentVersion = -1;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (currentVersion >= migrationId)
 | 
				
			||||||
 | 
					                return; // Don't execute the migration if we're already at the target version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            using var stream = typeof(SchemaService).Assembly.GetManifestResourceStream($"PluralKit.Core.Migrations.{migrationId}.sql");
 | 
				
			||||||
 | 
					            if (stream == null) throw new ArgumentException("Invalid migration ID");
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            using var reader = new StreamReader(stream);
 | 
				
			||||||
 | 
					            var migrationQuery = await reader.ReadToEndAsync();
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            _logger.Information("Current schema version is {CurrentVersion}, applying migration {MigrationId}", currentVersion, migrationId);
 | 
				
			||||||
 | 
					            await conn.ExecuteAsync(migrationQuery, transaction: tx);
 | 
				
			||||||
 | 
					            tx.Commit();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user