tgstation-server  4.4.0
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 System;
7 using System.Globalization;
8 using System.Linq;
9 using System.Reflection;
10 using System.Threading;
11 using System.Threading.Tasks;
15 
16 namespace Tgstation.Server.Host.Database
17 {
21 #pragma warning disable CA1506 // TODO: Decomplexify
22  public abstract class DatabaseContext : DbContext, IDatabaseContext
23  {
27  public DbSet<User> Users { get; set; }
28 
32  public DbSet<Instance> Instances { get; set; }
33 
37  public DbSet<CompileJob> CompileJobs { get; set; }
38 
42  public DbSet<RevisionInformation> RevisionInformations { get; set; }
43 
47  public DbSet<DreamMakerSettings> DreamMakerSettings { get; set; }
48 
52  public DbSet<ChatBot> ChatBots { get; set; }
53 
57  public DbSet<DreamDaemonSettings> DreamDaemonSettings { get; set; }
58 
62  public DbSet<RepositorySettings> RepositorySettings { get; set; }
63 
67  public DbSet<InstanceUser> InstanceUsers { get; set; }
68 
72  public DbSet<ChatChannel> ChatChannels { get; set; }
73 
77  public DbSet<Job> Jobs { get; set; }
78 
82  public DbSet<ReattachInformation> ReattachInformations { get; set; }
83 
87  public DbSet<TestMerge> TestMerges { get; set; }
88 
92  public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
93 
97  protected virtual DeleteBehavior RevInfoCompileJobDeleteBehavior => DeleteBehavior.ClientNoAction;
98 
101 
104 
107 
110 
113 
116 
119 
122 
125 
128 
131 
134 
139 
144 
149 
154 
159 
164 
169 
174 
179 
184 
189 
194 
200  public static Action<DbContextOptionsBuilder, DatabaseConfiguration> GetConfigureAction<TDatabaseContext>()
201  where TDatabaseContext : DatabaseContext
202  {
203  // HACK HACK HACK HACK HACK
204  const string ConfigureMethodName = nameof(SqlServerDatabaseContext.ConfigureWith);
205  var configureFunction = typeof(TDatabaseContext).GetMethod(
206  ConfigureMethodName,
207  BindingFlags.Public | BindingFlags.Static);
208 
209  if (configureFunction == null)
210  throw new InvalidOperationException($"Context type {typeof(TDatabaseContext).FullName} missing static {ConfigureMethodName} function!");
211 
212  return (optionsBuilder, config) => configureFunction.Invoke(null, new object[] { optionsBuilder, config });
213  }
214 
219  public DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
220  {
221  usersCollection = new DatabaseCollection<User>(Users);
222  instancesCollection = new DatabaseCollection<Instance>(Instances);
223  instanceUsersCollection = new DatabaseCollection<InstanceUser>(InstanceUsers);
224  compileJobsCollection = new DatabaseCollection<CompileJob>(CompileJobs);
225  repositorySettingsCollection = new DatabaseCollection<RepositorySettings>(RepositorySettings);
226  dreamMakerSettingsCollection = new DatabaseCollection<DreamMakerSettings>(DreamMakerSettings);
227  dreamDaemonSettingsCollection = new DatabaseCollection<DreamDaemonSettings>(DreamDaemonSettings);
228  chatBotsCollection = new DatabaseCollection<ChatBot>(ChatBots);
229  chatChannelsCollection = new DatabaseCollection<ChatChannel>(ChatChannels);
230  revisionInformationsCollection = new DatabaseCollection<RevisionInformation>(RevisionInformations);
231  jobsCollection = new DatabaseCollection<Job>(Jobs);
232  reattachInformationsCollection = new DatabaseCollection<ReattachInformation>(ReattachInformations);
233  }
234 
236  protected override void OnModelCreating(ModelBuilder modelBuilder)
237  {
238  if (modelBuilder == null)
239  throw new ArgumentNullException(nameof(modelBuilder));
240 
241  base.OnModelCreating(modelBuilder);
242 
243  var userModel = modelBuilder.Entity<User>();
244  userModel.HasIndex(x => x.CanonicalName).IsUnique();
245  userModel.HasIndex(x => x.SystemIdentifier).IsUnique();
246  userModel.HasMany(x => x.TestMerges).WithOne(x => x.MergedBy).OnDelete(DeleteBehavior.Restrict);
247 
248  modelBuilder.Entity<InstanceUser>().HasIndex(x => new { x.UserId, x.InstanceId }).IsUnique();
249 
250  var revInfo = modelBuilder.Entity<RevisionInformation>();
251  revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade);
252  revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Cascade);
253  revInfo.HasIndex(x => new { x.InstanceId, x.CommitSha }).IsUnique();
254 
255  // IMPORTANT: When an instance is deleted (detached) it cascades into the maze of revinfo/testmerge/ritm/compilejob/job/ri relations
256  // This maze starts at revInfo and jobs
257  // jobs takes care of deleting compile jobs and ris
258  // rev info takes care of the rest
259  // Break the link here so the db doesn't shit itself complaining about cascading deletes
260  // EF will handle making the right query to destroy everything
261  // UPDATE: I fuck with this constantly in hopes of eliminating FK issues on instance detack
262  revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(RevInfoCompileJobDeleteBehavior);
263 
264  // Also break the link between ritm and testmerge so it doesn't cycle in a triangle with rev info
265  modelBuilder.Entity<TestMerge>().HasMany(x => x.RevisonInformations).WithOne(x => x.TestMerge).OnDelete(DeleteBehavior.ClientNoAction);
266 
267  var compileJob = modelBuilder.Entity<CompileJob>();
268  compileJob.HasIndex(x => x.DirectoryName);
269  compileJob.HasOne(x => x.Job).WithOne().OnDelete(DeleteBehavior.Cascade);
270 
271  modelBuilder.Entity<ReattachInformation>().HasOne(x => x.CompileJob).WithMany().OnDelete(DeleteBehavior.Cascade);
272 
273  var chatChannel = modelBuilder.Entity<ChatChannel>();
274  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique();
275  chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique();
276  chatChannel.HasOne(x => x.ChatSettings).WithMany(x => x.Channels).HasForeignKey(x => x.ChatSettingsId).OnDelete(DeleteBehavior.Cascade);
277 
278  modelBuilder.Entity<ChatBot>().HasIndex(x => new { x.InstanceId, x.Name }).IsUnique();
279 
280  var instanceModel = modelBuilder.Entity<Instance>();
281  instanceModel.HasIndex(x => x.Path).IsUnique();
282  instanceModel.HasMany(x => x.ChatSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
283  instanceModel.HasOne(x => x.DreamDaemonSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
284  instanceModel.HasOne(x => x.DreamMakerSettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
285  instanceModel.HasOne(x => x.RepositorySettings).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
286  instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
287  instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
288  instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
289  }
290 
292  public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
293 
295  public Task Drop(CancellationToken cancellationToken) => Database.EnsureDeletedAsync(cancellationToken);
296 
298  public async Task<bool> Migrate(ILogger<DatabaseContext> logger, CancellationToken cancellationToken)
299  {
300  if (logger == null)
301  throw new ArgumentNullException(nameof(logger));
302  var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
303  var wasEmpty = !migrations.Any();
304 
305  if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any())
306  {
307  logger.LogInformation("Migrating database...");
308  await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
309  }
310  else
311  logger.LogDebug("No migrations to apply");
312 
313  wasEmpty |= (await Users.AsQueryable().CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
314 
315  return wasEmpty;
316  }
317 
320  ILogger<DatabaseContext> logger,
321  Version version,
322  DatabaseType currentDatabaseType,
323  CancellationToken cancellationToken)
324  {
325  if(logger == null)
326  throw new ArgumentNullException(nameof(logger));
327  if (version == null)
328  throw new ArgumentNullException(nameof(version));
329  if (version < new Version(4, 0))
330  throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
331 
332  // Update this with new migrations as they are made
333  string targetMigration = null;
334 
335  if (currentDatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0))
336  throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!");
337 
338  if (version < new Version(4, 1, 0))
339  throw new NotSupportedException("Cannot migrate below version 4.1.0!");
340 
341  if(version < new Version(4, 4, 0))
342  switch (currentDatabaseType)
343  {
344  case DatabaseType.MariaDB:
345  case DatabaseType.MySql:
346  targetMigration = nameof(MYFixForeignKey);
347  break;
348  case DatabaseType.PostgresSql:
349  targetMigration = nameof(PGCreate);
350  break;
351  case DatabaseType.SqlServer:
352  case DatabaseType.Sqlite:
353  targetMigration = nameof(MSRemoveSoftColumns);
354  break;
355  default:
356  throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
357  }
358 
359  if (version < new Version(4, 2, 0))
360  targetMigration = currentDatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete);
361 
362  if (targetMigration == null)
363  {
364  logger.LogDebug("No down migration required.");
365  return;
366  }
367 
368  string migrationSubstitution;
369  switch (currentDatabaseType)
370  {
371  case DatabaseType.SqlServer:
372  // already setup
373  migrationSubstitution = null;
374  break;
375  case DatabaseType.MySql:
376  case DatabaseType.MariaDB:
377  migrationSubstitution = "MY{0}";
378  break;
379  case DatabaseType.Sqlite:
380  migrationSubstitution = "SL{0}";
381  break;
382  case DatabaseType.PostgresSql:
383  migrationSubstitution = "PG{0}";
384  break;
385  default:
386  throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}");
387  }
388 
389  if (migrationSubstitution != null)
390  targetMigration = String.Format(CultureInfo.InvariantCulture, migrationSubstitution, targetMigration.Substring(2));
391 
392  // even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
393  var dbServiceProvider = ((IInfrastructure<IServiceProvider>)Database).Instance;
394  var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
395 
396  logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
397  try
398  {
399  await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
400  }
401  catch (Exception e)
402  {
403  logger.LogCritical("Failed to migrate! Exception: {0}", e);
404  }
405  }
406  }
407 }
IDatabaseCollection< RevisionInformation > RevisionInformations
The RevisionInformations in the IDatabaseContext
IDatabaseCollection< InstanceUser > InstanceUsers
The InstanceUsers in the IDatabaseContext
DatabaseContext(DbContextOptions dbContextOptions)
Construct a DatabaseContext
IDatabaseCollection< ChatChannel > ChatChannels
The ChatChannel in the IDatabaseContext
Database representation of Components.Session.ReattachInformation
Fix the CompileJob/RevisionInformation foreign key for MySQL.
readonly IDatabaseCollection< ChatChannel > chatChannelsCollection
Backing field for IDatabaseContext.ChatChannels.
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
static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration)
Configure the SqlServerDatabaseContext.
IDatabaseCollection< RepositorySettings > RepositorySettings
The Models.RepositorySettings in the IDatabaseContext
readonly IDatabaseCollection< Instance > instancesCollection
Backing field for IDatabaseContext.Instances.
readonly IDatabaseCollection< DreamMakerSettings > dreamMakerSettingsCollection
Backing field for IDatabaseContext.DreamMakerSettings.
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
readonly IDatabaseCollection< CompileJob > compileJobsCollection
Backing field for IDatabaseContext.CompileJobs.
DatabaseType
Type of database to user
Definition: DatabaseType.cs:6
readonly IDatabaseCollection< InstanceUser > instanceUsersCollection
Backing field for IDatabaseContext.InstanceUsers.
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
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.
async Task SchemaDowngradeForServerVersion(ILogger< DatabaseContext > logger, Version version, DatabaseType currentDatabaseType, CancellationToken cancellationToken)
Attempt to downgrade the schema to the migration used for a given server version ...
async Task< bool > Migrate(ILogger< DatabaseContext > logger, CancellationToken cancellationToken)
Creates and migrates the IDatabaseContext