import { execFile } from "child_process"; import { Connection, getConnection } from "./db"; import { connectionUrl, execSql } from "./sling"; import { resolveBinary } from "./binaries"; // Atlas needs an empty scratch database ("dev database") to plan diffs. // The usual `docker://mysql/8` dev-url spins up a throwaway container, but // its dial to host.docker.internal is broken on some Docker runtimes (e.g. // OrbStack: connections establish, then the handshake EOFs). Use a dedicated // scratch schema on the target server instead — no Docker needed. const DEV_SCHEMA = "sling_ui_atlas_dev"; function devDatabaseUrl(tgt: Connection): string { const user = encodeURIComponent(tgt.user); const pass = encodeURIComponent(tgt.password); return `mysql://${user}:${pass}@${tgt.host}:${tgt.port}/${DEV_SCHEMA}`; } // Create the scratch schema if missing and return its URL. Atlas keeps it // clean after each run. export function prepareDevDatabase(tgt: Connection): string { execSql(tgt, `CREATE DATABASE IF NOT EXISTS \`${DEV_SCHEMA}\``); return devDatabaseUrl(tgt); } function baseApplyArgs(srcUrl: string, tgtUrl: string, devUrl: string): string[] { return [ "schema", "apply", "--url", tgtUrl, "--to", srcUrl, "--dev-url", devUrl, ]; } export function buildApplyArgs( srcUrl: string, tgtUrl: string, devUrl: string ): string[] { return [...baseApplyArgs(srcUrl, tgtUrl, devUrl), "--auto-approve"]; } // atlas rejects --dry-run combined with --auto-approve. export function buildDryRunArgs( srcUrl: string, tgtUrl: string, devUrl: string ): string[] { return [...baseApplyArgs(srcUrl, tgtUrl, devUrl), "--dry-run"]; } function atlasBin(): string { return resolveBinary("atlas") ?? "atlas"; } export function schemaDiff( sourceConnId: number, targetConnId: number ): Promise<{ ok: boolean; sql: string }> { const src = getConnection(sourceConnId); const tgt = getConnection(targetConnId); if (!src || !tgt) throw new Error("connection not found"); let devUrl: string; try { devUrl = prepareDevDatabase(tgt); } catch (e) { const err = e as { stdout?: string; stderr?: string; message?: string }; return Promise.resolve({ ok: false, sql: [err.stdout, err.stderr, err.message].filter(Boolean).join("\n"), }); } const args = buildDryRunArgs(connectionUrl(src), connectionUrl(tgt), devUrl); return new Promise((resolve) => { execFile( /* turbopackIgnore: true */ atlasBin(), args, { timeout: 120000, maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => { const sql = [stdout, stderr].filter(Boolean).join("\n"); resolve({ ok: !error, sql }); } ); }); }