export const id = '20260927-member-lookup-key';
export const name = 'Member: generic lookup slot (lookup_key) replacing credential_id';
/**
* `Member.credential_id` (20260926) war typ-gebunden benannt: eine Spalte, die
* genau ein Objekttyp mit wenigen Zeilen benutzt. Diese Migration ersetzt sie
* durch einen **generischen Lookup-Slot**.
*
* ## Konvention (neu, hier festgehalten)
*
* `Member.lookup_key` ist der **eine opake externe Schlüssel** eines Objekts:
* ein Wert, der von außen kommt (Client, Registrierung, Import) und über den
* das Objekt gesucht wird — NICHT die Objekt-UID.
*
* - **Ein Slot pro Objekt, global eindeutig.** Die Bedeutung ergibt sich aus
* `ObjectBase.Type`, nicht aus dem Spaltennamen. Kein Typ-spezifisches Feld,
* keine zweite Spalte für den nächsten Typ.
* - **Die Werte sind präfixiert** (`rc_…` für Runner-Credentials). Deshalb
* genügt ein globaler UNIQUE statt `(Type, key)` — Letzteres bräuchte `Type`
* in `Member` (Denormalisierung) und bringt für einen Zufallswert nichts.
* - **Kein anderer Zweck.** `Member.Data` trägt den heißen Zustand (Heartbeat,
* Capacity); `lookup_key` trägt nur den Suchschlüssel. Beides zu vermischen
* macht den Unique-Konflikt zur Zufallsfehlerquelle an unerwarteten Stellen.
*
* Die Rolle ist damit `SortName`/`FullTextIndex`/`PhonetikIndex`/`Geo`
* gleichgestellt: eine ausgezogene Spalte, die Lookup oder Sort dient — die
* Member-Konvention (080-Workspaces/040-Ist-Zustand: „heißer Zustand im
* Data-JSON, ausgezogene Spalten nur für Lookup/Sort").
*
* ## Ablauf
*
* 1. `lookup_key` an `Member` anlegen (varchar(128): ein generischer Slot
* sollte nicht auf die Länge seines ersten Nutzers dimensioniert sein).
* 2. UNIQUE `member_lookup_key_uq` anlegen.
* 3. Bestehende `credential_id`-Werte übernehmen, ihren Index und die Spalte
* entfernen.
*
* Idempotent: eine frische DB (initTables.sql kennt bereits `lookup_key`, kein
* `credential_id`) überspringt alles; eine laufende DB landet im selben
* Zustand. Hinweis: `20260926` legt `credential_id` weiterhin an — auf einem
* frischen Boot entsteht sie leer und wird hier wieder entfernt, weil die
* Migration bereits appliziert ist und nicht mehr geändert werden darf.
*/
const hasColumn = async (query, table, column) => {
const rows = await query(
`SELECT COUNT(*) AS c FROM information_schema.columns
WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?`,
[table, column],
);
return Number(rows[0]?.c) > 0;
};
const hasIndex = async (query, table, indexName) => {
const rows = await query(
`SELECT COUNT(*) AS c FROM information_schema.statistics
WHERE table_schema = DATABASE() AND table_name = ? AND index_name = ?`,
[table, indexName],
);
return Number(rows[0]?.c) > 0;
};
export const migrate = async ({ query }) => {
// ------------------------------------------- 1. Generischer Lookup-Slot
if (!(await hasColumn(query, 'Member', 'lookup_key'))) {
await query(
`ALTER TABLE \`Member\` ADD COLUMN \`lookup_key\` varchar(128) DEFAULT NULL`,
[],
);
}
if (!(await hasIndex(query, 'Member', 'member_lookup_key_uq'))) {
await query(
`ALTER TABLE \`Member\` ADD UNIQUE KEY \`member_lookup_key_uq\` (\`lookup_key\`)`,
[],
);
}
// ------------------------------- 2. Bestand aus credential_id übernehmen
// Die alte Spalte war UNIQUE, also kollidiert die Übernahme nicht. Erst
// kopieren, dann Index, dann Spalte — in dieser Reihenfolge, weil der
// UNIQUE auf lookup_key die Kopie sonst nicht absichern würde.
if (await hasColumn(query, 'Member', 'credential_id')) {
await query(
`UPDATE \`Member\` SET \`lookup_key\` = \`credential_id\`
WHERE \`credential_id\` IS NOT NULL AND \`credential_id\` <> ''`,
[],
);
if (await hasIndex(query, 'Member', 'member_credential_id_uq')) {
await query(`ALTER TABLE \`Member\` DROP INDEX \`member_credential_id_uq\``, []);
}
await query(`ALTER TABLE \`Member\` DROP COLUMN \`credential_id\``, []);
}
};