migrate: backfill old {{uuid}} placeholders to new {{label.w/o:uuid}} format
Runs at startup (idempotent) — only touches rows that still contain bare
{{uuid}} placeholders. Looks up each UUID in words first, then objects,
and rewrites to {{label.w:uuid}} or {{label.o:uuid}} accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -494,7 +494,93 @@ async function migrate() {
|
||||
AND bbox_x IS NULL
|
||||
`).catch(() => {});
|
||||
|
||||
// ── Migrate old {{uuid}} placeholders → new {{label.w:uuid}} / {{label.o:uuid}} ──
|
||||
await migratePlaceholders();
|
||||
|
||||
console.log('Migration complete');
|
||||
}
|
||||
|
||||
// UUID regex — matches bare {{uuid}} but NOT already-migrated {{label.w:uuid}}
|
||||
const UUID_RE = /\{\{([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\}\}/gi;
|
||||
|
||||
async function migratePlaceholders() {
|
||||
const textCols = {
|
||||
questions: ['sentence_de', 'sentence_en', 'sentence_sv'],
|
||||
statements: [
|
||||
'positive_sentence_de', 'positive_sentence_en', 'positive_sentence_sv',
|
||||
'negative_sentence_de', 'negative_sentence_en', 'negative_sentence_sv',
|
||||
],
|
||||
};
|
||||
|
||||
const uuidSet = new Set();
|
||||
const affected = {};
|
||||
|
||||
for (const [table, cols] of Object.entries(textCols)) {
|
||||
const whereClause = cols
|
||||
.map(c => `${c} ~ '\\{\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}\\}'`)
|
||||
.join(' OR ');
|
||||
const { rows } = await query(`SELECT id, ${cols.join(', ')} FROM ${table} WHERE ${whereClause}`);
|
||||
if (rows.length) {
|
||||
affected[table] = rows;
|
||||
rows.forEach(row => cols.forEach(col => {
|
||||
for (const m of (row[col] || '').matchAll(UUID_RE)) uuidSet.add(m[1]);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (uuidSet.size === 0) return;
|
||||
|
||||
const uuids = [...uuidSet];
|
||||
const labelMap = {};
|
||||
|
||||
// Words first
|
||||
const { rows: wordRows } = await query(
|
||||
`SELECT id, titel_de, titel_en FROM words WHERE id = ANY($1::uuid[])`, [uuids]
|
||||
);
|
||||
wordRows.forEach(w => { labelMap[w.id] = { label: w.titel_de || w.titel_en || 'Wort', type: 'w' }; });
|
||||
|
||||
// Remaining → objects
|
||||
const missing = uuids.filter(id => !labelMap[id]);
|
||||
if (missing.length) {
|
||||
const { rows: objRows } = await query(
|
||||
`SELECT o.id, w.titel_de, w.titel_en
|
||||
FROM objects o
|
||||
LEFT JOIN object_words ow ON ow.object_id = o.id
|
||||
LEFT JOIN words w ON w.id = ow.word_id
|
||||
WHERE o.id = ANY($1::uuid[])`, [missing]
|
||||
);
|
||||
const seen = new Set();
|
||||
objRows.forEach(r => {
|
||||
if (!seen.has(r.id)) {
|
||||
seen.add(r.id);
|
||||
labelMap[r.id] = { label: r.titel_de || r.titel_en || 'Objekt', type: 'o' };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// UPDATE affected rows
|
||||
for (const [table, rows] of Object.entries(affected)) {
|
||||
const cols = textCols[table];
|
||||
for (const row of rows) {
|
||||
const updates = {};
|
||||
for (const col of cols) {
|
||||
const text = row[col];
|
||||
if (!text) continue;
|
||||
const replaced = text.replace(UUID_RE, (_, uuid) => {
|
||||
const info = labelMap[uuid];
|
||||
return info ? `{{${info.label}.${info.type}:${uuid}}}` : `{{${uuid}}}`;
|
||||
});
|
||||
if (replaced !== text) updates[col] = replaced;
|
||||
}
|
||||
if (Object.keys(updates).length) {
|
||||
const setClauses = Object.keys(updates).map((k, i) => `${k} = $${i + 2}`).join(', ');
|
||||
await query(`UPDATE ${table} SET ${setClauses} WHERE id = $1`, [row.id, ...Object.values(updates)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const count = Object.values(affected).reduce((s, r) => s + r.length, 0);
|
||||
if (count > 0) console.log(`Placeholder migration: updated ${count} rows`);
|
||||
}
|
||||
|
||||
module.exports = migrate;
|
||||
|
||||
Reference in New Issue
Block a user