Make AddClipAdminMenuVisible migration idempotent

Rewrites DDL to use IF NOT EXISTS / DROP IF EXISTS and DROP COLUMN IF EXISTS
so the migration can be re-run safely against a DB that already has the schema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
AzuTear
2026-06-29 09:07:33 +02:00
parent b53c7fb736
commit 1ab26ec2fd
@@ -12,111 +12,89 @@ namespace Backend.Migrations
/// <inheritdoc /> /// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.AddColumn<bool>( migrationBuilder.Sql("""
name: "ClipAdminMenuVisible", ALTER TABLE "SiteSettings"
table: "SiteSettings", ADD COLUMN IF NOT EXISTS "ClipAdminMenuVisible" boolean NOT NULL DEFAULT FALSE;
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>( ALTER TABLE "SiteSettings"
name: "ShowactApplicationDisabledMessage", ADD COLUMN IF NOT EXISTS "ShowactApplicationDisabledMessage" character varying(240) NOT NULL DEFAULT '';
table: "SiteSettings",
type: "character varying(240)",
maxLength: 240,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<bool>( ALTER TABLE "SiteSettings"
name: "ShowactApplicationsEnabled", ADD COLUMN IF NOT EXISTS "ShowactApplicationsEnabled" boolean NOT NULL DEFAULT FALSE;
table: "SiteSettings",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>( ALTER TABLE "SiteSettings"
name: "ShowactsContent", ADD COLUMN IF NOT EXISTS "ShowactsContent" text NOT NULL DEFAULT '';
table: "SiteSettings",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>( ALTER TABLE "SiteSettings"
name: "ShowactsUrl", ADD COLUMN IF NOT EXISTS "ShowactsUrl" character varying(400) NOT NULL DEFAULT '';
table: "SiteSettings",
type: "character varying(400)",
maxLength: 400,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<bool>( ALTER TABLE "SiteSettings"
name: "SponsorsVisible", ADD COLUMN IF NOT EXISTS "SponsorsVisible" boolean NOT NULL DEFAULT TRUE;
table: "SiteSettings", """);
type: "boolean",
nullable: false,
defaultValue: true);
migrationBuilder.CreateTable( migrationBuilder.Sql("""
name: "ShowactApplications", CREATE TABLE IF NOT EXISTS "ShowactApplications" (
columns: table => new "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
{ "SeasonId" integer NOT NULL,
Id = table.Column<int>(type: "integer", nullable: false) "ArtistName" character varying(120) NOT NULL,
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), "ContactEmail" character varying(180) NOT NULL,
SeasonId = table.Column<int>(type: "integer", nullable: false), "ContactDiscord" character varying(120) NOT NULL,
ArtistName = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false), "PlatformUrl" character varying(500) NOT NULL,
ContactEmail = table.Column<string>(type: "character varying(180)", maxLength: 180, nullable: false), "PerformanceType" character varying(80) NOT NULL,
ContactDiscord = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false), "Description" character varying(1000) NOT NULL,
PlatformUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false), "TechnicalNotes" character varying(1000) NOT NULL,
PerformanceType = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false), "ReferenceUrl" character varying(500) NOT NULL,
Description = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: false), "Status" character varying(20) NOT NULL,
TechnicalNotes = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: false), "ReviewNote" character varying(500) NULL,
ReferenceUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false), "ReviewedByTwitchId" character varying(120) NULL,
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false), "CreatedFromIp" character varying(80) NOT NULL,
ReviewNote = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true), "UserAgent" character varying(400) NOT NULL,
ReviewedByTwitchId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true), "CreatedAt" timestamp with time zone NOT NULL,
CreatedFromIp = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false), "ReviewedAt" timestamp with time zone NULL
UserAgent = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false), );
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
ReviewedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ShowactApplications", x => x.Id);
table.ForeignKey(
name: "FK_ShowactApplications_Seasons_SeasonId",
column: x => x.SeasonId,
principalTable: "Seasons",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( CREATE TABLE IF NOT EXISTS "Sponsors" (
name: "Sponsors", "Id" integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
columns: table => new "SeasonId" integer NOT NULL,
{ "Name" character varying(120) NOT NULL,
Id = table.Column<int>(type: "integer", nullable: false) "WebsiteUrl" character varying(500) NOT NULL,
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), "LogoUrl" character varying(500) NOT NULL,
SeasonId = table.Column<int>(type: "integer", nullable: false), "Description" character varying(500) NOT NULL,
Name = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false), "Tier" character varying(80) NOT NULL,
WebsiteUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false), "SortOrder" integer NOT NULL,
LogoUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false), "IsVisible" boolean NOT NULL,
Description = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false), "CreatedAt" timestamp with time zone NOT NULL,
Tier = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false), "UpdatedAt" timestamp with time zone NULL
SortOrder = table.Column<int>(type: "integer", nullable: false), );
IsVisible = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false), DO $$
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true) BEGIN
}, IF NOT EXISTS (
constraints: table => SELECT 1
{ FROM pg_constraint
table.PrimaryKey("PK_Sponsors", x => x.Id); WHERE conname = 'FK_ShowactApplications_Seasons_SeasonId'
table.ForeignKey( ) THEN
name: "FK_Sponsors_Seasons_SeasonId", ALTER TABLE "ShowactApplications"
column: x => x.SeasonId, ADD CONSTRAINT "FK_ShowactApplications_Seasons_SeasonId"
principalTable: "Seasons", FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
principalColumn: "Id", ON DELETE CASCADE;
onDelete: ReferentialAction.Cascade); END IF;
}); END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'FK_Sponsors_Seasons_SeasonId'
) THEN
ALTER TABLE "Sponsors"
ADD CONSTRAINT "FK_Sponsors_Seasons_SeasonId"
FOREIGN KEY ("SeasonId") REFERENCES "Seasons" ("Id")
ON DELETE CASCADE;
END IF;
END $$;
""");
migrationBuilder.UpdateData( migrationBuilder.UpdateData(
table: "SiteSettings", table: "SiteSettings",
@@ -125,49 +103,43 @@ namespace Backend.Migrations
columns: new[] { "ClipAdminMenuVisible", "ShowactApplicationDisabledMessage", "ShowactsContent", "ShowactsUrl", "SponsorsVisible", "WorkflowRulesJson" }, columns: new[] { "ClipAdminMenuVisible", "ShowactApplicationDisabledMessage", "ShowactsContent", "ShowactsUrl", "SponsorsVisible", "WorkflowRulesJson" },
values: new object[] { true, "Showact-Bewerbungen sind aktuell geschlossen.", "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", "https://vtuber-star-awards.de/showacts", true, "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" }); values: new object[] { true, "Showact-Bewerbungen sind aktuell geschlossen.", "Informationen zu Showact-Bewerbungen, Ablauf und Kontakt fuer die Award-Show.", "https://vtuber-star-awards.de/showacts", true, "[{\"key\":\"max_finalists_per_category\",\"label\":\"Finale Kandidat:innen pro Kategorie\",\"enabled\":true,\"limit\":4,\"mode\":\"block\",\"description\":\"Begrenzt, wie viele finale Kandidat:innen in einer Kategorie vorbereitet werden.\"},{\"key\":\"max_candidate_appearances\",\"label\":\"Kandidaturen pro Person\",\"enabled\":true,\"limit\":2,\"mode\":\"warn\",\"description\":\"Warnt oder blockiert, wenn dieselbe Person in zu vielen Kategorien final auftaucht.\"},{\"key\":\"max_winner_placements\",\"label\":\"Gewinnerpl\\u00E4tze pro Person\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn dieselbe Person mehrfach als Gewinner:in gesetzt wird.\"},{\"key\":\"winner_requires_clip\",\"label\":\"Gewinner braucht Clip-Link\",\"enabled\":true,\"limit\":1,\"mode\":\"block\",\"description\":\"Verhindert oder warnt, wenn ein Gewinner ohne gepflegten YouTube-/Twitch-Clip gesetzt wird.\"}]" });
migrationBuilder.CreateIndex( migrationBuilder.Sql("""
name: "IX_ShowactApplications_SeasonId_Status", CREATE INDEX IF NOT EXISTS "IX_ShowactApplications_SeasonId_Status"
table: "ShowactApplications", ON "ShowactApplications" ("SeasonId", "Status");
columns: new[] { "SeasonId", "Status" });
migrationBuilder.CreateIndex( CREATE INDEX IF NOT EXISTS "IX_Sponsors_SeasonId_IsVisible_SortOrder"
name: "IX_Sponsors_SeasonId_IsVisible_SortOrder", ON "Sponsors" ("SeasonId", "IsVisible", "SortOrder");
table: "Sponsors", """);
columns: new[] { "SeasonId", "IsVisible", "SortOrder" });
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)
{ {
migrationBuilder.DropTable( migrationBuilder.Sql("""
name: "ShowactApplications"); DROP TABLE IF EXISTS "ShowactApplications";
migrationBuilder.DropTable( DROP TABLE IF EXISTS "Sponsors";
name: "Sponsors"); """);
migrationBuilder.DropColumn( migrationBuilder.Sql("""
name: "ClipAdminMenuVisible", ALTER TABLE "SiteSettings"
table: "SiteSettings"); DROP COLUMN IF EXISTS "ClipAdminMenuVisible";
migrationBuilder.DropColumn( ALTER TABLE "SiteSettings"
name: "ShowactApplicationDisabledMessage", DROP COLUMN IF EXISTS "ShowactApplicationDisabledMessage";
table: "SiteSettings");
migrationBuilder.DropColumn( ALTER TABLE "SiteSettings"
name: "ShowactApplicationsEnabled", DROP COLUMN IF EXISTS "ShowactApplicationsEnabled";
table: "SiteSettings");
migrationBuilder.DropColumn( ALTER TABLE "SiteSettings"
name: "ShowactsContent", DROP COLUMN IF EXISTS "ShowactsContent";
table: "SiteSettings");
migrationBuilder.DropColumn( ALTER TABLE "SiteSettings"
name: "ShowactsUrl", DROP COLUMN IF EXISTS "ShowactsUrl";
table: "SiteSettings");
migrationBuilder.DropColumn( ALTER TABLE "SiteSettings"
name: "SponsorsVisible", DROP COLUMN IF EXISTS "SponsorsVisible";
table: "SiteSettings"); """);
migrationBuilder.UpdateData( migrationBuilder.UpdateData(
table: "SiteSettings", table: "SiteSettings",