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).
};