Source: config/migrations/20260907-runner-companion.js

export const id = '20260907-runner-companion';
export const name = 'Runner companion table, drop members runtime tables and workspace/deployment objects';

/**
 * AP 1 (050-Migrationsplan): Die Mitglieder-Runtime-/Deployment-Ebene wird aus
 * members entfernt — Runners bleiben reine Topologie-Objekte (ObjectBase
 * Type='runner') mit einer Member-Muster-Companion-Zeile (`RunnerData`) für
 * heißen, nicht versionierten Zustand:
 *
 *   - credential_id/public_key  → gezogene Lookup-Spalten (Runner-Auth)
 *   - heartbeat/capacity        → Data-JSON (Topologie-Heartbeat)
 *
 * Bestehende Daten werden aus `runner_credentials`/`runner_runtime` übernommen.
 * Die 1:n-/Queue-/Transfer-Tabellen werden gedroppt; `workspace`/`deployment`-
 * Objekte + deren Links/Visible-Zeilen werden entfernt.
 *
 * KEINE ObjectBase/Links-Enum-MODIFY in dieser Migration: ObjectBase ist system-
 * versioniert, History-Partitionen enthalten alte Zeilen der entfernten Literale
 * (workspace/deployment/…) — ein MODIFY liefe in „Data truncated". Frische DBs
 * entstehen ohnehin ohne die Literale (die erzeugenden Migrationen sind aus der
 * Kette entfernt, vgl. 20260813-runner-object-types/20260827-tailnet-object-types);
 * bestehende dev-DBs behalten einen harmlosen Enum-Superset.
 *
 * Gilt für laufende dev-DBs (dbVersion) UND frische Boots (alphabetisch nach
 * 20260827-tailnet-object-types).
 */

const hasTable = async (query, tableName) => {
    const rows = await query(
        `SELECT COUNT(*) AS c FROM information_schema.tables
         WHERE table_schema = DATABASE() AND table_name = ?`,
        [tableName],
    );
    return Number(rows[0]?.c) > 0;
};

export const migrate = async ({ query }) => {
    // ---------------------------------------------------------------- RunnerData
    await query(`
        CREATE TABLE IF NOT EXISTS \`RunnerData\` (
            \`UID\` binary(16) NOT NULL,
            \`credential_id\` varchar(64) DEFAULT NULL,
            \`public_key\` text DEFAULT NULL,
            \`Data\` longtext NOT NULL,
            PRIMARY KEY (\`UID\`) USING BTREE,
            UNIQUE KEY \`RunnerData_cred_id_uq\` (\`credential_id\`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    `, []);

    // ----------------------------------------------------------------- data move
    // Bestehende Runner-Credentials + Runtime-Zustand in die Companion-Zeile.
    if (await hasTable(query, 'runner_credentials')) {
        await query(`
            INSERT IGNORE INTO RunnerData (UID, credential_id, public_key, Data)
            SELECT c.runner_uid, c.credential_id, c.public_key,
                   JSON_OBJECT(
                       'heartbeat',
                       COALESCE(r.heartbeat, JSON_OBJECT()),
                       'capacity',
                       COALESCE(r.capacity, JSON_OBJECT()),
                       'lastSeenAt',
                       DATE_FORMAT(r.last_seen_at, '%Y-%m-%dT%H:%i:%s.%fZ')
                   )
            FROM runner_credentials c
            LEFT JOIN runner_runtime r ON r.runner_uid = c.runner_uid
        `, []);
    }

    // Runner ohne Credential (z. B. angelegt, aber nie gexchanged) bekommen eine
    // leere Companion-Zeile, damit der Service einheitlich JOINen kann.
    await query(`
        INSERT IGNORE INTO RunnerData (UID, Data)
        SELECT ObjectBase.UID, JSON_OBJECT()
        FROM ObjectBase
        WHERE ObjectBase.Type = 'runner'
    `, []);

    // ------------------------------------------------------------- drop runtime
    for (const table of [
        'deployment_jobs',
        'deployment_events',
        'runner_leases',
        'workspace_transfers',
        'runner_preview_hosts',
        'deployment_workspace_state',
        'runner_registration_codes',
        'runner_credentials',
        'runner_runtime',
    ]) {
        await query(`DROP TABLE IF EXISTS \`${table}\``, []);
    }

    // ------------------------------------------------- delete workspace objects
    await query(`
        DELETE FROM Links
        WHERE Type IN ('workspaceRunner', 'workspaceLease', 'deploymentRunner')
    `, []);
    await query(`
        DELETE FROM Visible
        WHERE UID IN (
            SELECT UID FROM ObjectBase
            WHERE Type IN ('workspace', 'deployment', 'deploymentEndpoint')
        )
    `, []);
    await query(`
        DELETE FROM ObjectBase
        WHERE Type IN ('workspace', 'deployment', 'deploymentEndpoint')
    `, []);

    // Keine Enum-MODIFY hier (History-Partitionen — siehe Kopfkommentar).
};