mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
fix: add the new 2023-wire columns to the idempotent schema bootstrap
Adding entity columns and an EF migration was not enough, and the gap took down a live dev server: club home, matchmake/dorm, the presence heartbeat and the chat thread list all returned 500 with "column does not exist". The Postgres path is EnsureCreated-only and never replays migrations, so an EF migration adds nothing to a database that already exists. Every post-Initial column has to be repeated as an idempotent ALTER in DatabaseBootstrap — that is what the existing Ensure*Columns helpers are for — and the columns added by the 2023 wire work had no such helper. Observed failures, all from a running deployment: - PrivateInstances.RoomCode -> PrivateInstanceService.EnsureForDormAsync, which is on the path of BOTH matchmake/dorm and player/heartbeat. This is why the client could not load into the dorm at all. - Clubs.ClubChatEnabled / MinLevel -> ClubService.HomeClubAsync, so GET club/home/me 500'd on every boot. - ChatMessages.ModerationState -> ChatController.GetThreads, so the chat inbox 500'd. EnsureClient2023WireColumnsAsync covers all of them plus the playlist settings columns and the two new club tables, for Postgres and SQLite, and runs after the provider branch so both get it. Every statement is idempotent, so it is a no-op on a fresh database that EnsureCreated already built correctly. The test suite did not catch this because tests always run against a fresh database, where EnsureCreated builds the full model from the entities. Only an EXISTING database can be missing these columns.
This commit is contained in:
@@ -113,6 +113,7 @@ public static class DatabaseBootstrap
|
||||
await EnsureGiftPackageColumnsAsync(db);
|
||||
await EnsureAvatarColumnsAsync(db);
|
||||
await EnsureMarch2023TablesAsync(db);
|
||||
await EnsureClient2023WireColumnsAsync(db);
|
||||
|
||||
// Coach system account at Player.Id=1. The RR-Original room seeder
|
||||
// below assigns CreatorPlayerId=1 to every canonical room
|
||||
@@ -817,6 +818,134 @@ public static class DatabaseBootstrap
|
||||
await db.Database.ExecuteSqlRawAsync(sql);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>Schema introduced by the 2023-client wire-contract work:
|
||||
/// playlist settings the client can actually set, a host-chosen room code,
|
||||
/// per-role club permissions, the club image gallery, per-message chat
|
||||
/// moderation state, and QA comments on test cases.
|
||||
///
|
||||
/// This exists for the same reason as the other Ensure* helpers, and the
|
||||
/// reason is worth stating plainly because forgetting it is a live outage:
|
||||
/// the Postgres path is EnsureCreated-only and never replays migrations, so
|
||||
/// an EF migration alone adds nothing to an existing database. A handler
|
||||
/// that selects one of these columns then fails with "column does not
|
||||
/// exist" on every request — which is exactly how club home, matchmake/dorm
|
||||
/// and the presence heartbeat all started returning 500 at once.
|
||||
///
|
||||
/// Every statement is idempotent, so this is safe to run on a fresh
|
||||
/// database (where EnsureCreated already built everything) and on one that
|
||||
/// predates the change.</summary>
|
||||
private static async Task EnsureClient2023WireColumnsAsync(DorkNetDbContext db)
|
||||
{
|
||||
var provider = db.Database.ProviderName ?? string.Empty;
|
||||
if (provider.Contains("Npgsql", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var statements = new[]
|
||||
{
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""Accessibility"" integer NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""SupportsLevelVoting"" boolean NOT NULL DEFAULT false;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""SupportsJuniors"" boolean NOT NULL DEFAULT true;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""SupportsScreens"" boolean NOT NULL DEFAULT true;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""SupportsTeleportVR"" boolean NOT NULL DEFAULT true;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""SupportsWalkVR"" boolean NOT NULL DEFAULT true;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""WarningMask"" integer NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN IF NOT EXISTS ""CustomWarning"" character varying(512) NOT NULL DEFAULT '';",
|
||||
@"ALTER TABLE ""PrivateInstances"" ADD COLUMN IF NOT EXISTS ""RoomCode"" character varying(16) NOT NULL DEFAULT '';",
|
||||
@"ALTER TABLE ""Clubs"" ADD COLUMN IF NOT EXISTS ""MinLevel"" integer NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""Clubs"" ADD COLUMN IF NOT EXISTS ""ClubChatEnabled"" boolean NOT NULL DEFAULT true;",
|
||||
@"ALTER TABLE ""ChatMessages"" ADD COLUMN IF NOT EXISTS ""ModerationState"" integer NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""TestCases"" ADD COLUMN IF NOT EXISTS ""CommentsJson"" text NOT NULL DEFAULT '[]';",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "ClubRolePermissions" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"ClubId" bigint NOT NULL,
|
||||
"MembershipType" integer NOT NULL,
|
||||
"EditDetails" boolean NOT NULL DEFAULT false,
|
||||
"ApproveMember" boolean NOT NULL DEFAULT false,
|
||||
"CreateEvent" boolean NOT NULL DEFAULT false,
|
||||
"PostAnnouncement" boolean NOT NULL DEFAULT false,
|
||||
"EditPermissionSettings" boolean NOT NULL DEFAULT false,
|
||||
"BanUnban" boolean NOT NULL DEFAULT false,
|
||||
"UpdatedAt" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_ClubRolePermissions_ClubId_MembershipType"" ON ""ClubRolePermissions"" (""ClubId"", ""MembershipType"");",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "ClubAdditionalImages" (
|
||||
"Id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
"ClubId" bigint NOT NULL,
|
||||
"Slot" integer NOT NULL,
|
||||
"ImageName" character varying(256) NOT NULL DEFAULT '',
|
||||
"UpdatedAt" timestamp with time zone NOT NULL DEFAULT now()
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_ClubAdditionalImages_ClubId_Slot"" ON ""ClubAdditionalImages"" (""ClubId"", ""Slot"");",
|
||||
};
|
||||
foreach (var sql in statements)
|
||||
await db.Database.ExecuteSqlRawAsync(sql);
|
||||
return;
|
||||
}
|
||||
|
||||
// SQLite gets these from Database.Migrate(), but a database that was
|
||||
// baselined past the migration (BaselineExistingSchemaIfNeeded) would
|
||||
// skip them, so add the same columns defensively. SQLite has no
|
||||
// ADD COLUMN IF NOT EXISTS; the duplicate-column error is the success
|
||||
// case on a database that already has them.
|
||||
var sqlite = new[]
|
||||
{
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""Accessibility"" INTEGER NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""SupportsLevelVoting"" INTEGER NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""SupportsJuniors"" INTEGER NOT NULL DEFAULT 1;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""SupportsScreens"" INTEGER NOT NULL DEFAULT 1;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""SupportsTeleportVR"" INTEGER NOT NULL DEFAULT 1;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""SupportsWalkVR"" INTEGER NOT NULL DEFAULT 1;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""WarningMask"" INTEGER NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""Playlists"" ADD COLUMN ""CustomWarning"" TEXT NOT NULL DEFAULT '';",
|
||||
@"ALTER TABLE ""PrivateInstances"" ADD COLUMN ""RoomCode"" TEXT NOT NULL DEFAULT '';",
|
||||
@"ALTER TABLE ""Clubs"" ADD COLUMN ""MinLevel"" INTEGER NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""Clubs"" ADD COLUMN ""ClubChatEnabled"" INTEGER NOT NULL DEFAULT 1;",
|
||||
@"ALTER TABLE ""ChatMessages"" ADD COLUMN ""ModerationState"" INTEGER NOT NULL DEFAULT 0;",
|
||||
@"ALTER TABLE ""TestCases"" ADD COLUMN ""CommentsJson"" TEXT NOT NULL DEFAULT '[]';",
|
||||
};
|
||||
foreach (var sql in sqlite)
|
||||
{
|
||||
try { await db.Database.ExecuteSqlRawAsync(sql); } catch { }
|
||||
}
|
||||
|
||||
var sqliteTables = new[]
|
||||
{
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "ClubRolePermissions" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClubRolePermissions" PRIMARY KEY AUTOINCREMENT,
|
||||
"ClubId" INTEGER NOT NULL,
|
||||
"MembershipType" INTEGER NOT NULL,
|
||||
"EditDetails" INTEGER NOT NULL DEFAULT 0,
|
||||
"ApproveMember" INTEGER NOT NULL DEFAULT 0,
|
||||
"CreateEvent" INTEGER NOT NULL DEFAULT 0,
|
||||
"PostAnnouncement" INTEGER NOT NULL DEFAULT 0,
|
||||
"EditPermissionSettings" INTEGER NOT NULL DEFAULT 0,
|
||||
"BanUnban" INTEGER NOT NULL DEFAULT 0,
|
||||
"UpdatedAt" TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_ClubRolePermissions_ClubId_MembershipType"" ON ""ClubRolePermissions"" (""ClubId"", ""MembershipType"");",
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS "ClubAdditionalImages" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ClubAdditionalImages" PRIMARY KEY AUTOINCREMENT,
|
||||
"ClubId" INTEGER NOT NULL,
|
||||
"Slot" INTEGER NOT NULL,
|
||||
"ImageName" TEXT NOT NULL DEFAULT '',
|
||||
"UpdatedAt" TEXT NOT NULL DEFAULT ''
|
||||
);
|
||||
""",
|
||||
@"CREATE UNIQUE INDEX IF NOT EXISTS ""IX_ClubAdditionalImages_ClubId_Slot"" ON ""ClubAdditionalImages"" (""ClubId"", ""Slot"");",
|
||||
};
|
||||
foreach (var sql in sqliteTables)
|
||||
{
|
||||
try { await db.Database.ExecuteSqlRawAsync(sql); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task EnsureMarch2023TablesAsync(DorkNetDbContext db)
|
||||
{
|
||||
var provider = db.Database.ProviderName ?? string.Empty;
|
||||
|
||||
Reference in New Issue
Block a user