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 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user