import type Database from 'better-sqlite3'; export function migrateNativeInstances(handle: Database.Database): void { migrateUnplacedCreationPort(handle); handle.exec(` CREATE TABLE IF NOT EXISTS native_creation_intents ( deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id) ON DELETE CASCADE, selection TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS native_instances ( id TEXT PRIMARY KEY, server TEXT NOT NULL, host_id TEXT NOT NULL, generation INTEGER NOT NULL DEFAULT 1, library_id TEXT NOT NULL, image TEXT NOT NULL, bind_address TEXT NOT NULL DEFAULT '0.0.0.0', port INTEGER NOT NULL, public_address TEXT, public_port INTEGER, created_at TEXT NOT NULL, retired_at TEXT ); CREATE INDEX IF NOT EXISTS native_instance_host ON native_instances(host_id) WHERE retired_at IS NULL; CREATE UNIQUE INDEX IF NOT EXISTS native_instance_live_name ON native_instances(server) WHERE retired_at IS NULL; CREATE TABLE IF NOT EXISTS native_provisioning ( deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id), instance_id TEXT NOT NULL REFERENCES native_instances(id), generation INTEGER NOT NULL, stage_operation_id TEXT, operation_id TEXT, files_json TEXT, applied_at TEXT, verified_at TEXT ); CREATE INDEX IF NOT EXISTS native_provisioning_instance ON native_provisioning(instance_id); CREATE TABLE IF NOT EXISTS provisioning_safety_overrides ( deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id) ON DELETE CASCADE, actor_id TEXT NOT NULL, actor_name TEXT NOT NULL, confirmed_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS ec_host_migrations ( deployment_id TEXT PRIMARY KEY REFERENCES server_deployments(id), source_host_id TEXT NOT NULL, instance_id TEXT NOT NULL REFERENCES native_instances(id), target_host_id TEXT, target_port INTEGER, target_bind TEXT, remove_operation_id TEXT, removed_at TEXT ); CREATE INDEX IF NOT EXISTS ec_host_migrations_source ON ec_host_migrations(source_host_id); CREATE INDEX IF NOT EXISTS ec_host_migrations_target ON ec_host_migrations(target_host_id); CREATE TABLE IF NOT EXISTS native_creation_cancellations ( creation_id TEXT PRIMARY KEY REFERENCES server_deployments(id), cleanup_id TEXT NOT NULL UNIQUE REFERENCES server_deployments(id) ); `); } /** Accepted native creations receive their host port only when provisioning places them. */ function migrateUnplacedCreationPort(handle: Database.Database): void { const columns = handle.prepare('PRAGMA table_info(server_creation_jobs)').all() as { name: string; notnull: number }[]; if (!columns.find(column => column.name === 'port')?.notnull) return; const { sql } = handle.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='server_creation_jobs'").get() as { sql: string }; const replacement = sql.replace(/\bport\s+INTEGER\s+NOT\s+NULL\b/i, 'port INTEGER'); if (replacement === sql) throw new Error('Could not migrate unplaced creation ports.'); const dependent = handle.prepare("SELECT sql FROM sqlite_master WHERE tbl_name='server_creation_jobs' AND type IN ('index','trigger') AND sql IS NOT NULL") .all() as { sql: string }[]; const foreignKeys = handle.pragma('foreign_keys', { simple: true }); handle.pragma('foreign_keys = OFF'); try { handle.transaction(() => { handle.exec(replacement.replace('server_creation_jobs', 'server_creation_jobs_unplaced')); const names = columns.map(column => `"${column.name.replaceAll('"', '""')}"`).join(', '); handle.exec(`INSERT INTO server_creation_jobs_unplaced(rowid,${names}) SELECT rowid,${names} FROM server_creation_jobs; DROP TABLE server_creation_jobs; ALTER TABLE server_creation_jobs_unplaced RENAME TO server_creation_jobs;`); for (const object of dependent) handle.exec(object.sql); if ((handle.pragma('foreign_key_check') as unknown[]).length) throw new Error('Unplaced creation migration found broken references.'); })(); } finally { handle.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`); } }