Add basic database schema migration system
This commit is contained in:
parent
4d07886ec8
commit
4a30e56298
@ -51,12 +51,12 @@ namespace PluralKit.Bot
|
||||
var logger = services.GetRequiredService<ILogger>().ForContext<Initialize>();
|
||||
var coreConfig = services.GetRequiredService<CoreConfig>();
|
||||
var botConfig = services.GetRequiredService<BotConfig>();
|
||||
var schema = services.GetRequiredService<SchemaService>();
|
||||
|
||||
using (Sentry.SentrySdk.Init(coreConfig.SentryUrl))
|
||||
{
|
||||
logger.Information("Connecting to database");
|
||||
using (var conn = await services.GetRequiredService<DbConnectionFactory>().Obtain())
|
||||
await Schema.CreateTables(conn);
|
||||
await schema.ApplyMigrations();
|
||||
|
||||
logger.Information("Connecting to Discord");
|
||||
var client = services.GetRequiredService<IDiscordClient>() as DiscordShardedClient;
|
||||
@ -83,6 +83,7 @@ namespace PluralKit.Bot
|
||||
|
||||
.AddSingleton<DbConnectionCountHolder>()
|
||||
.AddTransient<DbConnectionFactory>()
|
||||
.AddTransient<SchemaService>()
|
||||
|
||||
.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
|
||||
do $$ begin
|
||||
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,
|
||||
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
|
||||
(
|
||||
@ -89,12 +91,6 @@ create table if not exists switch_members
|
||||
switch serial not null references switches (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
|
||||
(
|
||||
@ -110,3 +106,7 @@ create table if not exists servers
|
||||
log_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>
|
||||
<None Remove="db_schema.sql" />
|
||||
<EmbeddedResource Include="db_schema.sql" />
|
||||
<EmbeddedResource Include="Migrations\*.sql" />
|
||||
</ItemGroup>
|
||||
|
||||
</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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user