2018 lines
71 KiB
JavaScript
2018 lines
71 KiB
JavaScript
const fs = require('node:fs')
|
||
const path = require('node:path')
|
||
|
||
const ROOT_DIR = path.resolve(__dirname, '..')
|
||
const STANDARD_PRISMA_MIGRATION_DIR_PATTERN = /^\d{14}_[A-Za-z0-9_]+$/
|
||
const LEGACY_NON_STANDARD_PRISMA_MIGRATION_DIRS = new Set([
|
||
'20251213_performance_optimization',
|
||
'add_packaging_plan',
|
||
])
|
||
const TARGET_ONLY_READINESS_ASSURANCES = new Set([
|
||
'external-readonly-contract',
|
||
'database-guard-summary',
|
||
'foundation-quality-summary',
|
||
])
|
||
const MIGRATION_GUARD_CHECK_TYPES = new Set([
|
||
'trigger',
|
||
'check-constraint',
|
||
'unique-index',
|
||
'partial-unique-index',
|
||
])
|
||
|
||
function hasExplicitReadinessChecks(migration) {
|
||
return Boolean(
|
||
migration.columnCheck ||
|
||
(Array.isArray(migration.columnDefaultChecks) && migration.columnDefaultChecks.length > 0) ||
|
||
(Array.isArray(migration.guardChecks) && migration.guardChecks.length > 0) ||
|
||
(Array.isArray(migration.relationColumnChecks) && migration.relationColumnChecks.length > 0) ||
|
||
(Array.isArray(migration.dataChecks) && migration.dataChecks.length > 0),
|
||
)
|
||
}
|
||
|
||
const REQUIRED_MIGRATIONS = [
|
||
{
|
||
targetRelation: 'external.product_skus_v1',
|
||
targetRelations: ['external.product_skus_v1', 'external.intake_products_v1'],
|
||
path: 'prisma/migrations/20260529213000_add_external_readonly_contract/migration.sql',
|
||
},
|
||
{
|
||
targetRelation: '"IntakeNonIntegratedSkuItem"',
|
||
path: 'prisma/migrations/20260530093000_add_intake_non_integrated_sku_items/migration.sql',
|
||
guardChecks: [
|
||
{
|
||
type: 'trigger',
|
||
name: 'IntakeNonIntegratedProduct_soft_delete_cleanup_sku_items',
|
||
tableName: 'IntakeNonIntegratedProduct',
|
||
},
|
||
],
|
||
dataChecks: [
|
||
{
|
||
name: 'IntakeNonIntegratedSkuItem_payload_missing_rows_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
WITH source_sku_items AS (
|
||
SELECT
|
||
p."id" AS product_id,
|
||
item.ordinality::integer AS sort_order,
|
||
COALESCE(NULLIF(btrim(item.sku_item ->> 'id'), ''), 'sku-' || item.ordinality::text) AS raw_sku_item_id
|
||
FROM "IntakeNonIntegratedProduct" p
|
||
CROSS JOIN LATERAL jsonb_array_elements(
|
||
CASE
|
||
WHEN jsonb_typeof(p."payload"::jsonb -> 'skuItems') = 'array'
|
||
THEN p."payload"::jsonb -> 'skuItems'
|
||
ELSE '[]'::jsonb
|
||
END
|
||
) WITH ORDINALITY AS item(sku_item, ordinality)
|
||
WHERE p."deletedAt" IS NULL
|
||
),
|
||
deduped_sku_items AS (
|
||
SELECT
|
||
source_sku_items.*,
|
||
CASE
|
||
WHEN COUNT(*) OVER (PARTITION BY product_id, raw_sku_item_id) = 1
|
||
THEN raw_sku_item_id
|
||
ELSE raw_sku_item_id || '-' || ROW_NUMBER() OVER (
|
||
PARTITION BY product_id, raw_sku_item_id
|
||
ORDER BY sort_order
|
||
)::text
|
||
END AS sku_item_id
|
||
FROM source_sku_items
|
||
)
|
||
SELECT COUNT(*)::integer AS row_count
|
||
FROM deduped_sku_items source
|
||
LEFT JOIN "IntakeNonIntegratedSkuItem" sku
|
||
ON sku."productId" = source.product_id
|
||
AND sku."skuItemId" = source.sku_item_id
|
||
WHERE sku."id" IS NULL
|
||
`,
|
||
},
|
||
],
|
||
repairSql: `
|
||
WITH source_sku_items AS (
|
||
SELECT
|
||
p."id" AS product_id,
|
||
item.sku_item,
|
||
item.ordinality::integer AS sort_order,
|
||
COALESCE(NULLIF(btrim(item.sku_item ->> 'id'), ''), 'sku-' || item.ordinality::text) AS raw_sku_item_id
|
||
FROM "IntakeNonIntegratedProduct" p
|
||
CROSS JOIN LATERAL jsonb_array_elements(
|
||
CASE
|
||
WHEN jsonb_typeof(p."payload"::jsonb -> 'skuItems') = 'array'
|
||
THEN p."payload"::jsonb -> 'skuItems'
|
||
ELSE '[]'::jsonb
|
||
END
|
||
) WITH ORDINALITY AS item(sku_item, ordinality)
|
||
WHERE p."deletedAt" IS NULL
|
||
),
|
||
deduped_sku_items AS (
|
||
SELECT
|
||
source_sku_items.*,
|
||
CASE
|
||
WHEN COUNT(*) OVER (PARTITION BY product_id, raw_sku_item_id) = 1
|
||
THEN raw_sku_item_id
|
||
ELSE raw_sku_item_id || '-' || ROW_NUMBER() OVER (
|
||
PARTITION BY product_id, raw_sku_item_id
|
||
ORDER BY sort_order
|
||
)::text
|
||
END AS sku_item_id
|
||
FROM source_sku_items
|
||
),
|
||
normalized_sku_items AS (
|
||
SELECT
|
||
product_id,
|
||
sku_item,
|
||
sort_order,
|
||
sku_item_id,
|
||
NULLIF(upper(btrim(COALESCE(sku_item ->> 'skuCode', ''))), '') AS normalized_sku_code,
|
||
NULLIF(upper(btrim(COALESCE(sku_item ->> 'barcode', ''))), '') AS normalized_barcode,
|
||
array_remove(
|
||
ARRAY[
|
||
CASE
|
||
WHEN NULLIF(btrim(COALESCE(sku_item ->> 'skuCode', '')), '') IS NULL
|
||
THEN 'sku_code'
|
||
END,
|
||
CASE
|
||
WHEN NULLIF(btrim(COALESCE(sku_item ->> 'specName', '')), '') IS NULL
|
||
THEN 'spec_name'
|
||
END,
|
||
CASE
|
||
WHEN NULLIF(btrim(COALESCE(sku_item ->> 'model', '')), '') IS NULL
|
||
THEN 'model'
|
||
END
|
||
]::text[],
|
||
NULL
|
||
) AS missing_required_fields
|
||
FROM deduped_sku_items
|
||
),
|
||
quality_sku_items AS (
|
||
SELECT
|
||
normalized_sku_items.*,
|
||
COUNT(*) FILTER (WHERE normalized_sku_code IS NOT NULL) OVER (
|
||
PARTITION BY normalized_sku_code
|
||
) AS sku_code_occurrences,
|
||
COUNT(*) FILTER (WHERE normalized_barcode IS NOT NULL) OVER (
|
||
PARTITION BY normalized_barcode
|
||
) AS barcode_occurrences
|
||
FROM normalized_sku_items
|
||
)
|
||
INSERT INTO "IntakeNonIntegratedSkuItem" (
|
||
"id",
|
||
"productId",
|
||
"skuItemId",
|
||
"sortOrder",
|
||
"skuSequence",
|
||
"batchNo",
|
||
"packageGroupId",
|
||
"image",
|
||
"imageName",
|
||
"model",
|
||
"skuCode",
|
||
"specName",
|
||
"barcode",
|
||
"size",
|
||
"color",
|
||
"material",
|
||
"boardMaterialType",
|
||
"versionNo",
|
||
"attributeCode",
|
||
"drawingId",
|
||
"installMethod",
|
||
"executeStandard",
|
||
"nationalStandardCompliant",
|
||
"manufacturer",
|
||
"manufacturerCode",
|
||
"salesStatus",
|
||
"tagMaterial",
|
||
"model3dDataUrl",
|
||
"source",
|
||
"sourceTable",
|
||
"sourceSkuId",
|
||
"contentLocked",
|
||
"dataQualityStatus",
|
||
"missingRequiredFields",
|
||
"createdAt",
|
||
"updatedAt"
|
||
)
|
||
SELECT
|
||
product_id || ':' || sku_item_id AS id,
|
||
product_id,
|
||
sku_item_id,
|
||
sort_order,
|
||
CASE
|
||
WHEN COALESCE(sku_item ->> 'skuSequence', '') ~ '^-?[0-9]+$'
|
||
THEN (sku_item ->> 'skuSequence')::integer
|
||
ELSE 0
|
||
END,
|
||
CASE
|
||
WHEN COALESCE(sku_item ->> 'batchNo', '') ~ '^[0-9]+$'
|
||
THEN GREATEST(1, (sku_item ->> 'batchNo')::integer)
|
||
ELSE 1
|
||
END,
|
||
COALESCE(sku_item ->> 'packageGroupId', ''),
|
||
NULLIF(sku_item ->> 'image', ''),
|
||
COALESCE(sku_item ->> 'imageName', ''),
|
||
COALESCE(sku_item ->> 'model', ''),
|
||
COALESCE(sku_item ->> 'skuCode', ''),
|
||
COALESCE(sku_item ->> 'specName', ''),
|
||
COALESCE(sku_item ->> 'barcode', ''),
|
||
COALESCE(sku_item ->> 'size', ''),
|
||
COALESCE(sku_item ->> 'color', ''),
|
||
COALESCE(sku_item ->> 'material', ''),
|
||
COALESCE(sku_item ->> 'boardMaterialType', ''),
|
||
COALESCE(NULLIF(sku_item ->> 'versionNo', ''), 'V01'),
|
||
COALESCE(sku_item ->> 'attributeCode', ''),
|
||
COALESCE(sku_item ->> 'drawingId', ''),
|
||
COALESCE(sku_item ->> 'installMethod', ''),
|
||
COALESCE(sku_item ->> 'executeStandard', ''),
|
||
COALESCE(sku_item ->> 'nationalStandardCompliant', ''),
|
||
COALESCE(sku_item ->> 'manufacturer', ''),
|
||
COALESCE(sku_item ->> 'manufacturerCode', ''),
|
||
COALESCE(sku_item ->> 'salesStatus', ''),
|
||
COALESCE(sku_item ->> 'tagMaterial', ''),
|
||
NULLIF(sku_item ->> 'model3dDataUrl', ''),
|
||
COALESCE(sku_item ->> 'source', ''),
|
||
COALESCE(sku_item ->> 'sourceTable', ''),
|
||
COALESCE(sku_item ->> 'sourceSkuId', ''),
|
||
LOWER(COALESCE(sku_item ->> 'contentLocked', '')) IN ('true', '1', 'yes', 'on'),
|
||
CASE
|
||
WHEN sku_code_occurrences > 1 OR barcode_occurrences > 1
|
||
THEN 'CONFLICT'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||
WHEN cardinality(missing_required_fields) = 0
|
||
THEN 'READY'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||
ELSE 'INCOMPLETE'::"IntakeNonIntegratedSkuDataQualityStatus"
|
||
END,
|
||
missing_required_fields,
|
||
CURRENT_TIMESTAMP,
|
||
CURRENT_TIMESTAMP
|
||
FROM quality_sku_items
|
||
ON CONFLICT ("productId", "skuItemId") DO NOTHING;
|
||
|
||
CREATE OR REPLACE FUNCTION "cleanup_intake_non_integrated_sku_items_on_soft_delete"()
|
||
RETURNS TRIGGER AS $$
|
||
BEGIN
|
||
DELETE FROM "IntakeNonIntegratedSkuItem"
|
||
WHERE "productId" = NEW."id";
|
||
|
||
RETURN NEW;
|
||
END;
|
||
$$ LANGUAGE plpgsql;
|
||
|
||
DROP TRIGGER IF EXISTS "IntakeNonIntegratedProduct_soft_delete_cleanup_sku_items"
|
||
ON "IntakeNonIntegratedProduct";
|
||
|
||
CREATE TRIGGER "IntakeNonIntegratedProduct_soft_delete_cleanup_sku_items"
|
||
AFTER UPDATE OF "deletedAt" ON "IntakeNonIntegratedProduct"
|
||
FOR EACH ROW
|
||
WHEN (OLD."deletedAt" IS NULL AND NEW."deletedAt" IS NOT NULL)
|
||
EXECUTE FUNCTION "cleanup_intake_non_integrated_sku_items_on_soft_delete"();
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"ProductSkuAssetProfile"',
|
||
targetRelations: ['"ProductSkuAssetProfile"', '"ProductSKU"'],
|
||
path: 'prisma/migrations/20260530103000_add_product_sku_asset_profile/migration.sql',
|
||
columnCheck: {
|
||
tableName: 'ProductSKU',
|
||
columnName: 'name',
|
||
},
|
||
dataChecks: [
|
||
{
|
||
name: 'ProductSKU_name_backfill_missing_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
SELECT COUNT(*)::integer AS row_count
|
||
FROM "ProductSKU" sku
|
||
JOIN "Product" product ON product."id" = sku."productId"
|
||
WHERE sku."deletedAt" IS NULL
|
||
AND product."deletedAt" IS NULL
|
||
AND NULLIF(btrim(COALESCE(sku."name", '')), '') IS NULL
|
||
`,
|
||
},
|
||
{
|
||
name: 'ProductSkuAssetProfile_asset_snapshot_missing_rows_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
WITH sku_source AS (
|
||
SELECT sku."id" AS sku_id
|
||
FROM "ProductSKU" sku
|
||
JOIN "Product" product ON product."id" = sku."productId"
|
||
WHERE sku."deletedAt" IS NULL
|
||
AND product."deletedAt" IS NULL
|
||
AND jsonb_typeof(sku."attributes"::jsonb -> 'assetSkuSnapshot') = 'object'
|
||
)
|
||
SELECT COUNT(*)::integer AS row_count
|
||
FROM sku_source source
|
||
LEFT JOIN "ProductSkuAssetProfile" profile ON profile."skuId" = source.sku_id
|
||
WHERE profile."id" IS NULL
|
||
`,
|
||
},
|
||
],
|
||
repairSql: `
|
||
UPDATE "ProductSKU" sku
|
||
SET "name" = COALESCE(
|
||
NULLIF(sku."attributes"::jsonb ->> 'skuName', ''),
|
||
NULLIF(sku."attributes"::jsonb -> 'assetSkuSnapshot' ->> 'specName', ''),
|
||
sku."skuCode"
|
||
)
|
||
FROM "Product" product
|
||
WHERE product."id" = sku."productId"
|
||
AND sku."deletedAt" IS NULL
|
||
AND product."deletedAt" IS NULL
|
||
AND NULLIF(btrim(COALESCE(sku."name", '')), '') IS NULL;
|
||
|
||
WITH sku_source AS (
|
||
SELECT
|
||
sku."id" AS sku_id,
|
||
sku."sourceAssetVariantId",
|
||
sku."sourceAssetVariantIds",
|
||
sku."attributes"::jsonb -> 'assetSkuSnapshot' AS asset_snapshot,
|
||
sku."updatedAt"
|
||
FROM "ProductSKU" sku
|
||
JOIN "Product" product ON product."id" = sku."productId"
|
||
WHERE sku."deletedAt" IS NULL
|
||
AND product."deletedAt" IS NULL
|
||
AND jsonb_typeof(sku."attributes"::jsonb -> 'assetSkuSnapshot') = 'object'
|
||
)
|
||
INSERT INTO "ProductSkuAssetProfile" (
|
||
"id",
|
||
"skuId",
|
||
"primarySourceAssetVariantId",
|
||
"sourceAssetVariantIds",
|
||
"sourceAssetFamilyId",
|
||
"sourceAssetSeriesId",
|
||
"model",
|
||
"modelName",
|
||
"specCode",
|
||
"specName",
|
||
"barcode",
|
||
"size",
|
||
"color",
|
||
"material",
|
||
"imageUrl",
|
||
"sourceStatus",
|
||
"syncedAt",
|
||
"lockedFields",
|
||
"lockedReasons",
|
||
"sourceVariants",
|
||
"createdAt",
|
||
"updatedAt"
|
||
)
|
||
SELECT
|
||
sku_id,
|
||
sku_id,
|
||
COALESCE(NULLIF(asset_snapshot ->> 'sourceAssetVariantId', ''), "sourceAssetVariantId"),
|
||
CASE
|
||
WHEN jsonb_typeof(asset_snapshot -> 'sourceAssetVariantIds') = 'array' THEN ARRAY(
|
||
SELECT jsonb_array_elements_text(asset_snapshot -> 'sourceAssetVariantIds')
|
||
)
|
||
ELSE COALESCE("sourceAssetVariantIds", ARRAY[]::TEXT[])
|
||
END,
|
||
NULLIF(asset_snapshot ->> 'sourceAssetFamilyId', ''),
|
||
NULLIF(asset_snapshot ->> 'sourceAssetSeriesId', ''),
|
||
NULLIF(asset_snapshot ->> 'model', ''),
|
||
NULLIF(asset_snapshot ->> 'modelName', ''),
|
||
NULLIF(asset_snapshot ->> 'specCode', ''),
|
||
NULLIF(asset_snapshot ->> 'specName', ''),
|
||
NULLIF(asset_snapshot ->> 'barcode', ''),
|
||
CASE
|
||
WHEN jsonb_typeof(asset_snapshot -> 'size') = 'object' THEN asset_snapshot -> 'size'
|
||
ELSE '{}'::jsonb
|
||
END,
|
||
NULLIF(asset_snapshot ->> 'color', ''),
|
||
NULLIF(asset_snapshot ->> 'material', ''),
|
||
NULLIF(asset_snapshot ->> 'imageUrl', ''),
|
||
CASE asset_snapshot ->> 'sourceStatus'
|
||
WHEN 'DELETED' THEN 'DELETED'::"ProductSkuAssetSourceStatus"
|
||
WHEN 'MISSING' THEN 'MISSING'::"ProductSkuAssetSourceStatus"
|
||
ELSE 'ACTIVE'::"ProductSkuAssetSourceStatus"
|
||
END,
|
||
CASE
|
||
WHEN COALESCE(asset_snapshot ->> 'syncedAt', '') ~ '^\\d{4}-\\d{2}-\\d{2}T'
|
||
THEN (asset_snapshot ->> 'syncedAt')::timestamp(3)
|
||
ELSE "updatedAt"
|
||
END,
|
||
COALESCE(
|
||
ARRAY(
|
||
SELECT jsonb_array_elements_text(asset_snapshot -> 'lockedFields')
|
||
WHERE jsonb_typeof(asset_snapshot -> 'lockedFields') = 'array'
|
||
),
|
||
ARRAY[]::TEXT[]
|
||
),
|
||
CASE
|
||
WHEN jsonb_typeof(asset_snapshot -> 'lockedReasons') = 'object' THEN asset_snapshot -> 'lockedReasons'
|
||
ELSE '{}'::jsonb
|
||
END,
|
||
CASE
|
||
WHEN jsonb_typeof(asset_snapshot -> 'sourceVariants') = 'array' THEN asset_snapshot -> 'sourceVariants'
|
||
ELSE '[]'::jsonb
|
||
END,
|
||
CURRENT_TIMESTAMP,
|
||
CURRENT_TIMESTAMP
|
||
FROM sku_source
|
||
ON CONFLICT ("skuId") DO NOTHING;
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"PreSkuBasicInfo"',
|
||
path: 'prisma/migrations/20260530130000_add_pre_sku_basic_info/migration.sql',
|
||
dataChecks: [
|
||
{
|
||
name: 'PreSkuBasicInfo_display_summary_missing_rows_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
WITH pre_sku_source AS (
|
||
SELECT pre_sku."id"
|
||
FROM "PreSku" pre_sku
|
||
WHERE (
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'model', pre_sku."familySelection"::jsonb ->> 'model', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'formalSkuCode', pre_sku."familySelection"::jsonb ->> 'formalSkuCode', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'barcode', pre_sku."familySelection"::jsonb ->> 'barcode', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'size', pre_sku."familySelection"::jsonb ->> 'size', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'color', pre_sku."familySelection"::jsonb ->> 'color', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'material', pre_sku."familySelection"::jsonb ->> 'material', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'specName', pre_sku."familySelection"::jsonb ->> 'specName', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'versionNo', pre_sku."familySelection"::jsonb ->> 'versionNo', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'setCount', pre_sku."familySelection"::jsonb ->> 'setCount', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'attributeCode', pre_sku."familySelection"::jsonb ->> 'attributeCode', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'drawingId', pre_sku."familySelection"::jsonb ->> 'drawingId', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'installMethod', pre_sku."familySelection"::jsonb ->> 'installMethod', '')), '') IS NOT NULL
|
||
)
|
||
)
|
||
SELECT COUNT(*)::integer AS row_count
|
||
FROM pre_sku_source source
|
||
LEFT JOIN "PreSkuBasicInfo" info ON info."preSkuId" = source."id"
|
||
WHERE info."id" IS NULL
|
||
`,
|
||
},
|
||
],
|
||
repairSql: `
|
||
INSERT INTO "PreSkuBasicInfo" (
|
||
"id",
|
||
"preSkuId",
|
||
"model",
|
||
"formalSkuCode",
|
||
"barcode",
|
||
"size",
|
||
"color",
|
||
"material",
|
||
"specName",
|
||
"versionNo",
|
||
"setCount",
|
||
"attributeCode",
|
||
"drawingId",
|
||
"installMethod",
|
||
"manualUpdatedBy",
|
||
"manualUpdatedByName",
|
||
"manualUpdatedAt",
|
||
"source",
|
||
"createdAt",
|
||
"updatedAt"
|
||
)
|
||
SELECT
|
||
pre_sku."id",
|
||
pre_sku."id",
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'model', pre_sku."familySelection"::jsonb ->> 'model', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'formalSkuCode', pre_sku."familySelection"::jsonb ->> 'formalSkuCode', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'barcode', pre_sku."familySelection"::jsonb ->> 'barcode', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'size', pre_sku."familySelection"::jsonb ->> 'size', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'color', pre_sku."familySelection"::jsonb ->> 'color', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'material', pre_sku."familySelection"::jsonb ->> 'material', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'specName', pre_sku."familySelection"::jsonb ->> 'specName', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'versionNo', pre_sku."familySelection"::jsonb ->> 'versionNo', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'setCount', pre_sku."familySelection"::jsonb ->> 'setCount', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'attributeCode', pre_sku."familySelection"::jsonb ->> 'attributeCode', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'drawingId', pre_sku."familySelection"::jsonb ->> 'drawingId', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'installMethod', pre_sku."familySelection"::jsonb ->> 'installMethod', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'basicInfoManualUpdatedBy', '')), ''),
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'basicInfoManualUpdatedByName', '')), ''),
|
||
CASE
|
||
WHEN COALESCE(pre_sku."displaySummary"::jsonb ->> 'basicInfoManualUpdatedAt', '') ~ '^\\d{4}-\\d{2}-\\d{2}T'
|
||
THEN (pre_sku."displaySummary"::jsonb ->> 'basicInfoManualUpdatedAt')::timestamp(3)
|
||
ELSE NULL
|
||
END,
|
||
CASE
|
||
WHEN pre_sku."displaySummary"::jsonb ? 'basicInfoManualUpdatedAt' THEN 'MANUAL'
|
||
ELSE 'DRAFT_GROUP'
|
||
END,
|
||
CURRENT_TIMESTAMP,
|
||
CURRENT_TIMESTAMP
|
||
FROM "PreSku" pre_sku
|
||
WHERE (
|
||
NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'model', pre_sku."familySelection"::jsonb ->> 'model', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'formalSkuCode', pre_sku."familySelection"::jsonb ->> 'formalSkuCode', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'barcode', pre_sku."familySelection"::jsonb ->> 'barcode', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'size', pre_sku."familySelection"::jsonb ->> 'size', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'color', pre_sku."familySelection"::jsonb ->> 'color', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'material', pre_sku."familySelection"::jsonb ->> 'material', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'specName', pre_sku."familySelection"::jsonb ->> 'specName', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'versionNo', pre_sku."familySelection"::jsonb ->> 'versionNo', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'setCount', pre_sku."familySelection"::jsonb ->> 'setCount', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'attributeCode', pre_sku."familySelection"::jsonb ->> 'attributeCode', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'drawingId', pre_sku."familySelection"::jsonb ->> 'drawingId', '')), '') IS NOT NULL
|
||
OR NULLIF(btrim(COALESCE(pre_sku."displaySummary"::jsonb ->> 'installMethod', pre_sku."familySelection"::jsonb ->> 'installMethod', '')), '') IS NOT NULL
|
||
)
|
||
ON CONFLICT ("preSkuId") DO NOTHING;
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"ProductSkuQualityInspection"',
|
||
path: 'prisma/migrations/20260530143000_add_sku_quality_inspections/migration.sql',
|
||
columnCheck: {
|
||
tableName: 'ProductSkuQualityInspection',
|
||
columnName: 'recordKey',
|
||
},
|
||
},
|
||
{
|
||
targetRelation: 'external.product_skus_v1',
|
||
path: 'prisma/migrations/20260530143000_tighten_external_readonly_view_sources/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'external-readonly-contract',
|
||
},
|
||
{
|
||
targetRelation: '"IntakeNonIntegratedProductProfile"',
|
||
path: 'prisma/migrations/20260530150000_add_intake_non_integrated_product_profile/migration.sql',
|
||
dataChecks: [
|
||
{
|
||
name: 'IntakeNonIntegratedProductProfile_missing_rows_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
SELECT COUNT(*)::integer AS row_count
|
||
FROM "IntakeNonIntegratedProduct" product
|
||
LEFT JOIN "IntakeNonIntegratedProductProfile" profile ON profile."productId" = product."id"
|
||
WHERE product."deletedAt" IS NULL
|
||
AND profile."id" IS NULL
|
||
`,
|
||
},
|
||
],
|
||
repairSql: `
|
||
INSERT INTO "IntakeNonIntegratedProductProfile" (
|
||
"id",
|
||
"productId",
|
||
"productName",
|
||
"productImageUrl",
|
||
"productCategory",
|
||
"salesCategory",
|
||
"productYear",
|
||
"productStatus",
|
||
"productDevelopmentKind",
|
||
"goodsType",
|
||
"materialType",
|
||
"productionType",
|
||
"size",
|
||
"color",
|
||
"tagMaterial",
|
||
"priceBand",
|
||
"demandFlowNo",
|
||
"processTemplate",
|
||
"productPlanning",
|
||
"brandPlanning",
|
||
"demandDepartment",
|
||
"spuOwner",
|
||
"operationsOwner",
|
||
"purchaseOwner",
|
||
"visualDirector",
|
||
"visualPlanning",
|
||
"structureDesigner",
|
||
"source",
|
||
"syncedAt",
|
||
"createdAt",
|
||
"updatedAt"
|
||
)
|
||
SELECT
|
||
'inip_' || product."id",
|
||
product."id",
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'productName'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'previewImageName'), ''),
|
||
product."spu"
|
||
),
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'productImage'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'previewImage'), '')
|
||
),
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'productCategory'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'productCategory'), ''),
|
||
''
|
||
),
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'salesCategory'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'salesCategory'), ''),
|
||
''
|
||
),
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'year'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'year'), ''),
|
||
''
|
||
),
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'currentStage'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'projectStatus'), ''),
|
||
''
|
||
),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'productDevelopmentKind'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'goodsType'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'materialType'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'productionType'), ''), ''),
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'size'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'size'), ''),
|
||
''
|
||
),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'color'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'tagMaterial'), ''), ''),
|
||
COALESCE(
|
||
NULLIF(btrim(product."payload"::jsonb ->> 'priceBand'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'targetPriceBand'), ''),
|
||
''
|
||
),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'demandFlowNo'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'processTemplate'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'productPlanning'), ''),
|
||
NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'planningOwner'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'brandPlanning'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'demandDepartment'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'spuOwner'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'operationsOwner'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'purchaseOwner'), ''), NULLIF(btrim(product."purchaser"), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'visualDirector'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb ->> 'visualPlanning'), ''), ''),
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'openingReference' ->> 'structureDesigner'), ''), ''),
|
||
'MIGRATED_FROM_PAYLOAD',
|
||
CURRENT_TIMESTAMP,
|
||
CURRENT_TIMESTAMP,
|
||
CURRENT_TIMESTAMP
|
||
FROM "IntakeNonIntegratedProduct" product
|
||
WHERE product."deletedAt" IS NULL
|
||
ON CONFLICT ("productId") DO NOTHING;
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"ProductPatentRecord"',
|
||
path: 'prisma/migrations/20260530162000_add_product_patent_records/migration.sql',
|
||
},
|
||
{
|
||
targetRelation: '"IntakeNonIntegratedProductProfile"',
|
||
path: 'prisma/migrations/20260530172000_add_stock_source_fields_to_intake_product_profile/migration.sql',
|
||
columnCheck: {
|
||
tableName: 'IntakeNonIntegratedProductProfile',
|
||
columnName: 'sourceSpu',
|
||
},
|
||
dataChecks: [
|
||
{
|
||
name: 'IntakeNonIntegratedProductProfile_stock_source_payload_diff_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
SELECT COUNT(*)::integer AS row_count
|
||
FROM "IntakeNonIntegratedProductProfile" profile
|
||
JOIN "IntakeNonIntegratedProduct" product ON product."id" = profile."productId"
|
||
WHERE product."deletedAt" IS NULL
|
||
AND (
|
||
COALESCE(profile."sourceSpu", '') IS DISTINCT FROM COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSpu'), ''), '')
|
||
OR COALESCE(profile."sourceSkuId", '') IS DISTINCT FROM COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSkuId'), ''), '')
|
||
OR COALESCE(profile."sourceCategory", '') IS DISTINCT FROM COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceCategory'), ''), '')
|
||
OR COALESCE(profile."sourceItemType", '') IS DISTINCT FROM COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceItemType'), ''), '')
|
||
OR COALESCE(profile."sourceSalesStatus", '') IS DISTINCT FROM COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSalesStatus'), ''), '')
|
||
OR COALESCE(profile."sourceEnabled", '') IS DISTINCT FROM COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceEnabled'), ''), '')
|
||
OR COALESCE(profile."sourceStockDisabled", '') IS DISTINCT FROM COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceStockDisabled'), ''), '')
|
||
OR profile."stockSkuCount" IS DISTINCT FROM CASE
|
||
WHEN jsonb_typeof(product."payload"::jsonb -> 'skuItems') = 'array'
|
||
THEN jsonb_array_length(product."payload"::jsonb -> 'skuItems')
|
||
ELSE 0
|
||
END
|
||
OR profile."stockHasOnSaleSku" IS DISTINCT FROM (
|
||
COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSalesStatus'), ''), '') = '销售'
|
||
)
|
||
)
|
||
`,
|
||
},
|
||
],
|
||
repairSql: `
|
||
UPDATE "IntakeNonIntegratedProductProfile" profile
|
||
SET
|
||
"sourceSpu" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSpu'), ''), ''),
|
||
"sourceSkuId" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSkuId'), ''), ''),
|
||
"sourceCategory" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceCategory'), ''), ''),
|
||
"sourceItemType" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceItemType'), ''), ''),
|
||
"sourceSalesStatus" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSalesStatus'), ''), ''),
|
||
"sourceEnabled" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceEnabled'), ''), ''),
|
||
"sourceStockDisabled" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceStockDisabled'), ''), ''),
|
||
"stockSkuCount" = CASE
|
||
WHEN jsonb_typeof(product."payload"::jsonb -> 'skuItems') = 'array'
|
||
THEN jsonb_array_length(product."payload"::jsonb -> 'skuItems')
|
||
ELSE 0
|
||
END,
|
||
"stockHasOnSaleSku" = COALESCE(NULLIF(btrim(product."payload"::jsonb -> 'metadata' ->> 'sourceSalesStatus'), ''), '') = '销售'
|
||
FROM "IntakeNonIntegratedProduct" product
|
||
WHERE product."id" = profile."productId"
|
||
AND product."deletedAt" IS NULL;
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"IntakeReferenceLibraryItem"',
|
||
path: 'prisma/migrations/20260530190000_add_intake_reference_library/migration.sql',
|
||
},
|
||
{
|
||
targetRelation: 'external.intake_sku_items_v1',
|
||
path: 'prisma/migrations/20260531103000_tighten_external_intake_views_to_structured_sources/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'external-readonly-contract',
|
||
},
|
||
{
|
||
targetRelation: '"SpuNumberCounter"',
|
||
path: 'prisma/migrations/20260531113000_add_spu_number_counter/migration.sql',
|
||
dataChecks: [
|
||
{
|
||
name: 'SpuNumberCounter_active_rule_missing_rows_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
SELECT COUNT(*)::integer AS row_count
|
||
FROM "IntakeReferenceLibraryItem" row
|
||
LEFT JOIN "SpuNumberCounter" counter ON counter."productCategory" = row."name"
|
||
WHERE row."libraryType" = 'SPU_RULE'
|
||
AND row."isActive" = true
|
||
AND NULLIF(btrim(row."name"), '') IS NOT NULL
|
||
AND NULLIF(btrim(row."code"), '') IS NOT NULL
|
||
AND counter."id" IS NULL
|
||
`,
|
||
},
|
||
],
|
||
repairSql: `
|
||
INSERT INTO "SpuNumberCounter" (
|
||
"id",
|
||
"productCategory",
|
||
"prefix",
|
||
"lastUsedIndex",
|
||
"sourceLibraryItemId",
|
||
"createdBy",
|
||
"updatedBy",
|
||
"createdAt",
|
||
"updatedAt"
|
||
)
|
||
SELECT
|
||
'spu-counter-' || regexp_replace(lower(row."itemKey"), '[^a-z0-9_-]+', '-', 'g') AS "id",
|
||
row."name" AS "productCategory",
|
||
upper(row."code") AS "prefix",
|
||
GREATEST(
|
||
COALESCE(NULLIF(row."payload"::jsonb -> 'rule' ->> 'spuIndex', '')::integer, 0),
|
||
COALESCE(used.max_used_index, 0)
|
||
) AS "lastUsedIndex",
|
||
row."id" AS "sourceLibraryItemId",
|
||
row."createdBy",
|
||
row."updatedBy",
|
||
row."createdAt",
|
||
row."updatedAt"
|
||
FROM "IntakeReferenceLibraryItem" row
|
||
LEFT JOIN LATERAL (
|
||
SELECT MAX((substring(product."spuBase" from ('^' || upper(row."code") || '([0-9]{4})$')))::integer) AS max_used_index
|
||
FROM "IntakeNonIntegratedProduct" product
|
||
WHERE product."deletedAt" IS NULL
|
||
AND NULLIF(btrim(row."code"), '') IS NOT NULL
|
||
AND product."spuBase" ~ ('^' || upper(row."code") || '[0-9]{4}$')
|
||
) used ON true
|
||
WHERE row."libraryType" = 'SPU_RULE'
|
||
AND row."isActive" = true
|
||
AND NULLIF(btrim(row."name"), '') IS NOT NULL
|
||
AND NULLIF(btrim(row."code"), '') IS NOT NULL
|
||
ON CONFLICT ("productCategory") DO UPDATE SET
|
||
"prefix" = EXCLUDED."prefix",
|
||
"lastUsedIndex" = GREATEST("SpuNumberCounter"."lastUsedIndex", EXCLUDED."lastUsedIndex"),
|
||
"sourceLibraryItemId" = EXCLUDED."sourceLibraryItemId",
|
||
"updatedBy" = EXCLUDED."updatedBy",
|
||
"updatedAt" = CURRENT_TIMESTAMP;
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"IntakeNonIntegratedSkuItem"',
|
||
path: 'prisma/migrations/20260531143000_add_intake_master_data_check_constraints/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"SpuNumberCounter"',
|
||
path: 'prisma/migrations/20260531153000_add_spu_number_counter_check_constraints/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"IntakeReferenceLibraryItem"',
|
||
path: 'prisma/migrations/20260531162000_add_spu_rule_product_category_unique_guard/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: 'external.intake_products_v1',
|
||
path: 'prisma/migrations/20260531170000_tighten_external_intake_product_people_sources/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'external-readonly-contract',
|
||
},
|
||
{
|
||
targetRelation: '"ProductSkuAssetProfile"',
|
||
path: 'prisma/migrations/20260531173000_add_product_sku_asset_profile_barcode_unique_guard/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductSKU"',
|
||
path: 'prisma/migrations/20260531181000_add_product_sku_active_code_marker_guard/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductSkuAssetProfile"',
|
||
path: 'prisma/migrations/20260531190000_add_product_sku_asset_profile_sku_deleted_guard/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'foundation-quality-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductSKU"',
|
||
path: 'prisma/migrations/20260531203000_normalize_core_code_identity_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductSkuAssetProfile"',
|
||
path: 'prisma/migrations/20260531213000_add_product_sku_asset_profile_identity_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductPatentRecord"',
|
||
path: 'prisma/migrations/20260531223000_add_product_patent_record_identity_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"IntakeReferenceLibraryItem"',
|
||
path: 'prisma/migrations/20260531233000_add_intake_reference_library_identity_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"SpuNumberCounter"',
|
||
path: 'prisma/migrations/20260531234000_add_spu_number_counter_identity_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"PreSkuBasicInfo"',
|
||
path: 'prisma/migrations/20260531235000_add_pre_sku_basic_info_extended_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"AssetSkuSyncEvent"',
|
||
path: 'prisma/migrations/20260601000000_add_asset_sku_sync_event_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductSkuAssetProfile"',
|
||
path: 'prisma/migrations/20260601001000_add_product_sku_asset_profile_source_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"PreSkuPackagingPlan"',
|
||
path: 'prisma/migrations/20260601002000_add_packaging_warehouse_foundation_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ApprovalWorkflow"',
|
||
path: 'prisma/migrations/20260601003000_add_approval_workflow_foundation_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"User"',
|
||
path: 'prisma/migrations/20260601004000_add_people_org_foundation_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"PlanningWorkspace"',
|
||
path: 'prisma/migrations/20260601005000_add_planning_foundation_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductCollaborator"',
|
||
path: 'prisma/migrations/20260601006000_add_share_upload_collaboration_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"Panel"',
|
||
path: 'prisma/migrations/20260601007000_add_legacy_cad_structure_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductCategory"',
|
||
path: 'prisma/migrations/20260601008000_add_reference_option_and_mapping_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"SystemConfig"',
|
||
path: 'prisma/migrations/20260601009000_add_config_audit_permission_guards/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: 'external.intake_sku_items_v1',
|
||
path: 'prisma/migrations/20260601010000_add_external_intake_stable_business_keys/migration.sql',
|
||
alwaysApply: true,
|
||
relationColumnChecks: [
|
||
{
|
||
relationName: 'external.intake_products_v1',
|
||
columnNames: ['product_business_key'],
|
||
},
|
||
{
|
||
relationName: 'external.intake_products_ready_v1',
|
||
columnNames: ['product_business_key'],
|
||
},
|
||
{
|
||
relationName: 'external.intake_sku_items_v1',
|
||
columnNames: ['product_business_key', 'sku_business_key'],
|
||
},
|
||
{
|
||
relationName: 'external.intake_sku_items_ready_v1',
|
||
columnNames: ['product_business_key', 'sku_business_key'],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: 'external.product_skus_v1',
|
||
path: 'prisma/migrations/20260601011000_add_external_product_sku_stable_business_keys/migration.sql',
|
||
alwaysApply: true,
|
||
relationColumnChecks: [
|
||
{
|
||
relationName: 'external.product_skus_v1',
|
||
columnNames: ['product_business_key', 'sku_business_key'],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: 'external.product_assets_v1',
|
||
path: 'prisma/migrations/20260601012000_add_external_product_asset_bom_business_keys/migration.sql',
|
||
alwaysApply: true,
|
||
relationColumnChecks: [
|
||
{
|
||
relationName: 'external.product_assets_v1',
|
||
columnNames: ['product_business_key', 'sku_business_key'],
|
||
},
|
||
{
|
||
relationName: 'external.product_bom_v1',
|
||
columnNames: ['product_business_key', 'sku_business_key'],
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: '"IntakeReferenceLibraryItem"',
|
||
path: 'prisma/migrations/20260601013000_add_spu_rule_normalized_category_guard/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"IntakeReferenceLibraryItem"',
|
||
path: 'prisma/migrations/20260601014000_add_reference_library_code_required_guard/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'database-guard-summary',
|
||
},
|
||
{
|
||
targetRelation: '"ProductSKU"',
|
||
path: 'prisma/migrations/20260601015000_add_product_sku_soft_delete_identity_fields/migration.sql',
|
||
alwaysApply: true,
|
||
columnCheck: {
|
||
tableName: 'ProductSKU',
|
||
columnName: 'originalSkuCode',
|
||
},
|
||
guardChecks: [
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'ProductSKU_soft_delete_identity_valid',
|
||
tableName: 'ProductSKU',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'ProductSKU_no_soft_delete_lifecycle_attributes',
|
||
tableName: 'ProductSKU',
|
||
},
|
||
],
|
||
repairSql: `
|
||
UPDATE "ProductSKU"
|
||
SET
|
||
"originalSkuCode" = upper(btrim(
|
||
COALESCE(
|
||
NULLIF(attributes->>'_originalSkuCode', ''),
|
||
CASE
|
||
WHEN strpos("skuCode", '__deleted__') > 0
|
||
THEN substring("skuCode" from 1 for strpos("skuCode", '__deleted__') - 1)
|
||
ELSE "skuCode"
|
||
END
|
||
)
|
||
)),
|
||
"deletedCodeAt" = COALESCE(
|
||
"deletedCodeAt",
|
||
CASE
|
||
WHEN NULLIF(attributes->>'_deletedCodeAt', '') ~ '^\\d{4}-\\d{2}-\\d{2}T'
|
||
THEN (attributes->>'_deletedCodeAt')::timestamp(3)
|
||
ELSE "deletedAt"
|
||
END
|
||
),
|
||
"deletedByProductId" = COALESCE("deletedByProductId", NULLIF(btrim(attributes->>'_deletedByProductId'), '')),
|
||
"productDeletedAt" = COALESCE(
|
||
"productDeletedAt",
|
||
CASE
|
||
WHEN NULLIF(attributes->>'_productDeletedAt', '') ~ '^\\d{4}-\\d{2}-\\d{2}T'
|
||
THEN (attributes->>'_productDeletedAt')::timestamp(3)
|
||
ELSE NULL
|
||
END
|
||
)
|
||
WHERE "deletedAt" IS NOT NULL;
|
||
|
||
UPDATE "ProductSKU"
|
||
SET attributes = attributes
|
||
- '_originalSkuCode'
|
||
- '_deletedCodeAt'
|
||
- '_deletedByProductId'
|
||
- '_productDeletedAt'
|
||
WHERE attributes ? '_originalSkuCode'
|
||
OR attributes ? '_deletedCodeAt'
|
||
OR attributes ? '_deletedByProductId'
|
||
OR attributes ? '_productDeletedAt';
|
||
|
||
ALTER TABLE "ProductSKU"
|
||
DROP CONSTRAINT IF EXISTS "ProductSKU_soft_delete_identity_valid",
|
||
ADD CONSTRAINT "ProductSKU_soft_delete_identity_valid"
|
||
CHECK (
|
||
(
|
||
"deletedAt" IS NULL
|
||
AND "originalSkuCode" IS NULL
|
||
AND "deletedCodeAt" IS NULL
|
||
AND "deletedByProductId" IS NULL
|
||
AND "productDeletedAt" IS NULL
|
||
)
|
||
OR
|
||
(
|
||
"deletedAt" IS NOT NULL
|
||
AND NULLIF(btrim(COALESCE("originalSkuCode", '')), '') IS NOT NULL
|
||
AND "originalSkuCode" = upper(btrim("originalSkuCode"))
|
||
AND ("deletedByProductId" IS NULL OR NULLIF(btrim("deletedByProductId"), '') IS NOT NULL)
|
||
)
|
||
);
|
||
|
||
ALTER TABLE "ProductSKU"
|
||
DROP CONSTRAINT IF EXISTS "ProductSKU_no_soft_delete_lifecycle_attributes",
|
||
ADD CONSTRAINT "ProductSKU_no_soft_delete_lifecycle_attributes"
|
||
CHECK (
|
||
NOT (
|
||
attributes ? '_originalSkuCode'
|
||
OR attributes ? '_deletedCodeAt'
|
||
OR attributes ? '_deletedByProductId'
|
||
OR attributes ? '_productDeletedAt'
|
||
)
|
||
);
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: 'external.materials_v1',
|
||
targetRelations: ['external.materials_v1', 'external.product_bom_v1'],
|
||
path: 'prisma/migrations/20260601016000_tighten_external_material_and_bom_contract/migration.sql',
|
||
alwaysApply: true,
|
||
targetOnlyReadinessAssurance: 'external-readonly-contract',
|
||
},
|
||
{
|
||
targetRelation: '"Hardware"',
|
||
targetRelations: ['"Hardware"', '"Product"', '"ProductSKU"', '"Substrate"'],
|
||
path: 'prisma/migrations/20260601017000_align_migration_replay_to_current_schema/migration.sql',
|
||
columnCheck: {
|
||
tableName: 'Hardware',
|
||
columnName: 'geoWidth',
|
||
},
|
||
columnDefaultChecks: [
|
||
{
|
||
tableName: 'AssemblyStep',
|
||
columnName: 'partIds',
|
||
expectedDefault: null,
|
||
},
|
||
{
|
||
tableName: 'AssemblyStep',
|
||
columnName: 'hardwareIds',
|
||
expectedDefault: null,
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: '"MasterDataQualityDecision"',
|
||
path: 'prisma/migrations/20260601018000_add_master_data_quality_decisions/migration.sql',
|
||
guardChecks: [
|
||
{
|
||
type: 'partial-unique-index',
|
||
name: 'MasterDataQualityDecision_open_target_key',
|
||
tableName: 'MasterDataQualityDecision',
|
||
predicatePattern: 'status = \'APPROVED_INCOMPLETE\'',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_target_not_blank',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_status_allowed',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_reason_not_blank',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_decidedBy_not_blank',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_resolved_state_valid',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
],
|
||
repairSql: `
|
||
ALTER TABLE "MasterDataQualityDecision"
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_target_not_blank",
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_status_allowed",
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_reason_not_blank",
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_decidedBy_not_blank",
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_resolved_state_valid",
|
||
ADD CONSTRAINT "MasterDataQualityDecision_target_not_blank" CHECK (
|
||
NULLIF(btrim("targetType"), '') IS NOT NULL
|
||
AND NULLIF(btrim("targetId"), '') IS NOT NULL
|
||
),
|
||
ADD CONSTRAINT "MasterDataQualityDecision_status_allowed" CHECK (
|
||
"status" IN ('APPROVED_INCOMPLETE', 'RESOLVED')
|
||
),
|
||
ADD CONSTRAINT "MasterDataQualityDecision_reason_not_blank" CHECK (
|
||
NULLIF(btrim("reason"), '') IS NOT NULL
|
||
),
|
||
ADD CONSTRAINT "MasterDataQualityDecision_decidedBy_not_blank" CHECK (
|
||
NULLIF(btrim("decidedBy"), '') IS NOT NULL
|
||
),
|
||
ADD CONSTRAINT "MasterDataQualityDecision_resolved_state_valid" CHECK (
|
||
("status" <> 'RESOLVED' AND "resolvedAt" IS NULL AND "resolvedBy" IS NULL)
|
||
OR
|
||
("status" = 'RESOLVED' AND "resolvedAt" IS NOT NULL AND NULLIF(btrim(COALESCE("resolvedBy", '')), '') IS NOT NULL)
|
||
);
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS "MasterDataQualityDecision_open_target_key"
|
||
ON "MasterDataQualityDecision"("targetType", "targetId")
|
||
WHERE "status" = 'APPROVED_INCOMPLETE';
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"ProductSKU"',
|
||
path: 'prisma/migrations/20260601019000_add_formal_cost_bom_guards/migration.sql',
|
||
alwaysApply: true,
|
||
guardChecks: [
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'ProductSKU_price_cost_stock_non_negative',
|
||
tableName: 'ProductSKU',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: '"AssetVariant"',
|
||
targetRelations: ['"AssetSeries"', '"AssetFamily"', '"AssetVariant"'],
|
||
path: 'prisma/migrations/20260601020000_add_asset_center_source_guards/migration.sql',
|
||
alwaysApply: true,
|
||
guardChecks: [
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetSeries_identity_valid',
|
||
tableName: 'AssetSeries',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetSeries_metadata_object',
|
||
tableName: 'AssetSeries',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetFamily_identity_valid',
|
||
tableName: 'AssetFamily',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetFamily_metadata_object',
|
||
tableName: 'AssetFamily',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetFamily_lifecycle_state_valid',
|
||
tableName: 'AssetFamily',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetVariant_identity_valid',
|
||
tableName: 'AssetVariant',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetVariant_json_shape_valid',
|
||
tableName: 'AssetVariant',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetVariant_cost_weight_valid',
|
||
tableName: 'AssetVariant',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetVariant_lifecycle_state_valid',
|
||
tableName: 'AssetVariant',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'AssetVariant_inheritance_not_self',
|
||
tableName: 'AssetVariant',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: '"PreSku"',
|
||
targetRelations: ['"DraftGroup"', '"PreSku"'],
|
||
path: 'prisma/migrations/20260601021000_add_draft_pre_sku_source_guards/migration.sql',
|
||
alwaysApply: true,
|
||
guardChecks: [
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'DraftGroup_identity_valid',
|
||
tableName: 'DraftGroup',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'DraftGroup_summary_object',
|
||
tableName: 'DraftGroup',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'DraftGroup_optional_text_not_blank',
|
||
tableName: 'DraftGroup',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'PreSku_identity_valid',
|
||
tableName: 'PreSku',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'PreSku_process_snapshots_shape_valid',
|
||
tableName: 'PreSku',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'PreSku_weight_valid',
|
||
tableName: 'PreSku',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'PreSku_component_codes_valid',
|
||
tableName: 'PreSku',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'PreSku_optional_text_not_blank',
|
||
tableName: 'PreSku',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: '"ProductSKU"',
|
||
path: 'prisma/migrations/20260601022000_add_product_sku_attributes_truth_guard/migration.sql',
|
||
alwaysApply: true,
|
||
guardChecks: [
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'ProductSKU_no_formal_truth_attributes',
|
||
tableName: 'ProductSKU',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: '"ProductSKU"',
|
||
path: 'prisma/migrations/20260601023000_add_product_sku_asset_profile_deleted_at_trigger/migration.sql',
|
||
alwaysApply: true,
|
||
guardChecks: [
|
||
{
|
||
type: 'trigger',
|
||
name: 'ProductSKU_sync_asset_profile_deleted_at',
|
||
tableName: 'ProductSKU',
|
||
},
|
||
],
|
||
},
|
||
{
|
||
targetRelation: '"ProductSKU"',
|
||
path: 'prisma/migrations/20260601024000_add_product_sku_soft_delete_tombstone_guard/migration.sql',
|
||
alwaysApply: true,
|
||
guardChecks: [
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'ProductSKU_soft_delete_tombstone_valid',
|
||
tableName: 'ProductSKU',
|
||
},
|
||
],
|
||
repairSql: `
|
||
UPDATE "ProductSKU"
|
||
SET "deletedCodeAt" = COALESCE("deletedCodeAt", "deletedAt")
|
||
WHERE "deletedAt" IS NOT NULL
|
||
AND "deletedCodeAt" IS NULL;
|
||
|
||
ALTER TABLE "ProductSKU"
|
||
DROP CONSTRAINT IF EXISTS "ProductSKU_soft_delete_tombstone_valid",
|
||
ADD CONSTRAINT "ProductSKU_soft_delete_tombstone_valid"
|
||
CHECK (
|
||
"deletedAt" IS NULL
|
||
OR (
|
||
"deletedCodeAt" IS NOT NULL
|
||
AND (
|
||
("deletedByProductId" IS NULL AND "productDeletedAt" IS NULL)
|
||
OR (
|
||
NULLIF(btrim(COALESCE("deletedByProductId", '')), '') IS NOT NULL
|
||
AND "productDeletedAt" IS NOT NULL
|
||
)
|
||
)
|
||
)
|
||
);
|
||
`,
|
||
},
|
||
{
|
||
targetRelation: '"MasterDataQualityDecision"',
|
||
path: 'prisma/migrations/20260601025000_tighten_master_data_quality_decision_text_guards/migration.sql',
|
||
alwaysApply: true,
|
||
guardChecks: [
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_reason_not_placeholder',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_decidedBy_not_placeholder',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
{
|
||
type: 'check-constraint',
|
||
name: 'MasterDataQualityDecision_resolvedBy_not_placeholder',
|
||
tableName: 'MasterDataQualityDecision',
|
||
},
|
||
],
|
||
dataChecks: [
|
||
{
|
||
name: 'master_data_quality_decision_placeholder_text_zero',
|
||
expectedValue: 0,
|
||
sql: `
|
||
WITH placeholder_tokens AS (
|
||
SELECT unnest(ARRAY[
|
||
'unknown', 'unk', 'n/a', 'na', 'null', 'none', 'todo', 'tbd',
|
||
'default', 'test', '测试', '待补', '待补齐', '待确认', '未知',
|
||
'默认', '默认值', '暂无'
|
||
]) AS token
|
||
)
|
||
SELECT COUNT(*)::integer AS row_count /* data-check */
|
||
FROM "MasterDataQualityDecision"
|
||
WHERE (
|
||
NULLIF(btrim(COALESCE("reason", '')), '') IS NOT NULL
|
||
AND (
|
||
lower(regexp_replace(btrim("reason"), '\\s+', '', 'g')) IN (
|
||
SELECT token FROM placeholder_tokens
|
||
)
|
||
OR regexp_replace(btrim("reason"), '\\s+', '', 'g') ~ '(待补|待确认|默认值)'
|
||
)
|
||
)
|
||
OR (
|
||
NULLIF(btrim(COALESCE("decidedBy", '')), '') IS NOT NULL
|
||
AND (
|
||
lower(regexp_replace(btrim("decidedBy"), '\\s+', '', 'g')) IN (
|
||
SELECT token FROM placeholder_tokens
|
||
)
|
||
OR regexp_replace(btrim("decidedBy"), '\\s+', '', 'g') ~ '(待补|待确认|默认值)'
|
||
)
|
||
)
|
||
OR (
|
||
NULLIF(btrim(COALESCE("resolvedBy", '')), '') IS NOT NULL
|
||
AND (
|
||
lower(regexp_replace(btrim("resolvedBy"), '\\s+', '', 'g')) IN (
|
||
SELECT token FROM placeholder_tokens
|
||
)
|
||
OR regexp_replace(btrim("resolvedBy"), '\\s+', '', 'g') ~ '(待补|待确认|默认值)'
|
||
)
|
||
);
|
||
`,
|
||
},
|
||
],
|
||
repairSql: `
|
||
ALTER TABLE "MasterDataQualityDecision"
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_reason_not_placeholder",
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_decidedBy_not_placeholder",
|
||
DROP CONSTRAINT IF EXISTS "MasterDataQualityDecision_resolvedBy_not_placeholder",
|
||
ADD CONSTRAINT "MasterDataQualityDecision_reason_not_placeholder"
|
||
CHECK (
|
||
lower(regexp_replace(btrim("reason"), '\\s+', '', 'g')) NOT IN (
|
||
'unknown', 'unk', 'n/a', 'na', 'null', 'none', 'todo', 'tbd',
|
||
'default', 'test', '测试', '待补', '待补齐', '待确认', '未知',
|
||
'默认', '默认值', '暂无'
|
||
)
|
||
AND regexp_replace(btrim("reason"), '\\s+', '', 'g') !~ '(待补|待确认|默认值)'
|
||
),
|
||
ADD CONSTRAINT "MasterDataQualityDecision_decidedBy_not_placeholder"
|
||
CHECK (
|
||
lower(regexp_replace(btrim("decidedBy"), '\\s+', '', 'g')) NOT IN (
|
||
'unknown', 'unk', 'n/a', 'na', 'null', 'none', 'todo', 'tbd',
|
||
'default', 'test', '测试', '待补', '待补齐', '待确认', '未知',
|
||
'默认', '默认值', '暂无'
|
||
)
|
||
AND regexp_replace(btrim("decidedBy"), '\\s+', '', 'g') !~ '(待补|待确认|默认值)'
|
||
),
|
||
ADD CONSTRAINT "MasterDataQualityDecision_resolvedBy_not_placeholder"
|
||
CHECK (
|
||
"resolvedBy" IS NULL
|
||
OR (
|
||
lower(regexp_replace(btrim("resolvedBy"), '\\s+', '', 'g')) NOT IN (
|
||
'unknown', 'unk', 'n/a', 'na', 'null', 'none', 'todo', 'tbd',
|
||
'default', 'test', '测试', '待补', '待补齐', '待确认', '未知',
|
||
'默认', '默认值', '暂无'
|
||
)
|
||
AND regexp_replace(btrim("resolvedBy"), '\\s+', '', 'g') !~ '(待补|待确认|默认值)'
|
||
)
|
||
);
|
||
`,
|
||
},
|
||
]
|
||
|
||
function getMigrationTimestamp(migrationPath) {
|
||
const match = String(migrationPath).match(/prisma\/migrations\/(\d{14})_[^/]+\/migration\.sql$/)
|
||
return match?.[1] ?? null
|
||
}
|
||
|
||
function getMigrationTargetRelations(migration) {
|
||
if (Array.isArray(migration.targetRelations)) {
|
||
return migration.targetRelations
|
||
}
|
||
return migration.targetRelation ? [migration.targetRelation] : []
|
||
}
|
||
|
||
function getRelationSearchTerms(relationName) {
|
||
const raw = String(relationName)
|
||
const unquoted = raw.replace(/"/g, '')
|
||
const relationOnly = unquoted.split('.').pop()
|
||
return [...new Set([raw, unquoted, relationOnly].filter(Boolean).map((term) => term.toLowerCase()))]
|
||
}
|
||
|
||
function validateRequiredMigrationCatalog(options = {}) {
|
||
const rootDir = options.rootDir ?? ROOT_DIR
|
||
const migrations = options.migrations ?? REQUIRED_MIGRATIONS
|
||
const errors = []
|
||
const seenPaths = new Set()
|
||
let previousPath = ''
|
||
|
||
validatePrismaMigrationDirectories({ rootDir })
|
||
|
||
migrations.forEach((migration, index) => {
|
||
const label = migration.path || `#${index + 1}`
|
||
if (!migration.path || typeof migration.path !== 'string') {
|
||
errors.push(`迁移清单第 ${index + 1} 项缺少 path。`)
|
||
return
|
||
}
|
||
if (seenPaths.has(migration.path)) {
|
||
errors.push(`迁移清单存在重复 path:${migration.path}`)
|
||
}
|
||
seenPaths.add(migration.path)
|
||
if (previousPath && migration.path < previousPath) {
|
||
errors.push(`迁移清单顺序必须按 path 升序排列:${previousPath} -> ${migration.path}`)
|
||
}
|
||
previousPath = migration.path
|
||
|
||
if (!getMigrationTimestamp(migration.path)) {
|
||
errors.push(`迁移 path 不符合 prisma/migrations/<timestamp>_name/migration.sql:${migration.path}`)
|
||
}
|
||
|
||
const targetRelations = getMigrationTargetRelations(migration)
|
||
if (targetRelations.length === 0) {
|
||
errors.push(`迁移缺少 targetRelation / targetRelations:${label}`)
|
||
}
|
||
if (migration.columnCheck) {
|
||
if (!migration.columnCheck.tableName || !migration.columnCheck.columnName) {
|
||
errors.push(`迁移 columnCheck 必须同时声明 tableName 和 columnName:${label}`)
|
||
}
|
||
const normalizedTargets = targetRelations
|
||
.map((relationName) => String(relationName).replace(/"/g, '').split('.').pop())
|
||
if (!normalizedTargets.includes(migration.columnCheck.tableName)) {
|
||
errors.push(`迁移 columnCheck.tableName 必须属于 targetRelation:${label}`)
|
||
}
|
||
}
|
||
if (Array.isArray(migration.columnDefaultChecks)) {
|
||
migration.columnDefaultChecks.forEach((check, checkIndex) => {
|
||
if (!check.tableName || !check.columnName || !Object.hasOwn(check, 'expectedDefault')) {
|
||
errors.push(`迁移 columnDefaultChecks[${checkIndex}] 必须声明 tableName、columnName 和 expectedDefault:${label}`)
|
||
}
|
||
})
|
||
}
|
||
if (Array.isArray(migration.guardChecks)) {
|
||
migration.guardChecks.forEach((check, checkIndex) => {
|
||
if (!MIGRATION_GUARD_CHECK_TYPES.has(check.type) || !check.name || !check.tableName) {
|
||
errors.push(`迁移 guardChecks[${checkIndex}] 必须声明 type、name 和 tableName:${label}`)
|
||
}
|
||
if (check.type === 'partial-unique-index' && !check.predicatePattern) {
|
||
errors.push(`迁移 guardChecks[${checkIndex}] 的 partial-unique-index 必须声明 predicatePattern:${label}`)
|
||
}
|
||
})
|
||
}
|
||
if (Array.isArray(migration.relationColumnChecks)) {
|
||
migration.relationColumnChecks.forEach((check, checkIndex) => {
|
||
if (!check.relationName || !Array.isArray(check.columnNames) || check.columnNames.length === 0) {
|
||
errors.push(`迁移 relationColumnChecks[${checkIndex}] 必须声明 relationName 和 columnNames:${label}`)
|
||
}
|
||
})
|
||
}
|
||
if (Array.isArray(migration.dataChecks)) {
|
||
migration.dataChecks.forEach((check, checkIndex) => {
|
||
if (!check.name || typeof check.sql !== 'string' || !Object.hasOwn(check, 'expectedValue')) {
|
||
errors.push(`迁移 dataChecks[${checkIndex}] 必须声明 name、sql 和 expectedValue:${label}`)
|
||
}
|
||
})
|
||
}
|
||
if (migration.alwaysApply && !hasExplicitReadinessChecks(migration)) {
|
||
if (!TARGET_ONLY_READINESS_ASSURANCES.has(migration.targetOnlyReadinessAssurance)) {
|
||
errors.push(
|
||
`alwaysApply 迁移没有显式 readiness check 时,必须声明 targetOnlyReadinessAssurance:${label}`,
|
||
)
|
||
}
|
||
}
|
||
|
||
const absolutePath = path.join(rootDir, migration.path)
|
||
if (!fs.existsSync(absolutePath)) {
|
||
errors.push(`迁移文件不存在:${migration.path}`)
|
||
return
|
||
}
|
||
|
||
const sql = fs.readFileSync(absolutePath, 'utf8').toLowerCase()
|
||
targetRelations.forEach((relationName) => {
|
||
const hasRelation = getRelationSearchTerms(relationName).some((term) => sql.includes(term))
|
||
if (!hasRelation) {
|
||
errors.push(`迁移文件未引用声明的 targetRelation:${migration.path} -> ${relationName}`)
|
||
}
|
||
})
|
||
})
|
||
|
||
if (errors.length > 0) {
|
||
throw new Error(`主数据治理迁移清单无效:\n${errors.join('\n')}`)
|
||
}
|
||
|
||
return {
|
||
migrationCount: migrations.length,
|
||
structureMigrationCount: migrations.filter((migration) => !migration.alwaysApply).length,
|
||
guardMigrationCount: migrations.filter((migration) => migration.alwaysApply).length,
|
||
targetOnlyAlwaysApplyMigrationCount: migrations.filter(
|
||
(migration) => migration.alwaysApply && !hasExplicitReadinessChecks(migration),
|
||
).length,
|
||
}
|
||
}
|
||
|
||
function validatePrismaMigrationDirectories(options = {}) {
|
||
const rootDir = options.rootDir ?? ROOT_DIR
|
||
const migrationsDir = path.join(rootDir, 'prisma/migrations')
|
||
const errors = []
|
||
|
||
if (!fs.existsSync(migrationsDir)) {
|
||
throw new Error(`Prisma migrations 目录不存在:${path.relative(rootDir, migrationsDir)}`)
|
||
}
|
||
|
||
const dirs = fs
|
||
.readdirSync(migrationsDir, { withFileTypes: true })
|
||
.filter((entry) => entry.isDirectory())
|
||
.map((entry) => entry.name)
|
||
.sort()
|
||
|
||
dirs.forEach((dirName) => {
|
||
const migrationSqlPath = path.join(migrationsDir, dirName, 'migration.sql')
|
||
if (!fs.existsSync(migrationSqlPath)) {
|
||
errors.push(`Prisma migration 缺少 migration.sql:prisma/migrations/${dirName}`)
|
||
}
|
||
if (
|
||
!STANDARD_PRISMA_MIGRATION_DIR_PATTERN.test(dirName) &&
|
||
!LEGACY_NON_STANDARD_PRISMA_MIGRATION_DIRS.has(dirName)
|
||
) {
|
||
errors.push(
|
||
`Prisma migration 目录名不符合 <timestamp>_name 且未登记为历史例外:prisma/migrations/${dirName}`,
|
||
)
|
||
}
|
||
})
|
||
|
||
if (errors.length > 0) {
|
||
throw new Error(`Prisma migration 目录无效:\n${errors.join('\n')}`)
|
||
}
|
||
|
||
return {
|
||
migrationDirectoryCount: dirs.length,
|
||
legacyNonStandardMigrationDirectories: dirs.filter((dirName) =>
|
||
LEGACY_NON_STANDARD_PRISMA_MIGRATION_DIRS.has(dirName),
|
||
),
|
||
}
|
||
}
|
||
|
||
function classifyAppliedMigrationPaths(appliedMigrationPaths, migrations = REQUIRED_MIGRATIONS) {
|
||
const idempotentMigrationPaths = new Set(
|
||
migrations
|
||
.filter((migration) => migration.alwaysApply)
|
||
.map((migration) => migration.path),
|
||
)
|
||
return {
|
||
structureMigrations: appliedMigrationPaths.filter(
|
||
(migrationPath) => !idempotentMigrationPaths.has(migrationPath),
|
||
),
|
||
idempotentMigrations: appliedMigrationPaths.filter(
|
||
(migrationPath) => idempotentMigrationPaths.has(migrationPath),
|
||
),
|
||
}
|
||
}
|
||
|
||
function getPendingStructureMigrationPaths(appliedMigrationPaths, migrations = REQUIRED_MIGRATIONS) {
|
||
return classifyAppliedMigrationPaths(appliedMigrationPaths, migrations).structureMigrations
|
||
}
|
||
|
||
async function relationExists(client, relationName) {
|
||
const result = await client.query('SELECT to_regclass($1) AS relation_name', [relationName])
|
||
return result.rows[0]?.relation_name !== null
|
||
}
|
||
|
||
async function columnExists(client, tableName, columnName) {
|
||
const result = await client.query(`
|
||
SELECT 1
|
||
FROM information_schema.columns
|
||
WHERE table_schema = 'public'
|
||
AND table_name = $1
|
||
AND column_name = $2
|
||
LIMIT 1
|
||
`, [tableName, columnName])
|
||
return result.rowCount > 0
|
||
}
|
||
|
||
async function columnDefaultMatches(client, tableName, columnName, expectedDefault) {
|
||
const result = await client.query(`
|
||
SELECT column_default
|
||
FROM information_schema.columns
|
||
WHERE table_schema = 'public'
|
||
AND table_name = $1
|
||
AND column_name = $2
|
||
LIMIT 1
|
||
`, [tableName, columnName])
|
||
if (result.rowCount === 0) return false
|
||
const actualDefault = result.rows[0]?.column_default ?? null
|
||
return actualDefault === expectedDefault
|
||
}
|
||
|
||
function parseRelationName(relationName) {
|
||
const normalized = String(relationName).replace(/"/g, '')
|
||
const parts = normalized.split('.').filter(Boolean)
|
||
if (parts.length === 1) {
|
||
return { schemaName: 'public', tableName: parts[0] }
|
||
}
|
||
return {
|
||
schemaName: parts[parts.length - 2],
|
||
tableName: parts[parts.length - 1],
|
||
}
|
||
}
|
||
|
||
async function relationColumnExists(client, relationName, columnName) {
|
||
const { schemaName, tableName } = parseRelationName(relationName)
|
||
const result = await client.query(`
|
||
SELECT 1
|
||
FROM information_schema.columns
|
||
WHERE table_schema = $1
|
||
AND table_name = $2
|
||
AND column_name = $3
|
||
LIMIT 1
|
||
`, [schemaName, tableName, columnName])
|
||
return result.rowCount > 0
|
||
}
|
||
|
||
async function dataCheckPasses(client, check) {
|
||
let result
|
||
try {
|
||
result = await client.query(check.sql)
|
||
} catch (error) {
|
||
if (error?.code === '42P01') {
|
||
return false
|
||
}
|
||
throw error
|
||
}
|
||
const row = result.rows[0] ?? {}
|
||
const firstValue = Object.values(row)[0]
|
||
return firstValue === check.expectedValue
|
||
}
|
||
|
||
function normalizeSqlFragment(value) {
|
||
return String(value ?? '')
|
||
.replace(/\s+/g, ' ')
|
||
.replace(/""/g, '"')
|
||
.trim()
|
||
}
|
||
|
||
async function guardExists(client, guard) {
|
||
if (guard.type === 'trigger') {
|
||
const result = await client.query(`
|
||
SELECT 1
|
||
FROM information_schema.triggers
|
||
WHERE event_object_schema = 'public'
|
||
AND event_object_table = $1
|
||
AND trigger_name = $2
|
||
LIMIT 1
|
||
`, [guard.tableName, guard.name])
|
||
return result.rowCount > 0
|
||
}
|
||
if (guard.type === 'check-constraint') {
|
||
const result = await client.query(`
|
||
SELECT 1
|
||
FROM pg_constraint c
|
||
JOIN pg_class t ON t.oid = c.conrelid
|
||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
||
WHERE n.nspname = 'public'
|
||
AND t.relname = $1
|
||
AND c.conname = $2
|
||
AND c.contype = 'c'
|
||
LIMIT 1
|
||
`, [guard.tableName, guard.name])
|
||
return result.rowCount > 0
|
||
}
|
||
if (guard.type === 'unique-index' || guard.type === 'partial-unique-index') {
|
||
const result = await client.query(`
|
||
SELECT
|
||
idx.indisunique AS is_unique,
|
||
pg_get_expr(idx.indpred, idx.indrelid) AS predicate,
|
||
pg_get_indexdef(cls.oid) AS definition
|
||
FROM pg_index idx
|
||
JOIN pg_class cls ON cls.oid = idx.indexrelid
|
||
JOIN pg_class table_cls ON table_cls.oid = idx.indrelid
|
||
JOIN pg_namespace ns ON ns.oid = table_cls.relnamespace
|
||
WHERE ns.nspname = 'public'
|
||
AND cls.relname = $1
|
||
AND table_cls.relname = $2
|
||
LIMIT 1
|
||
`, [guard.name, guard.tableName])
|
||
const row = result.rows[0]
|
||
const predicate = normalizeSqlFragment(row?.predicate)
|
||
const definition = normalizeSqlFragment(row?.definition)
|
||
const hasExpectedPredicate =
|
||
guard.type !== 'partial-unique-index' ||
|
||
(predicate && predicate.includes(normalizeSqlFragment(guard.predicatePattern)))
|
||
const hasExpectedDefinition =
|
||
!guard.definitionPattern || definition.includes(normalizeSqlFragment(guard.definitionPattern))
|
||
|
||
return Boolean(row?.is_unique) && hasExpectedPredicate && hasExpectedDefinition
|
||
}
|
||
return false
|
||
}
|
||
|
||
async function assertMigrationTargetReady(client, migration) {
|
||
const targetRelations = getMigrationTargetRelations(migration)
|
||
const relationChecks = await Promise.all(
|
||
targetRelations.map(async (relationName) => ({
|
||
relationName,
|
||
exists: await relationExists(client, relationName),
|
||
})),
|
||
)
|
||
const missingRelations = relationChecks
|
||
.filter((check) => !check.exists)
|
||
.map((check) => check.relationName)
|
||
|
||
if (missingRelations.length > 0) {
|
||
throw new Error(
|
||
`治理迁移执行后未生成声明的 targetRelation:${migration.path} -> ${missingRelations.join(', ')}`,
|
||
)
|
||
}
|
||
|
||
if (
|
||
migration.columnCheck &&
|
||
!await columnExists(client, migration.columnCheck.tableName, migration.columnCheck.columnName)
|
||
) {
|
||
throw new Error(
|
||
`治理迁移执行后未生成声明的 columnCheck:${migration.path} -> ${migration.columnCheck.tableName}.${migration.columnCheck.columnName}`,
|
||
)
|
||
}
|
||
if (Array.isArray(migration.columnDefaultChecks)) {
|
||
for (const check of migration.columnDefaultChecks) {
|
||
if (!await columnDefaultMatches(client, check.tableName, check.columnName, check.expectedDefault)) {
|
||
throw new Error(
|
||
`治理迁移执行后未满足声明的 columnDefaultCheck:${migration.path} -> ${check.tableName}.${check.columnName}`,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(migration.guardChecks)) {
|
||
for (const check of migration.guardChecks) {
|
||
if (!await guardExists(client, check)) {
|
||
throw new Error(
|
||
`治理迁移执行后未满足声明的 guardCheck:${migration.path} -> ${check.type}:${check.tableName}.${check.name}`,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(migration.relationColumnChecks)) {
|
||
for (const check of migration.relationColumnChecks) {
|
||
for (const columnName of check.columnNames) {
|
||
if (!await relationColumnExists(client, check.relationName, columnName)) {
|
||
throw new Error(
|
||
`治理迁移执行后未满足声明的 relationColumnCheck:${migration.path} -> ${check.relationName}.${columnName}`,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (Array.isArray(migration.dataChecks)) {
|
||
for (const check of migration.dataChecks) {
|
||
if (!await dataCheckPasses(client, check)) {
|
||
throw new Error(
|
||
`治理迁移执行后未满足声明的 dataCheck:${migration.path} -> ${check.name}`,
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function applyMissingMigrationFiles(client, migrations = REQUIRED_MIGRATIONS) {
|
||
const applied = []
|
||
const readinessByPath = new Map()
|
||
for (const migration of migrations) {
|
||
readinessByPath.set(migration.path, await getMigrationApplyReadiness(client, migration))
|
||
}
|
||
|
||
const shouldReplayIdempotentMigrations = Array.from(readinessByPath.values()).some(
|
||
(readiness) => !readiness.ready,
|
||
)
|
||
|
||
for (const migration of migrations) {
|
||
const readiness = readinessByPath.get(migration.path)
|
||
const shouldReplayCurrentIdempotent = Boolean(
|
||
migration.alwaysApply &&
|
||
shouldReplayIdempotentMigrations &&
|
||
!hasExplicitReadinessChecks(migration),
|
||
)
|
||
|
||
if (readiness.ready && !shouldReplayCurrentIdempotent) {
|
||
continue
|
||
}
|
||
|
||
const canUseRepairSql = Boolean(
|
||
readiness.targetExists &&
|
||
readiness.columnReady &&
|
||
readiness.columnDefaultReady &&
|
||
readiness.relationColumnReady &&
|
||
!readiness.dataReady &&
|
||
migration.repairSql,
|
||
)
|
||
const canUseRepairSqlForGuard = Boolean(
|
||
readiness.targetExists &&
|
||
readiness.columnReady &&
|
||
readiness.columnDefaultReady &&
|
||
readiness.relationColumnReady &&
|
||
readiness.dataReady &&
|
||
!readiness.guardReady &&
|
||
migration.repairSql,
|
||
)
|
||
const absolutePath = path.join(ROOT_DIR, migration.path)
|
||
await client.query(
|
||
canUseRepairSql || canUseRepairSqlForGuard
|
||
? migration.repairSql
|
||
: fs.readFileSync(absolutePath, 'utf8'),
|
||
)
|
||
await assertMigrationTargetReady(client, migration)
|
||
applied.push(migration.path)
|
||
}
|
||
|
||
const replayedIdempotentMigrations = applied.some((migrationPath) => {
|
||
const migration = migrations.find((item) => item.path === migrationPath)
|
||
return migration?.alwaysApply
|
||
})
|
||
if (!replayedIdempotentMigrations) {
|
||
return applied
|
||
}
|
||
|
||
for (const migration of migrations.filter((item) => !item.alwaysApply)) {
|
||
const readiness = await getMigrationApplyReadiness(client, migration)
|
||
if (readiness.ready) continue
|
||
|
||
const canUseRepairSql = Boolean(
|
||
readiness.targetExists &&
|
||
readiness.columnReady &&
|
||
readiness.columnDefaultReady &&
|
||
readiness.relationColumnReady &&
|
||
migration.repairSql,
|
||
)
|
||
const absolutePath = path.join(ROOT_DIR, migration.path)
|
||
await client.query(canUseRepairSql ? migration.repairSql : fs.readFileSync(absolutePath, 'utf8'))
|
||
await assertMigrationTargetReady(client, migration)
|
||
if (!applied.includes(migration.path)) {
|
||
applied.push(migration.path)
|
||
}
|
||
}
|
||
return applied
|
||
}
|
||
|
||
async function getMigrationApplyReadiness(client, migration) {
|
||
const targetRelations = getMigrationTargetRelations(migration)
|
||
const targetExists = (
|
||
await Promise.all(targetRelations.map((relationName) => relationExists(client, relationName)))
|
||
).every(Boolean)
|
||
const columnReady = !migration.columnCheck ||
|
||
await columnExists(client, migration.columnCheck.tableName, migration.columnCheck.columnName)
|
||
const columnDefaultReady = await migrationColumnDefaultChecksPass(client, migration)
|
||
const guardReady = await migrationGuardChecksPass(client, migration)
|
||
const relationColumnReady = await migrationRelationColumnChecksPass(client, migration)
|
||
const dataReady = targetExists
|
||
? await migrationDataChecksPass(client, migration)
|
||
: false
|
||
return {
|
||
targetExists,
|
||
columnReady,
|
||
columnDefaultReady,
|
||
guardReady,
|
||
relationColumnReady,
|
||
dataReady,
|
||
ready:
|
||
targetExists &&
|
||
columnReady &&
|
||
columnDefaultReady &&
|
||
guardReady &&
|
||
relationColumnReady &&
|
||
dataReady,
|
||
}
|
||
}
|
||
|
||
async function migrationColumnDefaultChecksPass(client, migration) {
|
||
if (!Array.isArray(migration.columnDefaultChecks) || migration.columnDefaultChecks.length === 0) {
|
||
return true
|
||
}
|
||
const results = await Promise.all(
|
||
migration.columnDefaultChecks.map((check) =>
|
||
columnDefaultMatches(client, check.tableName, check.columnName, check.expectedDefault),
|
||
),
|
||
)
|
||
return results.every(Boolean)
|
||
}
|
||
|
||
async function migrationGuardChecksPass(client, migration) {
|
||
if (!Array.isArray(migration.guardChecks) || migration.guardChecks.length === 0) {
|
||
return true
|
||
}
|
||
const results = await Promise.all(migration.guardChecks.map((check) => guardExists(client, check)))
|
||
return results.every(Boolean)
|
||
}
|
||
|
||
async function migrationRelationColumnChecksPass(client, migration) {
|
||
if (!Array.isArray(migration.relationColumnChecks) || migration.relationColumnChecks.length === 0) {
|
||
return true
|
||
}
|
||
const results = []
|
||
for (const check of migration.relationColumnChecks) {
|
||
for (const columnName of check.columnNames) {
|
||
results.push(await relationColumnExists(client, check.relationName, columnName))
|
||
}
|
||
}
|
||
return results.every(Boolean)
|
||
}
|
||
|
||
async function migrationDataChecksPass(client, migration) {
|
||
if (!Array.isArray(migration.dataChecks) || migration.dataChecks.length === 0) {
|
||
return true
|
||
}
|
||
const results = []
|
||
for (const check of migration.dataChecks) {
|
||
results.push(await dataCheckPasses(client, check))
|
||
}
|
||
return results.every(Boolean)
|
||
}
|
||
|
||
module.exports = {
|
||
REQUIRED_MIGRATIONS,
|
||
LEGACY_NON_STANDARD_PRISMA_MIGRATION_DIRS,
|
||
applyMissingMigrationFiles,
|
||
assertMigrationTargetReady,
|
||
classifyAppliedMigrationPaths,
|
||
columnDefaultMatches,
|
||
columnExists,
|
||
dataCheckPasses,
|
||
guardExists,
|
||
hasExplicitReadinessChecks,
|
||
getPendingStructureMigrationPaths,
|
||
getMigrationTargetRelations,
|
||
getMigrationTimestamp,
|
||
relationColumnExists,
|
||
relationExists,
|
||
validateRequiredMigrationCatalog,
|
||
validatePrismaMigrationDirectories,
|
||
}
|