using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using foodmarket.Infrastructure.Persistence; #nullable disable namespace foodmarket.Infrastructure.Persistence.Migrations { /// 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. [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( 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(type: "uuid", nullable: false), UnitOfMeasureId = table.Column(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); } } }