deckhearth/migrations/1781442175729_normalize-lorcana-set-codes.js
Randall Stillwell ec9bb2b93e feat(catalog): unified multi-game bulk sync + schema map update
Ship Pokemon and Lorcana bulk import libs/scripts, unified weekly cron
sync across MTG/Pokemon/Lorcana with per-game error isolation and
catalog_sync_log telemetry. Admin UI adds Unified/Incremental/Bulk MTG modes.

- Rename reconcile migrations to 1781442330* timestamps so they apply
  after bulk-data migrations without node-pg-migrate ordering conflicts
- Add Lorcana set-code normalization + orphan cleanup migrations
- Drop stricter user_cards_user_card_unique (keep 3-column foil unique)
- Update docs/SCHEMA_MAP.md for tags, card_tags, catalog_sync_log, bulk columns

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 08:22:19 -05:00

119 lines
3.4 KiB
JavaScript

/**
* Normalize legacy Lorcana set_code values and scryfall_id (external id) shapes
* so bulk import from lorcana-api.com upserts cleanly instead of inserting duplicates.
*
* Legacy imports used numeric set codes (1/2/3) or lowercase abbreviations (tfc/rotf/ink).
* The bulk API uses Set_ID values TFC, ROF, INK, etc., and Unique_ID like TFC-001.
*/
export const shorthands = undefined;
export const up = (pgm) => {
pgm.sql(`
UPDATE cards
SET game = 'Lorcana'
WHERE game ILIKE 'lorcana' AND game <> 'Lorcana';
UPDATE cards
SET set_code = 'TFC', set_name = 'The First Chapter'
WHERE game = 'Lorcana' AND set_code IN ('1', 'tfc', 'TFC');
UPDATE cards
SET set_code = 'ROF', set_name = 'Rise of the Floodborn'
WHERE game = 'Lorcana' AND set_code IN ('2', 'rotf', 'ROF');
UPDATE cards
SET set_code = 'INK', set_name = 'Into the Inklands'
WHERE game = 'Lorcana' AND set_code IN ('3', 'ink', 'INK', 'ITI');
UPDATE cards
SET set_code = 'TFC', set_name = 'The First Chapter'
WHERE game = 'Lorcana'
AND set_code IS NULL
AND set_name ILIKE '%first chapter%';
UPDATE cards
SET set_code = 'ROF', set_name = 'Rise of the Floodborn'
WHERE game = 'Lorcana'
AND set_code IS NULL
AND set_name ILIKE '%rise of the floodborn%';
UPDATE cards
SET set_code = 'INK', set_name = 'Into the Inklands'
WHERE game = 'Lorcana'
AND set_code IS NULL
AND set_name ILIKE '%into the inklands%';
`);
// Drop legacy duplicate rows when the canonical Unique_ID row already exists.
pgm.sql(`
DELETE FROM cards AS dup
WHERE dup.game = 'Lorcana'
AND dup.set_code IS NOT NULL
AND TRIM(dup.card_number) <> ''
AND dup.scryfall_id IS DISTINCT FROM (
dup.set_code || '-' || LPAD(
REGEXP_REPLACE(TRIM(dup.card_number), '/.*$', ''),
3,
'0'
)
)
AND EXISTS (
SELECT 1
FROM cards AS keeper
WHERE keeper.scryfall_id = dup.set_code || '-' || LPAD(
REGEXP_REPLACE(TRIM(dup.card_number), '/.*$', ''),
3,
'0'
)
);
`);
// Keep one row per set + collector number before scryfall_id unification.
pgm.sql(`
DELETE FROM cards AS dup
WHERE dup.game = 'Lorcana'
AND dup.set_code IS NOT NULL
AND TRIM(dup.card_number) <> ''
AND dup.id <> (
SELECT MIN(peer.id)
FROM cards AS peer
WHERE peer.game = 'Lorcana'
AND peer.set_code = dup.set_code
AND REGEXP_REPLACE(TRIM(peer.card_number), '/.*$', '') = REGEXP_REPLACE(TRIM(dup.card_number), '/.*$', '')
);
`);
// Unify remaining legacy scryfall_id values to Unique_ID shape (SET-NNN).
pgm.sql(`
UPDATE cards AS c
SET scryfall_id = c.set_code || '-' || LPAD(
REGEXP_REPLACE(TRIM(c.card_number), '/.*$', ''),
3,
'0'
)
WHERE c.game = 'Lorcana'
AND c.set_code IS NOT NULL
AND TRIM(c.card_number) <> ''
AND c.scryfall_id IS DISTINCT FROM (
c.set_code || '-' || LPAD(
REGEXP_REPLACE(TRIM(c.card_number), '/.*$', ''),
3,
'0'
)
)
AND NOT EXISTS (
SELECT 1
FROM cards AS other
WHERE other.scryfall_id = c.set_code || '-' || LPAD(
REGEXP_REPLACE(TRIM(c.card_number), '/.*$', ''),
3,
'0'
)
);
`);
};
export const down = () => {
throw new Error('Irreversible data migration');
};