Skip to content

Commit

Permalink
feat: add db migration
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverbooth committed Jul 21, 2024
1 parent 7360a11 commit 2e00364
Show file tree
Hide file tree
Showing 6 changed files with 236 additions and 7 deletions.
59 changes: 59 additions & 0 deletions Hammer/Commands/MigrateDatabaseCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.SlashCommands;
using DSharpPlus.SlashCommands.Attributes;
using Hammer.Services;
using Microsoft.Extensions.Logging;

namespace Hammer.Commands;

internal sealed class MigrateDatabaseCommand : ApplicationCommandModule
{
private readonly ILogger<MigrateDatabaseCommand> _logger;
private readonly DatabaseService _databaseService;

/// <summary>
/// Initializes a new instance of the <see cref="MigrateDatabaseCommand" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="databaseService">The database service.</param>
public MigrateDatabaseCommand(ILogger<MigrateDatabaseCommand> logger, DatabaseService databaseService)
{
_logger = logger;
_databaseService = databaseService;
}

[SlashCommand("migratedb", "Migrates the SQLite database to MySQL/MariaDB.", false)]
[SlashRequireGuild]
public async Task MigrateDatabaseAsync(InteractionContext context)
{
var embed = new DiscordEmbedBuilder();
embed.WithColor(DiscordColor.Orange);
embed.WithTitle("⏳ Migration in progress");
embed.WithDescription("Please wait while the database is migrated...");

await context.CreateResponseAsync(embed);
try
{
int rows = await _databaseService.MigrateAsync();

var builder = new DiscordWebhookBuilder();
embed.WithColor(DiscordColor.Green);
embed.WithTitle("✅ Migration complete");
embed.WithDescription($"All data has been successfully migrated. {rows:N0} rows affected");
builder.AddEmbed(embed);
await context.EditResponseAsync(builder);
}
catch (Exception exception)
{
_logger.LogError(exception, "Failed to migrate");
var builder = new DiscordWebhookBuilder();
embed.WithColor(DiscordColor.Red);
embed.WithTitle($"⚠️ Migration failed: {exception.GetType().Name}");
embed.WithDescription($"{exception.GetType().Name} was thrown during migration: {exception.Message}\n\n" +
"View the log for more details");
builder.AddEmbed(embed);
await context.EditResponseAsync(builder);
}
}
}
2 changes: 1 addition & 1 deletion Hammer/Data/HammerContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace Hammer.Data;

/// <summary>
/// Represents a session with the <c>hammer.db</c> database.
/// Represents a session with the Hammer database.
/// </summary>
internal sealed class HammerContext : DbContext
{
Expand Down
116 changes: 116 additions & 0 deletions Hammer/Data/MigrationContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
using Hammer.Data.EntityConfigurations;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MuteConfiguration = Hammer.Data.EntityConfigurations.MuteConfiguration;

namespace Hammer.Data;

/// <summary>
/// Represents a session with the <c>hammer.db</c> database.
/// </summary>
internal sealed class MigrationContext : DbContext
{
private readonly ILogger<MigrationContext> _logger;

/// <summary>
/// Initializes a new instance of the <see cref="MigrationContext" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
public MigrationContext(ILogger<MigrationContext> logger)
{
_logger = logger;
}

/// <summary>
/// Gets the set of alt accounts.
/// </summary>
/// <value>The set of alt accounts.</value>
public DbSet<AltAccount> AltAccounts { get; private set; } = null!;

/// <summary>
/// Gets the set of users who are blocked from making reports.
/// </summary>
/// <value>The set of blocked reporters.</value>
public DbSet<BlockedReporter> BlockedReporters { get; private set; } = null!;

/// <summary>
/// Gets the set of staff-deleted messages.
/// </summary>
/// <value>The set of staff-deleted messages.</value>
public DbSet<DeletedMessage> DeletedMessages { get; private set; } = null!;

/// <summary>
/// Gets the set of infractions.
/// </summary>
/// <value>The set of infractions.</value>
public DbSet<Infraction> Infractions { get; private set; } = null!;

/// <summary>
/// Gets the set of member notes.
/// </summary>
/// <value>The set of member notes.</value>
public DbSet<MemberNote> MemberNotes { get; private set; } = null!;

/// <summary>
/// Gets the set of mutes.
/// </summary>
/// <value>The set of mutes.</value>
public DbSet<Mute> Mutes { get; private set; } = null!;

/// <summary>
/// Gets the set of reported messages.
/// </summary>
/// <value>The set of reported messages.</value>
public DbSet<ReportedMessage> ReportedMessages { get; private set; } = null!;

/// <summary>
/// Gets the set of rules.
/// </summary>
/// <value>The set of rules.</value>
public DbSet<Rule> Rules { get; private set; } = null!;

/// <summary>
/// Gets the set of staff messages.
/// </summary>
/// <value>The set of staff messages.</value>
public DbSet<StaffMessage> StaffMessages { get; private set; } = null!;

/// <summary>
/// Gets the set of temporary bans.
/// </summary>
/// <value>The set of temporary bans.</value>
public DbSet<TemporaryBan> TemporaryBans { get; private set; } = null!;

/// <summary>
/// Gets the set of tracked messages.
/// </summary>
/// <value>The set of tracked messages.</value>
public DbSet<TrackedMessage> TrackedMessages { get; private set; } = null!;

/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
base.OnConfiguring(optionsBuilder);

_logger.LogDebug("Using SQLite database provider");
optionsBuilder.UseSqlite("Data Source='data/hammer.db'");
}

/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.ApplyConfiguration(new AltAccountConfiguration(false));
modelBuilder.ApplyConfiguration(new BlockedReporterConfiguration(false));
modelBuilder.ApplyConfiguration(new DeletedMessageConfiguration(false));
modelBuilder.ApplyConfiguration(new InfractionConfiguration(false));
modelBuilder.ApplyConfiguration(new MemberNoteConfiguration(false));
modelBuilder.ApplyConfiguration(new MuteConfiguration(false));
modelBuilder.ApplyConfiguration(new StaffMessageConfiguration());
modelBuilder.ApplyConfiguration(new ReportedMessageConfiguration());
modelBuilder.ApplyConfiguration(new TemporaryBanConfiguration(false));
modelBuilder.ApplyConfiguration(new TrackedMessageConfiguration(false));
modelBuilder.ApplyConfiguration(new RuleConfiguration());
}
}
1 change: 1 addition & 0 deletions Hammer/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
}));

builder.Services.AddDbContextFactory<HammerContext>();
builder.Services.AddDbContextFactory<MigrationContext>();
builder.Services.AddHostedSingleton<DatabaseService>();

builder.Services.AddSingleton<HttpClient>();
Expand Down
1 change: 1 addition & 0 deletions Hammer/Services/BotService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
slashCommands.RegisterCommands<KickCommand>();
slashCommands.RegisterCommands<MessageCommand>();
slashCommands.RegisterCommands<MessageHistoryCommand>();
slashCommands.RegisterCommands<MigrateDatabaseCommand>();
slashCommands.RegisterCommands<MuteCommand>();
slashCommands.RegisterCommands<NoteCommand>();
slashCommands.RegisterCommands<PruneInfractionsCommand>();
Expand Down
64 changes: 58 additions & 6 deletions Hammer/Services/DatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,87 @@ internal sealed class DatabaseService : BackgroundService
{
private readonly ILogger<DatabaseService> _logger;
private readonly IDbContextFactory<HammerContext> _dbContextFactory;
private readonly IDbContextFactory<MigrationContext> _migrationContextFactory;

/// <summary>
/// Initializes a new instance of the <see cref="DatabaseService" /> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="dbContextFactory">The DbContext factory.</param>
public DatabaseService(ILogger<DatabaseService> logger, IDbContextFactory<HammerContext> dbContextFactory)
/// <param name="dbContextFactory">The <see cref="HammerContext" /> factory.</param>
/// <param name="migrationContextFactory">The <see cref="MigrationContext" /> factory.</param>
public DatabaseService(ILogger<DatabaseService> logger,
IDbContextFactory<HammerContext> dbContextFactory,
IDbContextFactory<MigrationContext> migrationContextFactory)
{
_logger = logger;
_dbContextFactory = dbContextFactory;
_migrationContextFactory = migrationContextFactory;
}

/// <summary>
/// Migrates the database from one source to another.
/// </summary>
public async Task<int> MigrateAsync()
{
await using HammerContext context = await _dbContextFactory.CreateDbContextAsync();
await context.Database.EnsureCreatedAsync();

if (!context.IsMySql)
{
_logger.LogWarning("Cannot migrate from SQLite to SQLite. This operation will be skipped");
return 0;
}

await using MigrationContext migration = await _migrationContextFactory.CreateDbContextAsync();

_logger.LogInformation("Migrating database");
context.AltAccounts.AddRange(migration.AltAccounts);
context.BlockedReporters.AddRange(migration.BlockedReporters);
context.DeletedMessages.AddRange(migration.DeletedMessages);
context.Infractions.AddRange(migration.Infractions);
context.MemberNotes.AddRange(migration.MemberNotes);
context.Mutes.AddRange(migration.Mutes);
context.ReportedMessages.AddRange(migration.ReportedMessages);
context.Rules.AddRange(migration.Rules);
context.StaffMessages.AddRange(migration.StaffMessages);
context.TemporaryBans.AddRange(migration.TemporaryBans);
context.TrackedMessages.AddRange(migration.TrackedMessages);

_logger.LogDebug("Saving database");
await context.SaveChangesAsync();

int count = 0;
count += context.AltAccounts.Count();
count += context.BlockedReporters.Count();
count += context.DeletedMessages.Count();
count += context.Infractions.Count();
count += context.MemberNotes.Count();
count += context.Mutes.Count();
count += context.ReportedMessages.Count();
count += context.Rules.Count();
count += context.StaffMessages.Count();
count += context.TemporaryBans.Count();
count += context.TrackedMessages.Count();
return count;
}

/// <inheritdoc />
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await CreateDatabaseAsync().ConfigureAwait(false);
await CreateDatabaseAsync();
}

private async Task CreateDatabaseAsync()
{
await using HammerContext context = await _dbContextFactory.CreateDbContextAsync().ConfigureAwait(false);
await using HammerContext context = await _dbContextFactory.CreateDbContextAsync();

if (Environment.GetEnvironmentVariable("USE_MYSQL") != "1")
{
_logger.LogInformation("Creating database");
await context.Database.EnsureCreatedAsync().ConfigureAwait(false);
await context.Database.EnsureCreatedAsync();
}

_logger.LogInformation("Applying migrations");
await context.Database.MigrateAsync().ConfigureAwait(false);
await context.Database.MigrateAsync();
}
}

0 comments on commit 2e00364

Please sign in to comment.