Some checks are pending
stage api зашёл в crash-loop после деплоя phase5c: DevDataSeeder упал с «column IsActive does not exist», потому что миграция Phase5c не была подхвачена db.Database.Migrate(). EF Core ищет миграции по [MigrationAttribute] на классе (или Designer-файле, который этот атрибут содержит). Без него миграция в сборке есть, но не известна runtime-механизму. Также чиню e2e: URL единиц был /api/catalog/units (404), правильный — /api/catalog/units-of-measure.
168 lines
7.8 KiB
C#
168 lines
7.8 KiB
C#
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||
using Microsoft.EntityFrameworkCore.Migrations;
|
||
using foodmarket.Infrastructure.Persistence;
|
||
|
||
#nullable disable
|
||
|
||
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||
{
|
||
/// <summary>Phase5c — рефакторинг справочника единиц измерения в глобальный.
|
||
/// До: каждая орга держала свои 5 копий («штука», «кг», ...). 95 строк
|
||
/// в БД на 19 орг — duplication, и редактирует их кто угодно.
|
||
/// После: 5 globals (OrganizationId=NULL), CRUD только у SuperAdmin.
|
||
/// Орга включает нужные единицы через junction org_units_of_measure.
|
||
///
|
||
/// Миграция данных:
|
||
/// 1. По одной строке на каждую (Code, Name) пару поднимается в global
|
||
/// (OrganizationId→NULL).
|
||
/// 2. Junction наполняется: каждая орга получает запись о включённости
|
||
/// того global'а, чью (Code, Name) она раньше держала локально.
|
||
/// 3. products.UnitOfMeasureId remap'ится с tenant-row на global.
|
||
/// 4. Оставшиеся tenant-rows (дубликаты) удаляются.
|
||
/// 5. Если в БД не было какого-то канонического (Code, Name), он
|
||
/// добавляется явно.
|
||
///
|
||
/// Безопасно для prod: products FK (OnDelete=Restrict) не падает
|
||
/// благодаря шагу 3 перед DELETE на шаге 4.</summary>
|
||
[DbContext(typeof(AppDbContext))]
|
||
[Migration("20260508000000_Phase5c_UnitsOfMeasureGlobal")]
|
||
public partial class Phase5c_UnitsOfMeasureGlobal : Migration
|
||
{
|
||
protected override void Up(MigrationBuilder b)
|
||
{
|
||
// 0. IsActive (default true) — для будущего soft-delete.
|
||
b.AddColumn<bool>(
|
||
name: "IsActive",
|
||
schema: "public",
|
||
table: "units_of_measure",
|
||
type: "boolean",
|
||
nullable: false,
|
||
defaultValue: true);
|
||
|
||
// 1. Старый unique index (OrganizationId, Code) — больше не нужен.
|
||
b.DropIndex(
|
||
name: "IX_units_of_measure_OrganizationId_Code",
|
||
schema: "public",
|
||
table: "units_of_measure");
|
||
|
||
// 2. Поднять одну строку на (Code, Name) пару в global.
|
||
b.Sql(@"
|
||
WITH first_per_pair AS (
|
||
SELECT DISTINCT ON (""Code"", ""Name"") ""Id""
|
||
FROM public.units_of_measure
|
||
WHERE ""OrganizationId"" IS NOT NULL
|
||
ORDER BY ""Code"", ""Name"", ""Id""
|
||
)
|
||
UPDATE public.units_of_measure
|
||
SET ""OrganizationId"" = NULL
|
||
WHERE ""Id"" IN (SELECT ""Id"" FROM first_per_pair);");
|
||
|
||
// 3. Создать junction org_units_of_measure.
|
||
b.CreateTable(
|
||
name: "org_units_of_measure",
|
||
schema: "public",
|
||
columns: table => new
|
||
{
|
||
OrganizationId = table.Column<System.Guid>(type: "uuid", nullable: false),
|
||
UnitOfMeasureId = table.Column<System.Guid>(type: "uuid", nullable: false),
|
||
},
|
||
constraints: table =>
|
||
{
|
||
table.PrimaryKey("PK_org_units_of_measure", x => new { x.OrganizationId, x.UnitOfMeasureId });
|
||
table.ForeignKey(
|
||
name: "FK_org_units_of_measure_units_of_measure_UnitOfMeasureId",
|
||
column: x => x.UnitOfMeasureId,
|
||
principalSchema: "public",
|
||
principalTable: "units_of_measure",
|
||
principalColumn: "Id",
|
||
onDelete: Microsoft.EntityFrameworkCore.Migrations.ReferentialAction.Restrict);
|
||
});
|
||
|
||
b.CreateIndex(
|
||
name: "IX_org_units_of_measure_UnitOfMeasureId",
|
||
schema: "public",
|
||
table: "org_units_of_measure",
|
||
column: "UnitOfMeasureId");
|
||
|
||
// 4. Заполнить junction: для каждой орги — её активные globals
|
||
// (через сравнение Code+Name).
|
||
b.Sql(@"
|
||
INSERT INTO public.org_units_of_measure (""OrganizationId"", ""UnitOfMeasureId"")
|
||
SELECT DISTINCT t.""OrganizationId"", g.""Id""
|
||
FROM public.units_of_measure t
|
||
JOIN public.units_of_measure g
|
||
ON g.""OrganizationId"" IS NULL
|
||
AND g.""Code"" = t.""Code""
|
||
AND g.""Name"" = t.""Name""
|
||
WHERE t.""OrganizationId"" IS NOT NULL
|
||
ON CONFLICT DO NOTHING;");
|
||
|
||
// 5. Remap products.UnitOfMeasureId с tenant-row на global.
|
||
b.Sql(@"
|
||
UPDATE public.products p
|
||
SET ""UnitOfMeasureId"" = g.""Id""
|
||
FROM public.units_of_measure t, public.units_of_measure g
|
||
WHERE p.""UnitOfMeasureId"" = t.""Id""
|
||
AND t.""OrganizationId"" IS NOT NULL
|
||
AND g.""OrganizationId"" IS NULL
|
||
AND g.""Code"" = t.""Code""
|
||
AND g.""Name"" = t.""Name"";");
|
||
|
||
// 6. Удалить tenant-row дубликаты (на globals никто уже не ссылается
|
||
// напрямую кроме junction и products — те remap'нуты выше).
|
||
b.Sql(@"DELETE FROM public.units_of_measure WHERE ""OrganizationId"" IS NOT NULL;");
|
||
|
||
// 7. Доинсёртить канонические globals, если каких-то не было в БД.
|
||
b.Sql(@"
|
||
INSERT INTO public.units_of_measure (""Id"", ""Code"", ""Name"", ""IsActive"")
|
||
SELECT gen_random_uuid(), v.code, v.name, true
|
||
FROM (VALUES
|
||
('796','штука'),
|
||
('166','килограмм'),
|
||
('112','литр'),
|
||
('006','метр'),
|
||
('625','упаковка')
|
||
) AS v(code, name)
|
||
WHERE NOT EXISTS (
|
||
SELECT 1 FROM public.units_of_measure
|
||
WHERE ""OrganizationId"" IS NULL AND ""Code"" = v.code
|
||
);");
|
||
|
||
// 8. Новый unique index на Code среди active globals.
|
||
b.CreateIndex(
|
||
name: "IX_units_of_measure_Code",
|
||
schema: "public",
|
||
table: "units_of_measure",
|
||
column: "Code",
|
||
unique: true,
|
||
filter: "\"IsActive\" = true");
|
||
}
|
||
|
||
protected override void Down(MigrationBuilder b)
|
||
{
|
||
b.DropIndex(
|
||
name: "IX_units_of_measure_Code",
|
||
schema: "public",
|
||
table: "units_of_measure");
|
||
|
||
b.DropTable(
|
||
name: "org_units_of_measure",
|
||
schema: "public");
|
||
|
||
b.DropColumn(
|
||
name: "IsActive",
|
||
schema: "public",
|
||
table: "units_of_measure");
|
||
|
||
// Восстановить старый unique index. Данные не возвращаем — это
|
||
// одностороння миграция (rollback вернёт лишь схему).
|
||
b.CreateIndex(
|
||
name: "IX_units_of_measure_OrganizationId_Code",
|
||
schema: "public",
|
||
table: "units_of_measure",
|
||
columns: new[] { "OrganizationId", "Code" },
|
||
unique: true);
|
||
}
|
||
}
|
||
}
|