Fix database round start date issues (#26838)

How can ONE DATABASE COLUMN have so many cursed issues I don't know, but it certainly pissed off the devil in its previous life.

The start_date column on round entities in the database was added by https://github.com/space-wizards/space-station-14/pull/21153. For some reason, this PR gave the column a nonsensical default value instead of making it nullable. This default value causes the code from #25280 to break. It actually trips an assert though that's not what the original issue report ran into.

This didn't get noticed on wizden servers because we at some point backfilled the start_date column based on the stored admin logs.

So I change the database model to make this column nullable, updated the C# code to match, and made the existing migration set the invalid values to be NULL instead. Cool.

Wait how's SQLite handle in this scenario anyways? Well actually turns out the column was *completely broken* in the first place!

The code for inserting into the round table was copy pasted between SQLite and PostgreSQL, with the only difference being that the SQLite key manually assigned the primary key instead of letting SQLite AUTOINCREMENT it. And then the code to give a start_date value was only added to the PostgreSQL version (which is actually in the base class already). So for SQLite that column's been filled up with the same invalid default the whole time.

Why was the code manually assigning a PK? I checked the SQLite docs for AUTOINCREMENT[1], and the behavior seems appropriate.

I removed the SQLite-specific code path and it just seems to work regardless. The migration just sets the old values to NULL too.

BUT WAIT, THERE'S MORE!

Turns out just doing the migration on SQLite is a pain in the ass! EF Core has to create a new table to apply the nullability change, because SQLite doesn't support proper ALTER COLUMN. This causes the generated SQL commands to be weird and the UPDATE for the migration goes BEFORE the nullability change... I ended up having to make TWO migrations for SQLite. Yay.

Fixes #26800

[1]: https://www.sqlite.org/autoinc.html
This commit is contained in:
Pieter-Jan Briers
2024-04-14 07:39:43 +02:00
committed by GitHub
parent dbf8a036ec
commit d3ac3d06bb
12 changed files with 5268 additions and 41 deletions

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class FixRoundStartDateNullability : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldDefaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.Sql("UPDATE round SET start_date = NULL WHERE start_date = '-Infinity';");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
}
}
}

View File

@@ -845,10 +845,8 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("integer")
.HasColumnName("server_id");
b.Property<DateTime>("StartDate")
.ValueGeneratedOnAdd()
b.Property<DateTime?>("StartDate")
.HasColumnType("timestamp with time zone")
.HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.HasColumnName("start_date");
b.HasKey("Id")

View File

@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class FixRoundStartDateNullability : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "TEXT",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "TEXT",
oldDefaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "TEXT",
oldNullable: true);
}
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class FixRoundStartDateNullability2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// This needs to be its own separate migration,
// because EF Core re-arranges the order of the commands if it's a single migration...
// (only relevant for SQLite since it needs cursed shit to do ALTER COLUMN)
migrationBuilder.Sql("UPDATE round SET start_date = NULL WHERE start_date = '0001-01-01 00:00:00';");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@@ -796,10 +796,8 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("server_id");
b.Property<DateTime>("StartDate")
.ValueGeneratedOnAdd()
b.Property<DateTime?>("StartDate")
.HasColumnType("TEXT")
.HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.HasColumnName("start_date");
b.HasKey("Id")

View File

@@ -118,10 +118,6 @@ namespace Content.Server.Database
modelBuilder.Entity<Round>()
.HasIndex(round => round.StartDate);
modelBuilder.Entity<Round>()
.Property(round => round.StartDate)
.HasDefaultValue(default(DateTime));
modelBuilder.Entity<AdminLogPlayer>()
.HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId});
@@ -494,7 +490,7 @@ namespace Content.Server.Database
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime? StartDate { get; set; }
public List<Player> Players { get; set; } = default!;

View File

@@ -123,6 +123,6 @@ public sealed record PlayerRecord(
IPAddress LastSeenAddress,
ImmutableArray<byte>? HWId);
public sealed record RoundRecord(int Id, DateTimeOffset StartDate, ServerRecord Server);
public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server);
public sealed record ServerRecord(int Id, string Name);

View File

@@ -696,7 +696,7 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync(cancel);
}
public virtual async Task<int> AddNewRound(Server server, params Guid[] playerIds)
public async Task<int> AddNewRound(Server server, params Guid[] playerIds)
{
await using var db = await GetDb();

View File

@@ -452,34 +452,6 @@ namespace Content.Server.Database
return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!;
}
public override async Task<int> AddNewRound(Server server, params Guid[] playerIds)
{
await using var db = await GetDb();
var players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.ToListAsync();
var nextId = 1;
if (await db.DbContext.Round.AnyAsync())
{
nextId = db.DbContext.Round.Max(round => round.Id) + 1;
}
var round = new Round
{
Id = nextId,
Players = players,
ServerId = server.Id
};
db.DbContext.Round.Add(round);
await db.DbContext.SaveChangesAsync();
return round.Id;
}
protected override IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null)
{
IQueryable<AdminLog> query = db.AdminLog;