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