From 90c2f97c5b5a4c4222d37e2c68571bbe8d2dda40 Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Tue, 27 Oct 2026 11:28:26 +0100 Subject: [PATCH] Market records: delete the dropped legacy set; defuse the rename map - vitec:align-market-records gains --delete-dropped: soft-deletes the records the agency dropped from the taxonomy, including their MM rows (DataHandler does not clean the foreign side on delete). Decision: all nine legacy records go, including Healthcare #42 - the Overlake story loses its badge on purpose and reports as unresolved on future importer runs - the run exposed a regression: the RENAME_MAP entries Government -> Government & Agencies and Education -> Education Communications are not idempotent because both old names double as NEW main-market names. The re-run hijacked the main records (stories and teaser travelled along), created empty replacements and deleted the real sub-market records as unclaimed - repair: migrations/fix_market_rename_regression.php (dry-run default, guarded) swaps the pairs back - main records keep their stories and workbook teasers, the fresh empties take over the sub-market roles; applied and verified live (marketlist 11/11 with main detail pages, badge distribution back to 20/9/9/8/6/4/1, no legacy names left) - the two non-idempotent entries are removed from RENAME_MAP with a comment: a rename mapping whose old name is reused by the new taxonomy must not survive its one-time run Co-Authored-By: Claude Fable 5 --- migrations/fix_market_rename_regression.php | 121 ++++++++++++++++++ .../Command/AlignMarketRecordsCommand.php | 42 +++++- 2 files changed, 156 insertions(+), 7 deletions(-) create mode 100644 migrations/fix_market_rename_regression.php diff --git a/migrations/fix_market_rename_regression.php b/migrations/fix_market_rename_regression.php new file mode 100644 index 0000000..d2b36c5 --- /dev/null +++ b/migrations/fix_market_rename_regression.php @@ -0,0 +1,121 @@ + 'Government & Agencies' and 'Education' -> 'Education + * Communications' are NOT idempotent (the old names double as the NEW main + * market names). The re-run therefore renamed the MAIN records (#50/#20, + * stories and teaser travelled along), created fresh empty "Government" / + * "Education" records, and pass 4 deleted the original (empty) sub-market + * records as unclaimed. + * + * This script swaps everything back: + * - #
"Government & Agencies" -> title Government, slug government, + * detail_page = page /markets/government + * - the empty record titled "Government" (created today) -> title + * Government & Agencies, slug government-agencies, detail_page = + * page /markets/government/government-and-agencies + * - same pair for Education / Education Communications + * + * Plain PDO (pattern migrations/check_bg.php). Dry-run by default. + * + * Usage: php migrations/fix_market_rename_regression.php [--apply] + */ + +error_reporting(E_ALL); +ini_set('display_errors', '1'); + +$apply = in_array('--apply', $argv, true); + +$root = null; +$dir = __DIR__; +for ($i = 0; $i < 5; $i++) { + if (is_file($dir . '/config/system/settings.php')) { + $root = $dir; + break; + } + $dir = dirname($dir); +} +if ($root === null) { + exit("Could not locate project root above " . __DIR__ . "\n"); +} +$settings = require $root . '/config/system/settings.php'; +$db = $settings['DB']['Connections']['Default']; +$pdo = new PDO( + sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $db['host'] ?? 'localhost', $db['port'] ?? 3306, $db['dbname'] ?? ''), + $db['user'] ?? '', + $db['password'] ?? '', + [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC] +); + +function pageUid(PDO $pdo, string $slug): int +{ + $stmt = $pdo->prepare('SELECT uid FROM pages WHERE deleted = 0 AND slug = ?'); + $stmt->execute([$slug]); + return (int)($stmt->fetchColumn() ?: 0); +} + +function record(PDO $pdo, string $title): ?array +{ + $stmt = $pdo->prepare('SELECT uid, title, slug, detail_page, + (SELECT COUNT(*) FROM tx_vitec_usecase_market_mm mm WHERE mm.uid_foreign = m.uid) AS stories + FROM tx_vitec_domain_model_market m WHERE deleted = 0 AND title = ?'); + $stmt->execute([$title]); + $rows = $stmt->fetchAll(); + if (count($rows) !== 1) { + echo " !! " . count($rows) . " live record(s) titled \"$title\" - manual check needed\n"; + return null; + } + return $rows[0]; +} + +$pairs = [ + ['mainTitle' => 'Government', 'mainSlug' => 'government', 'mainPage' => '/markets/government', + 'subTitle' => 'Government & Agencies', 'subSlug' => 'government-agencies', 'subPage' => '/markets/government/government-and-agencies'], + ['mainTitle' => 'Education', 'mainSlug' => 'education', 'mainPage' => '/markets/education', + 'subTitle' => 'Education Communications', 'subSlug' => 'education-communications', 'subPage' => '/markets/education/education-communications'], +]; + +$updates = []; +foreach ($pairs as $pair) { + echo "=== {$pair['mainTitle']} ===\n"; + // the record carrying the stories currently holds the SUB title + $hijacked = record($pdo, $pair['subTitle']); // ex-main, renamed today + $fresh = record($pdo, $pair['mainTitle']); // empty, created today + if ($hijacked === null || $fresh === null) { + continue; + } + $mainPageUid = pageUid($pdo, $pair['mainPage']); + $subPageUid = pageUid($pdo, $pair['subPage']); + if (!$mainPageUid || !$subPageUid) { + echo " !! page lookup failed ({$pair['mainPage']} -> $mainPageUid, {$pair['subPage']} -> $subPageUid)\n"; + continue; + } + echo sprintf(" #%d \"%s\" (%d stories, slug %s, detail_page %d) -> BACK TO \"%s\" / %s / page %d\n", + $hijacked['uid'], $hijacked['title'], $hijacked['stories'], $hijacked['slug'], $hijacked['detail_page'], + $pair['mainTitle'], $pair['mainSlug'], $mainPageUid); + echo sprintf(" #%d \"%s\" (%d stories, slug %s, detail_page %d) -> BECOMES \"%s\" / %s / page %d\n", + $fresh['uid'], $fresh['title'], $fresh['stories'], $fresh['slug'], $fresh['detail_page'], + $pair['subTitle'], $pair['subSlug'], $subPageUid); + if ((int)$fresh['stories'] > 0) { + echo " !! the fresh record unexpectedly has stories - aborting this pair\n"; + continue; + } + $updates[] = [(int)$hijacked['uid'], $pair['mainTitle'], $pair['mainSlug'], $mainPageUid]; + $updates[] = [(int)$fresh['uid'], $pair['subTitle'], $pair['subSlug'], $subPageUid]; +} + +if (!$apply) { + exit("\nDRY RUN - add --apply to write.\n"); +} +if ($updates === []) { + exit("\nNothing to update.\n"); +} +$stmt = $pdo->prepare('UPDATE tx_vitec_domain_model_market SET title = ?, slug = ?, detail_page = ?, tstamp = UNIX_TIMESTAMP() WHERE uid = ?'); +foreach ($updates as [$uid, $title, $slug, $pageUid]) { + $stmt->execute([$title, $slug, $pageUid, $uid]); + echo " updated #$uid -> \"$title\"\n"; +} +echo "Done. Flush the cache, then verify the /markets marketlist and the story badges.\n"; diff --git a/packages/vitec/Classes/Command/AlignMarketRecordsCommand.php b/packages/vitec/Classes/Command/AlignMarketRecordsCommand.php index 938c6b4..91db37a 100644 --- a/packages/vitec/Classes/Command/AlignMarketRecordsCommand.php +++ b/packages/vitec/Classes/Command/AlignMarketRecordsCommand.php @@ -62,8 +62,13 @@ final class AlignMarketRecordsCommand extends Command // sub-markets 'Aviation' => 'Airports', 'Casinos & Sports Books' => 'Casinos', - 'Education' => 'Education Communications', - 'Government' => 'Government & Agencies', + // 'Education' => 'Education Communications' and + // 'Government' => 'Government & Agencies' were REMOVED 2026-10-27: + // both old names double as NEW main-market names, so a re-run + // hijacks the main record (rename is not idempotent when the old + // title is reused by the new taxonomy). The one-time renames ran on + // 2026-10-26; the collateral of the 10-27 re-run was repaired by + // migrations/fix_market_rename_regression.php. 'Defense/ Military' => 'Defense & Security', 'Defense Military' => 'Defense & Security', 'Emergency Services' => 'Emergency Operation Centres', @@ -81,12 +86,14 @@ final class AlignMarketRecordsCommand extends Command protected function configure(): void { $this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)'); + $this->addOption('delete-dropped', null, InputOption::VALUE_NONE, 'Soft-delete the records the agency dropped (incl. their MM rows)'); } protected function execute(InputInterface $input, OutputInterface $output): int { Bootstrap::initializeBackendAuthentication(); $apply = (bool)$input->getOption('apply'); + $deleteDropped = (bool)$input->getOption('delete-dropped'); $pool = GeneralUtility::makeInstance(ConnectionPool::class); $pages = $pool->getConnectionForTable('pages'); $markets = $pool->getConnectionForTable(self::TABLE); @@ -190,36 +197,57 @@ final class AlignMarketRecordsCommand extends Command $output->writeln(sprintf(' + CREATE "%s" (level %d, detail_page %d)', $target['title'], $target['level'], $target['pageUid'])); } - // pass 4: records without a target - the editorial decision list + // pass 4: records without a target. Default: the editorial decision + // list. With --delete-dropped (decision Olli 2026-10-27): soft-delete + // them, including their MM rows - a story whose only badge was such a + // record simply loses it and reports as unresolved on future + // importer runs (that WAS the decision for Overlake / Healthcare #42). + $cmdmap = []; + $droppedUids = []; foreach ($records as $record) { if (isset($claimedRecords[(int)$record['uid']])) { continue; } $stories = $this->stories($pool, (int)$record['uid']); - $output->writeln(sprintf(' ! DROPPED by the agency: #%d "%s"%s - untouched, %d story relation(s)%s', + $verb = $deleteDropped ? ($apply ? 'DELETING' : 'would DELETE') : 'untouched'; + $output->writeln(sprintf(' ! DROPPED by the agency: #%d "%s"%s - %s, %d story relation(s)%s', (int)$record['uid'], (string)$record['title'], ((int)$record['hidden'] === 1) ? ' (hidden)' : '', - count($stories), $stories === [] ? '' : ':')); + $verb, count($stories), $stories === [] ? '' : ' (badge disappears, story reports unresolved on re-runs):')); foreach ($stories as $story) { $output->writeln(' - ' . $story); } + if ($deleteDropped) { + $cmdmap[self::TABLE][(int)$record['uid']]['delete'] = 1; + $droppedUids[] = (int)$record['uid']; + } } if (!$apply) { $output->writeln("\nDRY RUN - nothing written. Add --apply to write."); return Command::SUCCESS; } - if ($datamap === []) { + if ($datamap === [] && $cmdmap === []) { $output->writeln('Nothing to change.'); return Command::SUCCESS; } $dataHandler = GeneralUtility::makeInstance(DataHandler::class); - $dataHandler->start($datamap, []); + $dataHandler->start($datamap, $cmdmap); $dataHandler->process_datamap(); + $dataHandler->process_cmdmap(); if ($dataHandler->errorLog !== []) { $output->writeln('DataHandler: ' . implode(' | ', $dataHandler->errorLog) . ''); return Command::FAILURE; } + if ($droppedUids !== []) { + // DataHandler does not clean foreign-side MM rows on delete + $mmConn = $pool->getConnectionForTable(self::MM); + $removed = 0; + foreach ($droppedUids as $uid) { + $removed += (int)$mmConn->delete(self::MM, ['uid_foreign' => $uid]); + } + $output->writeln(sprintf('%d record(s) soft-deleted, %d MM row(s) removed.', count($droppedUids), $removed)); + } $output->writeln('Done. Reindex Solr and flush the cache afterwards.'); return Command::SUCCESS; }