fix(migrations): чиним P0-блокеры разворачивания на чистой БД
Проблема: на свежей PostgreSQL `dotnet ef database update` падает на пяти миграциях подряд + рантайм-несовместимость схемы с domain Product/Store/ Counterparty. Невозможно поднять стек ни на dev, ни на новом стейдже. Найдено и починено: 1. Phase2c4_ReconcileStage пыталась AddColumn IsMarked, который Phase1Catalog (после рефакторинга) уже добавляет. Завернули в IF NOT EXISTS. 2. Phase5d_ProductVatDecimal ALTER COLUMN products.Vat падал — Vat теперь заменён на FK VatRateId, колонки нет. Завернули в IF EXISTS. 3. Phase5c_UnitsOfMeasureGlobal INSERT канонических ОКЕИ пропускал NOT NULL колонку Symbol (а также DecimalPlaces, IsBase, CreatedAt). Дополнили полным набором: шт/кг/л/м/уп. 4. Phase5d_DropUnitOfMeasureDescription дропала несуществующую колонку (Description в новой схеме отсутствует). Завернули в IF EXISTS. 5. Phase5a_EmployeeSoftDelete и Phase5b_PlatformSettings были написаны вручную без атрибутов [Migration] + [DbContext] — EF их игнорировал и пропускал применение (см. memory/feedback_ef_migrations.md). Добавили атрибуты + сделали идемпотентными. 6. Новая Phase5f_DropStoreKindRudiment: rudimentные колонки stores.Kind и counterparties.Kind (NOT NULL без default'а) роняли любой INSERT — ни одной организации/контрагента создать нельзя. Дропаем. 7. Новая Phase5g_ProductVatRealign: приводим products в соответствие с domain — дропаем FK→vat_rates + колонку VatRateId + IsAlcohol + пустую таблицу vat_rates; добавляем products.Vat numeric(5,2) DEFAULT 12 и VatEnabled bool DEFAULT true. Без этого ProductsController падает 42703 при создании любого товара. Все миграции идемпотентны (DO $$ ... IF EXISTS/NOT EXISTS ...) — повторное применение на старой стейдж-БД безопасно. Проверено: E2E full-cycle на свежей dev-БД проходит 12/12 шагов. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
35d70c5d80
commit
a06464baeb
|
|
@ -23,21 +23,22 @@ public partial class Phase2c4_ReconcileStage : Migration
|
|||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// 1. Добавляем IsMarked с дефолтом false.
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "IsMarked",
|
||||
schema: "public",
|
||||
table: "products",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
// 2. Если TrackingType есть в БД (стейдж) — бэкфиллим и удаляем.
|
||||
// На свежей БД (dev, где migrations 2c2/2c3 не применялись отдельно)
|
||||
// колонки не будет — IF EXISTS защищает от ошибки.
|
||||
// Полностью идемпотентно: на стейдже (старый Phase1Catalog без IsMarked,
|
||||
// но с TrackingType) колонку добавим и бэкфилл'нем. На свежей БД, где
|
||||
// отрефакторённый Phase1Catalog уже создал IsMarked, — пропускаем.
|
||||
// Без этого защитного блока миграция падает с
|
||||
// `column "IsMarked" of relation "products" already exists` при первом
|
||||
// dotnet ef database update на пустой БД.
|
||||
migrationBuilder.Sql("""
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'products'
|
||||
AND column_name = 'IsMarked') THEN
|
||||
ALTER TABLE public.products
|
||||
ADD COLUMN "IsMarked" boolean NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public' AND table_name = 'products'
|
||||
AND column_name = 'TrackingType') THEN
|
||||
|
|
|
|||
|
|
@ -11,17 +11,35 @@ public partial class Phase5d_ProductVatDecimal : Migration
|
|||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
// Идемпотентно: на стейдже Phase1Catalog создавал products.Vat integer,
|
||||
// а на свежей БД отрефакторённый Phase1Catalog уже создаёт products.VatRateId
|
||||
// (FK на vat_rates) и колонки Vat нет. Без NOT EXISTS-гарда миграция падает
|
||||
// с "column Vat does not exist" при первом dotnet ef database update.
|
||||
b.Sql("""
|
||||
ALTER TABLE public.products
|
||||
ALTER COLUMN "Vat" TYPE numeric(5,2) USING "Vat"::numeric(5,2);
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='products'
|
||||
AND column_name='Vat') THEN
|
||||
ALTER TABLE public.products
|
||||
ALTER COLUMN "Vat" TYPE numeric(5,2) USING "Vat"::numeric(5,2);
|
||||
END IF;
|
||||
END $$;
|
||||
""");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.Sql("""
|
||||
ALTER TABLE public.products
|
||||
ALTER COLUMN "Vat" TYPE integer USING ROUND("Vat")::integer;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='products'
|
||||
AND column_name='Vat') THEN
|
||||
ALTER TABLE public.products
|
||||
ALTER COLUMN "Vat" TYPE integer USING ROUND("Vat")::integer;
|
||||
END IF;
|
||||
END $$;
|
||||
""");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
|
@ -10,37 +12,42 @@ namespace foodmarket.Infrastructure.Persistence.Migrations
|
|||
/// IsActive=false + FiredAt — уволен
|
||||
/// IsActive=false + IsDeleted=true + DeletedAt — soft-deleted
|
||||
/// Физически Employee никогда не удаляем (FK из retail_sales, supplies).</summary>
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260506000000_Phase5a_EmployeeSoftDelete")]
|
||||
public partial class Phase5a_EmployeeSoftDelete : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.AddColumn<bool>(
|
||||
name: "IsDeleted",
|
||||
schema: "public",
|
||||
table: "employees",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
// Идемпотентно (см. CLAUDE memory feedback_ef_migrations): на чистой
|
||||
// dev-БД нужен AddColumn, на стейдже миграция могла быть применена
|
||||
// вручную через SQL — повторный AddColumn упадёт.
|
||||
b.Sql(@"
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='employees'
|
||||
AND column_name='IsDeleted') THEN
|
||||
ALTER TABLE public.employees
|
||||
ADD COLUMN ""IsDeleted"" boolean NOT NULL DEFAULT false;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='employees'
|
||||
AND column_name='DeletedAt') THEN
|
||||
ALTER TABLE public.employees
|
||||
ADD COLUMN ""DeletedAt"" timestamp with time zone NULL;
|
||||
END IF;
|
||||
END $$;");
|
||||
|
||||
b.AddColumn<System.DateTime>(
|
||||
name: "DeletedAt",
|
||||
schema: "public",
|
||||
table: "employees",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
|
||||
b.CreateIndex(
|
||||
name: "IX_employees_OrganizationId_IsDeleted",
|
||||
schema: "public",
|
||||
table: "employees",
|
||||
columns: new[] { "OrganizationId", "IsDeleted" });
|
||||
b.Sql(@"
|
||||
CREATE INDEX IF NOT EXISTS ""IX_employees_OrganizationId_IsDeleted""
|
||||
ON public.employees (""OrganizationId"", ""IsDeleted"");");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.DropIndex(name: "IX_employees_OrganizationId_IsDeleted", schema: "public", table: "employees");
|
||||
b.DropColumn(name: "IsDeleted", schema: "public", table: "employees");
|
||||
b.DropColumn(name: "DeletedAt", schema: "public", table: "employees");
|
||||
b.Sql(@"DROP INDEX IF EXISTS public.""IX_employees_OrganizationId_IsDeleted"";");
|
||||
b.Sql(@"ALTER TABLE public.employees DROP COLUMN IF EXISTS ""IsDeleted"";");
|
||||
b.Sql(@"ALTER TABLE public.employees DROP COLUMN IF EXISTS ""DeletedAt"";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
|
|
@ -7,33 +9,33 @@ namespace foodmarket.Infrastructure.Persistence.Migrations
|
|||
/// <summary>Платформенные настройки (singleton). SMTP-креды для отправки
|
||||
/// писем (forgot-password, нотификации). Управляется SuperAdmin'ом через
|
||||
/// /super-admin/platform-settings. Видна только им.</summary>
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260506100000_Phase5b_PlatformSettings")]
|
||||
public partial class Phase5b_PlatformSettings : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.CreateTable(
|
||||
name: "platform_settings",
|
||||
schema: "public",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<System.Guid>(type: "uuid", nullable: false),
|
||||
SmtpHost = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
SmtpPort = table.Column<int>(type: "integer", nullable: true),
|
||||
SmtpUseSsl = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||
SmtpStartTls = table.Column<bool>(type: "boolean", nullable: false, defaultValue: true),
|
||||
SmtpUsername = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
SmtpPasswordEncrypted = table.Column<string>(type: "text", nullable: true),
|
||||
FromEmail = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
FromName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
CreatedAt = table.Column<System.DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<System.DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
},
|
||||
constraints: table => table.PrimaryKey("PK_platform_settings", x => x.Id));
|
||||
// Идемпотентно — миграция могла быть применена ранее SQL-ом, либо
|
||||
// соседняя миграция могла создать таблицу под тем же именем.
|
||||
b.Sql(@"
|
||||
CREATE TABLE IF NOT EXISTS public.platform_settings (
|
||||
""Id"" uuid PRIMARY KEY,
|
||||
""SmtpHost"" varchar(200) NULL,
|
||||
""SmtpPort"" integer NULL,
|
||||
""SmtpUseSsl"" boolean NOT NULL DEFAULT false,
|
||||
""SmtpStartTls"" boolean NOT NULL DEFAULT true,
|
||||
""SmtpUsername"" varchar(200) NULL,
|
||||
""SmtpPasswordEncrypted"" text NULL,
|
||||
""FromEmail"" varchar(200) NULL,
|
||||
""FromName"" varchar(200) NULL,
|
||||
""CreatedAt"" timestamp with time zone NOT NULL,
|
||||
""UpdatedAt"" timestamp with time zone NULL
|
||||
);");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.DropTable(name: "platform_settings", schema: "public");
|
||||
b.Sql(@"DROP TABLE IF EXISTS public.platform_settings;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,16 +113,23 @@ UPDATE public.units_of_measure
|
|||
b.Sql(@"DELETE FROM public.units_of_measure WHERE ""OrganizationId"" IS NOT NULL;");
|
||||
|
||||
// 7. Доинсёртить канонические globals, если каких-то не было в БД.
|
||||
// Передаём ВСЕ NOT NULL поля схемы units_of_measure: Symbol, Name,
|
||||
// DecimalPlaces, IsBase, CreatedAt — иначе INSERT падает 23502
|
||||
// («null value in column "Symbol" violates not-null constraint»)
|
||||
// на свежей БД, где units_of_measure был пустой и до Phase5c
|
||||
// единицы не успели заinsert'иться.
|
||||
b.Sql(@"
|
||||
INSERT INTO public.units_of_measure (""Id"", ""Code"", ""Name"", ""IsActive"")
|
||||
SELECT gen_random_uuid(), v.code, v.name, true
|
||||
INSERT INTO public.units_of_measure
|
||||
(""Id"", ""Code"", ""Name"", ""Symbol"", ""DecimalPlaces"", ""IsBase"", ""IsActive"", ""CreatedAt"")
|
||||
SELECT gen_random_uuid(), v.code, v.name, v.symbol,
|
||||
v.decimals, false, true, now() AT TIME ZONE 'UTC'
|
||||
FROM (VALUES
|
||||
('796','штука'),
|
||||
('166','килограмм'),
|
||||
('112','литр'),
|
||||
('006','метр'),
|
||||
('625','упаковка')
|
||||
) AS v(code, name)
|
||||
('796','штука', 'шт', 0),
|
||||
('166','килограмм', 'кг', 3),
|
||||
('112','литр', 'л', 3),
|
||||
('006','метр', 'м', 2),
|
||||
('625','упаковка', 'уп', 0)
|
||||
) AS v(code, name, symbol, decimals)
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM public.units_of_measure
|
||||
WHERE ""OrganizationId"" IS NULL AND ""Code"" = v.code
|
||||
|
|
|
|||
|
|
@ -17,21 +17,32 @@ public partial class Phase5d_DropUnitOfMeasureDescription : Migration
|
|||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.DropColumn(
|
||||
name: "Description",
|
||||
schema: "public",
|
||||
table: "units_of_measure");
|
||||
// Идемпотентно: на старой стейдж-БД колонка Description была, на свежей
|
||||
// (после рефакторинга Phase1Catalog) её сразу нет — без IF EXISTS
|
||||
// миграция падает с 42703 «column Description does not exist».
|
||||
b.Sql(@"
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='units_of_measure'
|
||||
AND column_name='Description') THEN
|
||||
ALTER TABLE public.units_of_measure DROP COLUMN ""Description"";
|
||||
END IF;
|
||||
END $$;");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.AddColumn<string>(
|
||||
name: "Description",
|
||||
schema: "public",
|
||||
table: "units_of_measure",
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true);
|
||||
b.Sql(@"
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='units_of_measure'
|
||||
AND column_name='Description') THEN
|
||||
ALTER TABLE public.units_of_measure
|
||||
ADD COLUMN ""Description"" varchar(500) NULL;
|
||||
END IF;
|
||||
END $$;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <summary>Phase5f — выкидываем рудиментные колонки Kind из stores и
|
||||
/// counterparties.
|
||||
///
|
||||
/// История: ранние черновики домена различали тип объекта через целочисленное
|
||||
/// поле Kind. Позже модель упростили — для Store отдельной сущностью стал
|
||||
/// RetailPoint; для Counterparty — отдельный enum CounterpartyType (поле
|
||||
/// Type, а не Kind). Свойства Kind ушли из domain и configurations, но
|
||||
/// миграции Phase1Catalog продолжали создавать колонки как NOT NULL integer
|
||||
/// без default'а.
|
||||
///
|
||||
/// Симптом: при любом INSERT в stores или counterparties (seeder,
|
||||
/// signup-bootstrap, контроллер) Postgres падает 23502 «null value in
|
||||
/// column "Kind"». На чистой dev-БД невозможно зарегистрировать организацию
|
||||
/// и создать ни одного контрагента.
|
||||
///
|
||||
/// Фикс: дропаем колонки идемпотентно (стейдж уже мог быть руками
|
||||
/// почищен).</summary>
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260523120000_Phase5f_DropStoreKindRudiment")]
|
||||
public partial class Phase5f_DropStoreKindRudiment : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.Sql(@"
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='stores'
|
||||
AND column_name='Kind') THEN
|
||||
ALTER TABLE public.stores DROP COLUMN ""Kind"";
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='counterparties'
|
||||
AND column_name='Kind') THEN
|
||||
ALTER TABLE public.counterparties DROP COLUMN ""Kind"";
|
||||
END IF;
|
||||
END $$;");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.Sql(@"
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='stores'
|
||||
AND column_name='Kind') THEN
|
||||
ALTER TABLE public.stores
|
||||
ADD COLUMN ""Kind"" integer NOT NULL DEFAULT 0;
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='counterparties'
|
||||
AND column_name='Kind') THEN
|
||||
ALTER TABLE public.counterparties
|
||||
ADD COLUMN ""Kind"" integer NOT NULL DEFAULT 0;
|
||||
END IF;
|
||||
END $$;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <summary>Phase5g — приводим схему products в соответствие с текущим domain
|
||||
/// и кодом контроллеров.
|
||||
///
|
||||
/// Расхождение: в БД остались рудименты от старой попытки вынести VAT в
|
||||
/// отдельную таблицу <c>vat_rates</c> (FK products.VatRateId → vat_rates.Id),
|
||||
/// а также legacy-флаг IsAlcohol. Domain Product (см. food-market.domain/
|
||||
/// Catalog/Product.cs) теперь хранит ставку напрямую как <c>decimal Vat</c>
|
||||
/// + <c>bool VatEnabled</c>, vat_rates никем не используется. Любой INSERT
|
||||
/// в products падает 42703 «column "Vat" of relation "products" does not
|
||||
/// exist», что блокирует создание товаров и весь dataflow приёмка→продажа.
|
||||
///
|
||||
/// Миграция:
|
||||
/// 1. Дропаем FK products.VatRateId → vat_rates.
|
||||
/// 2. Дропаем колонку products.VatRateId.
|
||||
/// 3. Дропаем рудиментную колонку products.IsAlcohol.
|
||||
/// 4. Дропаем пустую таблицу vat_rates.
|
||||
/// 5. Добавляем products.Vat numeric(5,2) NOT NULL DEFAULT 12
|
||||
/// (12% — ставка НДС по умолчанию для KZ; конкретный товар может быть 0%).
|
||||
/// 6. Добавляем products.VatEnabled boolean NOT NULL DEFAULT true.
|
||||
///
|
||||
/// Все шаги идемпотентны (на стейдже могло быть применено вручную).</summary>
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260523130000_Phase5g_ProductVatRealign")]
|
||||
public partial class Phase5g_ProductVatRealign : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.Sql(@"
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 1. FK products.VatRateId -> vat_rates
|
||||
IF EXISTS (SELECT 1 FROM information_schema.table_constraints
|
||||
WHERE table_schema='public' AND table_name='products'
|
||||
AND constraint_name='FK_products_vat_rates_VatRateId') THEN
|
||||
ALTER TABLE public.products DROP CONSTRAINT ""FK_products_vat_rates_VatRateId"";
|
||||
END IF;
|
||||
|
||||
-- 2. products.VatRateId column
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='products'
|
||||
AND column_name='VatRateId') THEN
|
||||
DROP INDEX IF EXISTS public.""IX_products_VatRateId"";
|
||||
ALTER TABLE public.products DROP COLUMN ""VatRateId"";
|
||||
END IF;
|
||||
|
||||
-- 3. products.IsAlcohol rudiment
|
||||
IF EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='products'
|
||||
AND column_name='IsAlcohol') THEN
|
||||
ALTER TABLE public.products DROP COLUMN ""IsAlcohol"";
|
||||
END IF;
|
||||
|
||||
-- 4. vat_rates table
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema='public' AND table_name='vat_rates') THEN
|
||||
DROP TABLE public.vat_rates CASCADE;
|
||||
END IF;
|
||||
|
||||
-- 5. products.Vat numeric(5,2)
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='products'
|
||||
AND column_name='Vat') THEN
|
||||
ALTER TABLE public.products
|
||||
ADD COLUMN ""Vat"" numeric(5,2) NOT NULL DEFAULT 12;
|
||||
END IF;
|
||||
|
||||
-- 6. products.VatEnabled boolean
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema='public' AND table_name='products'
|
||||
AND column_name='VatEnabled') THEN
|
||||
ALTER TABLE public.products
|
||||
ADD COLUMN ""VatEnabled"" boolean NOT NULL DEFAULT true;
|
||||
END IF;
|
||||
END $$;");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
// Без vat_rates восстановить семантически невозможно — это деструктивная
|
||||
// миграция, откат вернёт только структуру.
|
||||
b.Sql(@"
|
||||
CREATE TABLE IF NOT EXISTS public.vat_rates (
|
||||
""Id"" uuid PRIMARY KEY,
|
||||
""Code"" varchar(10) NOT NULL,
|
||||
""Rate"" numeric(5,2) NOT NULL,
|
||||
""CreatedAt"" timestamp with time zone NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE public.products
|
||||
DROP COLUMN IF EXISTS ""VatEnabled"",
|
||||
DROP COLUMN IF EXISTS ""Vat"",
|
||||
ADD COLUMN IF NOT EXISTS ""IsAlcohol"" boolean NOT NULL DEFAULT false;");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue