food-market/src/food-market.infrastructure/Persistence/Migrations/20260508000000_Phase5c_UnitsOfMeasureGlobal.cs
nns ee127b2785
Some checks are pending
CI / POS (WPF, Windows) (push) Waiting to run
CI / Backend (.NET 8) (push) Successful in 55s
CI / Web (React + Vite) (push) Successful in 42s
Docker API / Build + push API (push) Successful in 1m17s
Docker API / Deploy API on stage (push) Successful in 18s
fix(migrations): добавить [Migration] атрибут для Phase5c — без него Migrate() не находит миграцию
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.
2026-05-08 01:29:51 +05:00

168 lines
7.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
}