Newer
Older
client_dwatch / scripts / local-db / apply-error-summaries.ts
/**
 * `scripts/local-db/20261029_error_summaries.sql` を ★ローカルの dwatch_local にだけ★ 流す実行部。
 *   facility_error_summaries / customer_error_summaries の error_detail を消し、error_list_id(error_lists への外部キー)を足す
 *   (出所: ステージングの information_schema 2026-10-29。先方の DDL 変更に合わせる)。
 *
 * ★実行方法: `npx tsx scripts/local-db/apply-error-summaries.ts`★(先に `apply-error-lists.ts` を流して error_lists があること)
 *   ★DATABASE_URL がローカル(localhost / 127.0.0.1)の dwatch_local を指していなければ、接続する前に止まる★
 *   (`lib/db/local-guard.ts`。シードと同じ歯止め)。`.env.local` の DATABASE_URL がステージングを指している間は、
 *   実行するシェルでだけ DATABASE_URL をローカルに向けて渡すこと(process.env は .env.local より優先される)。
 * ★ステージング・本番には絶対に流さない★ — スキーマは TeamWorks の領域(CLAUDE.md §2 / §7)。
 *
 * ★何度流しても同じ★(SQL 側が information_schema の分岐で書いてある)。
 * ★接続後にも `DATABASE()` が dwatch_local であることを確かめてから流す★(URL の検査だけに頼らない)。
 * ★流す前後で行数が変わらないことを確かめる★(ALTER だけなので行は増減しない)。
 * ★パスワード・接続文字列は出力しない★(CLAUDE.md §9.3)。
 *
 * `mariadb` は `@prisma/adapter-mariadb` の依存として入っているドライバ(直接の依存ではない)。
 *   複数文をまとめて流すため Prisma ではなくこれを使う(apply-error-lists.ts と同じ)。
 */
import dotenv from "dotenv";
dotenv.config({ path: ".env.local" });
dotenv.config();

import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import mariadb from "mariadb";
import { assertLocalDwatchDatabaseUrl } from "../../lib/db/local-guard";

const LOCAL_DATABASE_NAME = "dwatch_local";
const SQL_FILE = join(dirname(fileURLToPath(import.meta.url)), "20261029_error_summaries.sql");
const TABLES = ["facility_error_summaries", "customer_error_summaries"] as const;

async function countRows(connection: mariadb.Connection): Promise<Record<string, number>> {
  const counts: Record<string, number> = {};
  for (const table of TABLES) {
    // 表名は上の定数だけ(外から来た値は入らない)
    const rows = await connection.query<{ n: bigint | number }[]>(`SELECT COUNT(*) AS n FROM ${table}`);
    counts[table] = Number(rows[0]?.n ?? 0);
  }
  return counts;
}

async function main(): Promise<void> {
  const databaseUrl = process.env.DATABASE_URL;
  if (!databaseUrl) {
    throw new Error("[apply-error-summaries] DATABASE_URL が未設定です(値はこのエラーには出しません)。");
  }
  assertLocalDwatchDatabaseUrl(databaseUrl);

  const url = new URL(databaseUrl);
  const connection = await mariadb.createConnection({
    host: url.hostname,
    port: Number(url.port || "3306"),
    user: decodeURIComponent(url.username),
    password: decodeURIComponent(url.password),
    database: LOCAL_DATABASE_NAME,
    multipleStatements: true,
  });
  try {
    const [current] = await connection.query<{ db: string | null }[]>("SELECT DATABASE() AS db");
    if (current?.db !== LOCAL_DATABASE_NAME) {
      throw new Error(`[apply-error-summaries] 接続先の DB が ${LOCAL_DATABASE_NAME} ではありません。中断しました。`);
    }

    const before = await countRows(connection);
    await connection.query(readFileSync(SQL_FILE, "utf8"));
    const after = await countRows(connection);

    for (const table of TABLES) {
      const columns = await connection.query<{ COLUMN_NAME: string; COLUMN_TYPE: string; IS_NULLABLE: string }[]>(
        "SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME IN ('error_list_id', 'error_detail')",
        [LOCAL_DATABASE_NAME, table]
      );
      const foreignKeys = await connection.query<{ n: bigint | number }[]>(
        "SELECT COUNT(*) AS n FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = ? AND TABLE_NAME = ? AND CONSTRAINT_NAME = ? AND REFERENCED_TABLE_NAME = 'error_lists'",
        [LOCAL_DATABASE_NAME, table, `fk_${table}_error_list_id`]
      );
      const listId = columns.find((c) => c.COLUMN_NAME === "error_list_id");
      const hasDetail = columns.some((c) => c.COLUMN_NAME === "error_detail");
      if (!listId || listId.IS_NULLABLE !== "NO" || hasDetail || Number(foreignKeys[0]?.n ?? 0) !== 1) {
        throw new Error(`[apply-error-summaries] 流したあとの ${table} の形が想定と違います(error_list_id / error_detail / 外部キー)。`);
      }
      if (before[table] !== after[table]) {
        throw new Error(`[apply-error-summaries] ${table} の行数が変わりました(${before[table]} → ${after[table]})。`);
      }
      console.log(
        `[apply-error-summaries] ${LOCAL_DATABASE_NAME}.${table}: error_list_id = ${listId.COLUMN_TYPE}(NULL 可: ${listId.IS_NULLABLE})/ error_detail なし / 外部キーあり / 行数 ${after[table]}`
      );
    }
  } finally {
    await connection.end();
  }
}

main().catch((error: unknown) => {
  console.error(error instanceof Error ? error.message : error);
  process.exit(1);
});