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 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b3ef470b0
commit
90c2f97c5b
2 files changed
+156
-7
No files matched your search
@@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repair for the 2026-10-27 --delete-dropped run: the RENAME_MAP entries
|
||||||
|
* 'Government' -> '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:
|
||||||
|
* - #<main> "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";
|
||||||
@@ -62,8 +62,13 @@ final class AlignMarketRecordsCommand extends Command
|
|||||||
// sub-markets
|
// sub-markets
|
||||||
'Aviation' => 'Airports',
|
'Aviation' => 'Airports',
|
||||||
'Casinos & Sports Books' => 'Casinos',
|
'Casinos & Sports Books' => 'Casinos',
|
||||||
'Education' => 'Education Communications',
|
// 'Education' => 'Education Communications' and
|
||||||
'Government' => 'Government & Agencies',
|
// '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',
|
||||||
'Defense Military' => 'Defense & Security',
|
'Defense Military' => 'Defense & Security',
|
||||||
'Emergency Services' => 'Emergency Operation Centres',
|
'Emergency Services' => 'Emergency Operation Centres',
|
||||||
@@ -81,12 +86,14 @@ final class AlignMarketRecordsCommand extends Command
|
|||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)');
|
$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
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
Bootstrap::initializeBackendAuthentication();
|
Bootstrap::initializeBackendAuthentication();
|
||||||
$apply = (bool)$input->getOption('apply');
|
$apply = (bool)$input->getOption('apply');
|
||||||
|
$deleteDropped = (bool)$input->getOption('delete-dropped');
|
||||||
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
$pool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||||
$pages = $pool->getConnectionForTable('pages');
|
$pages = $pool->getConnectionForTable('pages');
|
||||||
$markets = $pool->getConnectionForTable(self::TABLE);
|
$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']));
|
$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) {
|
foreach ($records as $record) {
|
||||||
if (isset($claimedRecords[(int)$record['uid']])) {
|
if (isset($claimedRecords[(int)$record['uid']])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$stories = $this->stories($pool, (int)$record['uid']);
|
$stories = $this->stories($pool, (int)$record['uid']);
|
||||||
$output->writeln(sprintf(' <comment>! DROPPED by the agency: #%d "%s"%s - untouched, %d story relation(s)%s</comment>',
|
$verb = $deleteDropped ? ($apply ? 'DELETING' : 'would DELETE') : 'untouched';
|
||||||
|
$output->writeln(sprintf(' <comment>! DROPPED by the agency: #%d "%s"%s - %s, %d story relation(s)%s</comment>',
|
||||||
(int)$record['uid'], (string)$record['title'],
|
(int)$record['uid'], (string)$record['title'],
|
||||||
((int)$record['hidden'] === 1) ? ' (hidden)' : '',
|
((int)$record['hidden'] === 1) ? ' (hidden)' : '',
|
||||||
count($stories), $stories === [] ? '' : ':'));
|
$verb, count($stories), $stories === [] ? '' : ' (badge disappears, story reports unresolved on re-runs):'));
|
||||||
foreach ($stories as $story) {
|
foreach ($stories as $story) {
|
||||||
$output->writeln(' - ' . $story);
|
$output->writeln(' - ' . $story);
|
||||||
}
|
}
|
||||||
|
if ($deleteDropped) {
|
||||||
|
$cmdmap[self::TABLE][(int)$record['uid']]['delete'] = 1;
|
||||||
|
$droppedUids[] = (int)$record['uid'];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$apply) {
|
if (!$apply) {
|
||||||
$output->writeln("\nDRY RUN - nothing written. Add --apply to write.");
|
$output->writeln("\nDRY RUN - nothing written. Add --apply to write.");
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
if ($datamap === []) {
|
if ($datamap === [] && $cmdmap === []) {
|
||||||
$output->writeln('Nothing to change.');
|
$output->writeln('Nothing to change.');
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||||
$dataHandler->start($datamap, []);
|
$dataHandler->start($datamap, $cmdmap);
|
||||||
$dataHandler->process_datamap();
|
$dataHandler->process_datamap();
|
||||||
|
$dataHandler->process_cmdmap();
|
||||||
if ($dataHandler->errorLog !== []) {
|
if ($dataHandler->errorLog !== []) {
|
||||||
$output->writeln('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
|
$output->writeln('<error>DataHandler: ' . implode(' | ', $dataHandler->errorLog) . '</error>');
|
||||||
return Command::FAILURE;
|
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('<info>%d record(s) soft-deleted, %d MM row(s) removed.</info>', count($droppedUids), $removed));
|
||||||
|
}
|
||||||
$output->writeln('<info>Done. Reindex Solr and flush the cache afterwards.</info>');
|
$output->writeln('<info>Done. Reindex Solr and flush the cache afterwards.</info>');
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user