tgstation-server  4.3.2
The /tg/station 13 server suite
DatabaseContext.cs
Go to the documentation of this file.
1 using Microsoft.EntityFrameworkCore;
2 using Microsoft.EntityFrameworkCore.Infrastructure;
3 using Microsoft.EntityFrameworkCore.Migrations;
4 using Microsoft.Extensions.DependencyInjection;
5 using Microsoft.Extensions.Logging;
6 using Microsoft.Extensions.Options;
7 using System;
8 using System.Globalization;
9 using System.Linq;
10 using System.Threading;
11 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Database
17 {
21 #pragma warning disable CA1506 // TODO: Decomplexify
23  {
26 
30  public DbSet<User> Users { get; set; }
31 
35  public DbSet<Instance> Instances { get; set; }
36 
40  public DbSet<CompileJob> CompileJobs { get; set; }
41 
45  public DbSet<RevisionInformation> RevisionInformations { get; set; }
46 
50  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
51 
55  public DbSet<ChatBot> ChatBots { get; set; }
56 
60  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
61 
65  public DbSet<RepositorySettings> RepositorySettings { get; set; }
66 
70  public DbSet<InstanceUser> InstanceUsers { get; set; }
71 
75  public DbSet<ChatChannel> ChatChannels { get; set; }
76 
80  public DbSet<Job> Jobs { get; set; }
81 
85  public DbSet<ReattachInformation> ReattachInformations { get; set; }
86 
90  public DbSet<DualReattachInformation> WatchdogReattachInformations { get; set; }
91 
95  public DbSet<TestMerge> TestMerges { get; set; }
96 
100  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
101 
105  protected ILogger Logger { get; }
106 
111 
114 
117 
120 
123 
126 
129 
132 
135 
138 
141 
144 
147 
150 
155 
160 
165 
170 
175 
180 
185 
190 
195 
200 
205 
210 
215 
220 
228  public DatabaseContext(DbContextOptions dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
229  {
230  DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
231  this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
232  Logger = logger ?? throw new ArgumentNullException(nameof(logger));
233 
234  usersCollection = new DatabaseCollection<User>(Users);
235  instancesCollection = new DatabaseCollection<Instance>(Instances);
236  instanceUsersCollection = new DatabaseCollection<InstanceUser>(InstanceUsers);
237  compileJobsCollection = new DatabaseCollection<CompileJob>(CompileJobs);
238  repositorySettingsCollection = new DatabaseCollection<RepositorySettings>(RepositorySettings);
239  dreamMakerSettingsCollection = new DatabaseCollection<DreamMakerSettings>(DreamMakerSettings);
240  dreamDaemonSettingsCollection = new DatabaseCollection<DreamDaemonSettings>(DreamDaemonSettings);
241  chatBotsCollection = new DatabaseCollection<ChatBot>(ChatBots);
242  chatChannelsCollection = new DatabaseCollection<ChatChannel>(ChatChannels);
243  revisionInformationsCollection = new DatabaseCollection<RevisionInformation>(RevisionInformations);
244  jobsCollection = new DatabaseCollection<Job>(Jobs);
245  reattachInformationsCollection = new DatabaseCollection<ReattachInformation>(ReattachInformations);
246  watchdogReattachInformationsCollection = new DatabaseCollection<DualReattachInformation>(WatchdogReattachInformations);
247  }
248 
250  protected override void OnModelCreating(ModelBuilder modelBuilder)
251  {
252  // Setup our more complex database relations
253  Logger.LogTrace("Building entity framework context...");
254  base.OnModelCreating(modelBuilder);
255 
256  var userModel = modelBuilder.Entity<User>();
257  userModel.HasIndex(x => x.CanonicalName).IsUnique();
258  userModel.HasIndex(x => x.SystemIdentifier).IsUnique();
259  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
260 
261  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
262 
263  var revInfo = modelBuilder.Entity<RevisionInformation>();
264  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
265  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Cascade);
266  revInfo.HasIndex(x => new { x.InstanceId, x.CommitSha }).IsUnique();
267 
268  // IMPORTANT: When an instance is deleted (detached) it cascades into the maze of revinfo/testmerge/ritm/compilejob/job/ri relations
269  // This maze starts at revInfo and jobs
270  // jobs takes care of deleting compile jobs and ris
271  // rev info takes care of the rest
272  // Break the link here so the db doesn't shit itself complaining about cascading deletes
273  // EF will handle making the right query to destroy everything
274  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.ClientNoAction);
275 
276  // Also break the link between ritm and testmerge so it doesn't cycle in a triangle with rev info
277  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.ClientNoAction);
278 
279  var compileJob = modelBuilder.Entity<CompileJob>();
280  compileJob.HasIndex(x => x.DirectoryName);
281  compileJob.HasOne(x => x.Job).WithOne().OnDelete(DeleteBehavior.Cascade);
282 
283  var chatChannel = modelBuilder.Entity<ChatChannel>();
284  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
285  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
286  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
287 
288  modelBuilder.Entity<ChatBot>().HasIndex(x => new { x.InstanceId, x.Name }).IsUnique();
289 
290  var instanceModel = modelBuilder.Entity<Instance>();
291  instanceModel.HasIndex(x => x.Path).IsUnique();
292  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
293  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
294  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
295  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
296  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
297  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
298  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
299  instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade);
300  }
301 
303  public async Task Initialize(CancellationToken cancellationToken)
304  {
305  ValidateDatabaseType();
306 
308  {
309  Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database...");
310  await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false);
311  }
312 
313  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
314  var wasEmpty = !migrations.Any();
315 
316  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
317  {
318  Logger.LogInformation("Migrating database...");
319  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
320  }
321  else
322  Logger.LogDebug("No migrations to apply.");
323 
324  wasEmpty |= (await Users.AsQueryable().CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
325 
326  if (wasEmpty)
327  {
328  Logger.LogInformation("Seeding database...");
329  await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
330  }
331  else
332  {
334  {
335  Logger.LogWarning("Enabling and resetting admin password due to configuration!");
336  await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
337  }
338 
339  await databaseSeeder.SanitizeDatabase(this, cancellationToken).ConfigureAwait(false);
340  }
341  }
342 
344  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
345 
347  public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
348  {
349  if (version == null)
350  throw new ArgumentNullException(nameof(version));
351  if (version < new Version(4, 0))
352  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
353 
354  // Update this with new migrations as they are made
355  string targetMigration = null;
356 
357  if (DatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0))
358  throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!");
359 
360  if (version < new Version(4, 1, 0))
361  throw new NotSupportedException("Cannot migrate below version 4.1.0!");
362 
363  if (version < new Version(4, 2, 0))
364  targetMigration = DatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete);
365 
366  if (targetMigration == null)
367  {
368  Logger.LogDebug("No down migration required.");
369  return;
370  }
371 
372  string migrationSubstitution;
373  switch (DatabaseType)
374  {
375  case DatabaseType.SqlServer:
376  // already setup
377  migrationSubstitution = null;
378  break;
379  case DatabaseType.MySql:
380  case DatabaseType.MariaDB:
381  migrationSubstitution = "MY{0}";
382  break;
383  case DatabaseType.Sqlite:
384  migrationSubstitution = "SL{0}";
385  break;
386  case DatabaseType.PostgresSql:
387  migrationSubstitution = "PG{0}";
388  break;
389  default:
390  throw new InvalidOperationException($"Invalid DatabaseType: {DatabaseType}");
391  }
392 
393  if (migrationSubstitution != null)
394  targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration.Substring(2));
395 
396  // even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
397  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
398  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
399 
400  Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
401  try
402  {
403  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
404  }
405  catch (Exception e)
406  {
407  Logger.LogCritical("Failed to migrate! Exception: {0}", e);
408  }
409  }
410 
414  protected abstract void ValidateDatabaseType();
415  }
416 }
IDatabaseCollection< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
IDatabaseCollection< InstanceUser > InstanceUsers
The InstanceUsers in the IDatabaseContext
IDatabaseCollection< ChatChannel > ChatChannels
The ChatChannel in the IDatabaseContext
readonly IDatabaseCollection< DualReattachInformation > watchdogReattachInformationsCollection
Backing field for IDatabaseContext.WatchdogReattachInformations.
readonly IDatabaseCollection< ChatChannel > chatChannelsCollection
Backing field for IDatabaseContext.ChatChannels.
For initially seeding a database
Fix cascading data deletes for Models.Instances on MSSQL.
IDatabaseCollection< CompileJob > CompileJobs
The CompileJobs in the IDatabaseContext
Backend abstract implementation of IDatabaseContext
IDatabaseCollection< Instance > Instances
The Instances in the IDatabaseContext
IDatabaseCollection< ChatBot > ChatBots
The ChatBots in the IDatabaseContext
DatabaseType DatabaseType
The Configuration.DatabaseType to create
IDatabaseCollection< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
readonly IDatabaseCollection< Instance > instancesCollection
Backing field for IDatabaseContext.Instances.
readonly IDatabaseSeeder databaseSeeder
The IDatabaseSeeder for the DatabaseContext
readonly IDatabaseCollection< DreamMakerSettings > dreamMakerSettingsCollection
Backing field for IDatabaseContext.DreamMakerSettings.
bool ResetAdminPassword
If the admin user should be enabled and have it&#39;s password reset
IDatabaseCollection< DreamMakerSettings > DreamMakerSettings
The Models.DreamMakerSettings in the IDatabaseContext
readonly IDatabaseCollection< User > usersCollection
Backing field for IDatabaseContext.Users.
readonly IDatabaseCollection< RevisionInformation > revisionInformationsCollection
Backing field for IDatabaseContext.RevisionInformations.
Represents an Api.Models.Instance in the database
Definition: Instance.cs:8
async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
DatabaseContext(DbContextOptions dbContextOptions, IOptions< DatabaseConfiguration > databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger)
Construct a DatabaseContext
readonly IDatabaseCollection< CompileJob > compileJobsCollection
Backing field for IDatabaseContext.CompileJobs.
IDatabaseCollection< DualReattachInformation > WatchdogReattachInformations
The DbSet<TEntity> for DualReattachInformations
Configuration options for the Database.DatabaseContext
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
readonly IDatabaseCollection< InstanceUser > instanceUsersCollection
Backing field for IDatabaseContext.InstanceUsers.
bool DropDatabase
If the database should be deleted on application startup. Should not be used in production! ...
override void OnModelCreating(ModelBuilder modelBuilder)
readonly IDatabaseCollection< DreamDaemonSettings > dreamDaemonSettingsCollection
Backing field for IDatabaseContext.DreamDaemonSettings.
readonly IDatabaseCollection< ChatBot > chatBotsCollection
Backing field for IDatabaseContext.ChatBots.
readonly IDatabaseCollection< RepositorySettings > repositorySettingsCollection
Backing field for IDatabaseContext.RepositorySettings.
IDatabaseCollection< DreamDaemonSettings > DreamDaemonSettings
The Models.DreamDaemonSettings in the IDatabaseContext
IDatabaseCollection< ReattachInformation > ReattachInformations
The DbSet<TEntity> for ReattachInformations
IDatabaseCollection< Job > Jobs
The Jobs in the IDatabaseContext
async Task Initialize(CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext
readonly IDatabaseCollection< ReattachInformation > reattachInformationsCollection
Backing field for IDatabaseContext.ReattachInformations.
IDatabaseCollection< User > Users
The Users in the IDatabaseContext
readonly IDatabaseCollection< Job > jobsCollection
Backing field for IDatabaseContext.Jobs.