From 3147191f55ce893cdd23d95211dfc9d8dd409f59 Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 26 May 2026 15:00:04 +0200 Subject: [PATCH] migrate: backfill old {{uuid}} placeholders to new {{label.w/o:uuid}} format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/db-migrate.js | 86 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/db-migrate.js b/src/db-migrate.js index 09721ac..37f6ad6 100644 --- a/src/db-migrate.js +++ b/src/db-migrate.js @@ -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;